6767
6868#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE )]
6969class LoginController extends BaseOidcController {
70+ // these keys (state, nonce, login_providerid, redirect, code_verifier, timestamp)
71+ // are suffixed with the state value so they can be stored once per login flow
7072 private const STATE = 'oidc.state ' ;
7173 private const NONCE = 'oidc.nonce ' ;
72- public const PROVIDERID = 'oidc.providerid ' ;
74+ // this is the provider ID we store during the login flow (set by login, get by code)
75+ public const LOGIN_PROVIDERID = 'oidc.login.providerid ' ;
7376 public const REDIRECT_AFTER_LOGIN = 'oidc.redirect ' ;
74- private const ID_TOKEN = 'oidc.id_token ' ;
7577 private const CODE_VERIFIER = 'oidc.code_verifier ' ;
78+ private const TIMESTAMP = 'oidc.timestamp ' ;
79+
80+ // this is the provider ID we store once the authentication was successful
81+ // it is used by the singleLogout endpoint and the user backend
82+ public const PROVIDERID = 'oidc.providerid ' ;
83+ // this id token is used to send id_token_hint to the IdP logout endpoint
84+ private const ID_TOKEN = 'oidc.id_token ' ;
85+
86+ // we consider that a login flow should complete within 5 minutes
87+ private const LOGIN_FLOW_TIMEOUT = 300 ;
7688
7789 public function __construct (
7890 IRequest $ request ,
@@ -189,12 +201,15 @@ public function login(int $providerId, ?string $redirectUrl = null) {
189201 }
190202
191203 $ state = $ this ->random ->generate (32 , ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER );
192- $ this ->session ->set (self ::STATE , $ state );
204+ $ sessionKeySuffix = '- ' . $ state ;
205+ $ this ->session ->set (self ::STATE . $ sessionKeySuffix , $ state );
193206 $ this ->logger ->debug ('Storing OIDC state ' , ['state ' => $ state ]);
194- $ this ->session ->set (self ::REDIRECT_AFTER_LOGIN , $ redirectUrl );
207+ $ timestamp = $ this ->timeFactory ->getTime ();
208+ $ this ->session ->set (self ::TIMESTAMP . $ sessionKeySuffix , $ timestamp );
209+ $ this ->session ->set (self ::REDIRECT_AFTER_LOGIN . $ sessionKeySuffix , $ redirectUrl );
195210
196211 $ nonce = $ this ->random ->generate (32 , ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER );
197- $ this ->session ->set (self ::NONCE , $ nonce );
212+ $ this ->session ->set (self ::NONCE . $ sessionKeySuffix , $ nonce );
198213
199214 $ oidcSystemConfig = $ this ->config ->getSystemValue ('user_oidc ' , []);
200215 $ isPkceSupported = in_array ('S256 ' , $ discovery ['code_challenge_methods_supported ' ] ?? [], true );
@@ -203,10 +218,10 @@ public function login(int $providerId, ?string $redirectUrl = null) {
203218 if ($ isPkceEnabled ) {
204219 // PKCE code_challenge see https://datatracker.ietf.org/doc/html/rfc7636
205220 $ code_verifier = $ this ->random ->generate (128 , ISecureRandom::CHAR_DIGITS . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER );
206- $ this ->session ->set (self ::CODE_VERIFIER , $ code_verifier );
221+ $ this ->session ->set (self ::CODE_VERIFIER . $ sessionKeySuffix , $ code_verifier );
207222 }
208223
209- $ this ->session ->set (self ::PROVIDERID , $ providerId );
224+ $ this ->session ->set (self ::LOGIN_PROVIDERID . $ sessionKeySuffix , $ providerId );
210225 $ this ->session ->close ();
211226
212227 // get attribute mapping settings
@@ -360,13 +375,22 @@ public function code(string $state = '', string $code = '', string $scope = '',
360375 return $ this ->build403TemplateResponse ($ message , Http::STATUS_BAD_REQUEST , [], false );
361376 }
362377
363- $ storedState = $ this ->session ->get (self ::STATE );
378+ $ sessionKeySuffix = '- ' . $ state ;
379+ $ storedState = $ this ->session ->get (self ::STATE . $ sessionKeySuffix );
380+
381+ $ currentTimestamp = $ this ->timeFactory ->getTime ();
382+ $ sessionTimestamp = $ this ->session ->get (self ::TIMESTAMP . $ sessionKeySuffix );
383+ if ($ currentTimestamp - $ sessionTimestamp > self ::LOGIN_FLOW_TIMEOUT ) {
384+ // the state, nonce etc... were stored too long ago, the login flow has expired
385+ $ message = $ this ->l10n ->t ('The received state has expired. ' );
386+ return $ this ->build403TemplateResponse ($ message , Http::STATUS_FORBIDDEN , [], false );
387+ }
364388
365389 if ($ storedState !== $ state ) {
366390 $ this ->logger ->warning ('state does not match ' , [
367391 'got ' => $ state ,
368392 'expected ' => $ storedState ,
369- 'state_exists_in_session ' => $ this ->session ->exists (self ::STATE ),
393+ 'state_exists_in_session ' => $ this ->session ->exists (self ::STATE . $ sessionKeySuffix ),
370394 ]);
371395
372396 $ message = $ this ->l10n ->t ('The received state does not match the expected value. ' );
@@ -376,15 +400,15 @@ public function code(string $state = '', string $code = '', string $scope = '',
376400 'error_description ' => $ message ,
377401 'got ' => $ state ,
378402 'expected ' => $ storedState ,
379- 'state_exists_in_session ' => $ this ->session ->exists (self ::STATE ),
403+ 'state_exists_in_session ' => $ this ->session ->exists (self ::STATE . $ sessionKeySuffix ),
380404 ];
381405 return new JSONResponse ($ responseData , Http::STATUS_FORBIDDEN );
382406 }
383407 // we know debug mode is off, always throttle
384408 return $ this ->build403TemplateResponse ($ message , Http::STATUS_FORBIDDEN , ['reason ' => 'state does not match ' ], true );
385409 }
386410
387- $ providerId = (int )$ this ->session ->get (self ::PROVIDERID );
411+ $ providerId = (int )$ this ->session ->get (self ::LOGIN_PROVIDERID . $ sessionKeySuffix );
388412 $ provider = $ this ->providerMapper ->getProvider ($ providerId );
389413 try {
390414 $ providerClientSecret = $ this ->crypto ->decrypt ($ provider ->getClientSecret ());
@@ -409,7 +433,8 @@ public function code(string $state = '', string $code = '', string $scope = '',
409433 'grant_type ' => 'authorization_code ' ,
410434 ];
411435 if ($ isPkceEnabled ) {
412- $ requestBody ['code_verifier ' ] = $ this ->session ->get (self ::CODE_VERIFIER ); // Set for the PKCE flow
436+ // Set for the PKCE flow
437+ $ requestBody ['code_verifier ' ] = $ this ->session ->get (self ::CODE_VERIFIER . $ sessionKeySuffix );
413438 }
414439
415440 $ headers = [];
@@ -558,7 +583,7 @@ public function code(string $state = '', string $code = '', string $scope = '',
558583 }
559584 }
560585
561- if (isset ($ idTokenPayload ->nonce ) && $ idTokenPayload ->nonce !== $ this ->session ->get (self ::NONCE )) {
586+ if (isset ($ idTokenPayload ->nonce ) && $ idTokenPayload ->nonce !== $ this ->session ->get (self ::NONCE . $ sessionKeySuffix )) {
562587 $ this ->logger ->debug ('Nonce does not match ' );
563588 $ message = $ this ->l10n ->t ('The nonce does not match ' );
564589 return $ this ->build403TemplateResponse ($ message , Http::STATUS_FORBIDDEN , ['reason ' => 'invalid nonce ' ]);
@@ -627,6 +652,7 @@ public function code(string $state = '', string $code = '', string $scope = '',
627652 }
628653
629654 $ this ->session ->set (self ::ID_TOKEN , $ idTokenRaw );
655+ $ this ->session ->set (self ::PROVIDERID , $ providerId );
630656
631657 $ this ->logger ->debug ('Logging user in ' );
632658
@@ -692,7 +718,7 @@ public function code(string $state = '', string $code = '', string $scope = '',
692718
693719 $ this ->logger ->debug ('Redirecting user ' );
694720
695- $ redirectUrl = $ this ->session ->get (self ::REDIRECT_AFTER_LOGIN );
721+ $ redirectUrl = $ this ->session ->get (self ::REDIRECT_AFTER_LOGIN . $ sessionKeySuffix );
696722 if ($ redirectUrl ) {
697723 return $ this ->getRedirectResponse ($ redirectUrl );
698724 }
0 commit comments