30
30
use Symfony \Component \Routing \Generator \UrlGeneratorInterface ;
31
31
use Symfony \Component \Security \Core \Authentication \Token \Storage \TokenStorageInterface ;
32
32
use Symfony \Component \Security \Core \Exception \AccessDeniedException ;
33
+ use Symfony \Component \Security \Core \Exception \InvalidCsrfTokenException ;
33
34
use Symfony \Component \Security \Core \User \UserInterface ;
35
+ use Symfony \Component \Security \Csrf \CsrfToken ;
36
+ use Symfony \Component \Security \Csrf \CsrfTokenManagerInterface ;
34
37
use Twig \Environment as TwigEnvironment ;
35
38
36
39
/**
@@ -95,6 +98,11 @@ class AuthorizeController
95
98
*/
96
99
private $ eventDispatcher ;
97
100
101
+ /**
102
+ * @var CsrfTokenManagerInterface
103
+ */
104
+ private $ csrfTokenManager ;
105
+
98
106
/**
99
107
* This controller had been made as a service due to support symfony 4 where all* services are private by default.
100
108
* Thus, this is considered a bad practice to fetch services directly from container.
@@ -113,6 +121,7 @@ public function __construct(
113
121
ClientManagerInterface $ clientManager ,
114
122
EventDispatcherInterface $ eventDispatcher ,
115
123
TwigEnvironment $ twig ,
124
+ CsrfTokenManagerInterface $ csrfTokenManager ,
116
125
SessionInterface $ session = null
117
126
) {
118
127
$ this ->requestStack = $ requestStack ;
@@ -124,6 +133,7 @@ public function __construct(
124
133
$ this ->router = $ router ;
125
134
$ this ->clientManager = $ clientManager ;
126
135
$ this ->eventDispatcher = $ eventDispatcher ;
136
+ $ this ->csrfTokenManager = $ csrfTokenManager ;
127
137
$ this ->twig = $ twig ;
128
138
}
129
139
@@ -134,17 +144,21 @@ public function authorizeAction(Request $request)
134
144
{
135
145
$ user = $ this ->tokenStorage ->getToken ()->getUser ();
136
146
147
+ $ form = $ this ->authorizeForm ;
148
+ $ formHandler = $ this ->authorizeFormHandler ;
149
+
137
150
if (!$ user instanceof UserInterface) {
138
151
throw new AccessDeniedException ('This user does not have access to this section. ' );
139
152
}
140
153
141
154
if ($ this ->session && true === $ this ->session ->get ('_fos_oauth_server.ensure_logout ' )) {
155
+ $ this ->checkCsrfTokenBeforeInvalidingTheSession ($ form , $ request );
156
+
142
157
$ this ->session ->invalidate (600 );
143
158
$ this ->session ->set ('_fos_oauth_server.ensure_logout ' , true );
144
- }
145
159
146
- $ form = $ this ->authorizeForm ;
147
- $ formHandler = $ this -> authorizeFormHandler ;
160
+ $ this ->regenerateTokenForInvalidatedSession ( $ form , $ request ) ;
161
+ }
148
162
149
163
/** @var PreAuthorizationEvent $event */
150
164
$ event = $ this ->eventDispatcher ->dispatch (new PreAuthorizationEvent ($ user , $ this ->getClient ()));
@@ -216,7 +230,7 @@ protected function getClient()
216
230
217
231
if (null === $ clientId = $ request ->get ('client_id ' )) {
218
232
$ formData = $ request ->get ($ this ->authorizeForm ->getName (), []);
219
- $ clientId = isset ( $ formData ['client_id ' ]) ? $ formData [ ' client_id ' ] : null ;
233
+ $ clientId = $ formData ['client_id ' ] ?? null ;
220
234
}
221
235
222
236
$ this ->client = $ this ->clientManager ->findClientByPublicId ($ clientId );
@@ -247,4 +261,67 @@ private function getCurrentRequest()
247
261
248
262
return $ request ;
249
263
}
264
+
265
+ /**
266
+ * Validate if the current POST CSRF token is valid.
267
+ * We need to do this now as the session will be regenerated due to the `ensure_logout` parameter.
268
+ */
269
+ private function checkCsrfTokenBeforeInvalidingTheSession (Form $ form , Request $ request ): void
270
+ {
271
+ if (!$ request ->isMethod ('POST ' )) {
272
+ // no need to check the CSRF token if we are not on a POST request (ie. submitting the form)
273
+ return ;
274
+ }
275
+
276
+ if (!$ form ->getConfig ()->getOption ('csrf_protection ' )) {
277
+ // no csrf security, no need to validate token
278
+ return ;
279
+ }
280
+
281
+ $ tokenFieldName = $ form ->getConfig ()->getOption ('csrf_field_name ' );
282
+ $ tokenId = $ form ->getConfig ()->getOption ('csrf_token_id ' ) ?? $ form ->getName ();
283
+
284
+ $ formData = $ request ->request ->get ($ form ->getName ());
285
+ $ tokenValue = $ formData [$ tokenFieldName ] ?? null ;
286
+
287
+ $ token = new CsrfToken ($ tokenId , $ tokenValue );
288
+
289
+ if (!$ this ->csrfTokenManager ->isTokenValid ($ token )) {
290
+ throw new InvalidCsrfTokenException ();
291
+ }
292
+ }
293
+
294
+ /**
295
+ * This method will inject a newly regenerated CSRF token into the actual form
296
+ * as Symfony's form manager will check this token upon the current session.
297
+ *
298
+ * As we have regenerate a session, we need to inject the newly generated token into
299
+ * the form data.
300
+ *
301
+ * It does bypass Symfony form CSRF protection, but the CSRF token is validated
302
+ * in the `checkCsrfTokenBeforeInvalidingTheSession` method
303
+ */
304
+ private function regenerateTokenForInvalidatedSession (Form $ form , Request $ request ): void
305
+ {
306
+ if (!$ request ->isMethod ('POST ' )) {
307
+ // no need to check the CSRF token if we are not on a POST request (ie. submitting the form)
308
+ return ;
309
+ }
310
+
311
+ if (!$ form ->getConfig ()->getOption ('csrf_protection ' )) {
312
+ // no csrf security, no need to regenerate a valid token
313
+ return ;
314
+ }
315
+
316
+ $ tokenFieldName = $ form ->getConfig ()->getOption ('csrf_field_name ' );
317
+ $ tokenId = $ form ->getConfig ()->getOption ('csrf_token_id ' ) ?? $ form ->getName ();
318
+
319
+ // regenerate a new token and replace the form data as Symfony's form manager will check this token.
320
+ // the request token has already been checked.
321
+ $ newToken = $ this ->csrfTokenManager ->refreshToken ($ tokenId );
322
+
323
+ $ formData = $ request ->request ->get ($ form ->getName ());
324
+ $ formData [$ tokenFieldName ] = $ newToken ->getValue ();
325
+ $ request ->request ->set ($ form ->getName (), $ formData );
326
+ }
250
327
}
0 commit comments