Skip to content

Commit adfac3e

Browse files
committed
OIDC: Updated state handling to prevent loss from other requests
Which was occuring in chrome, where background requests to the PWA manifest, or opensearch, endpoint caused OIDC to fail due to lost state since it was only flashed to the session. This persists it with a manual TTL. Added tests to cover. Manually tested against Azure. For #5929
1 parent 9de2943 commit adfac3e

File tree

2 files changed

+47
-14
lines changed

2 files changed

+47
-14
lines changed

app/Access/Controllers/OidcController.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@
99

1010
class OidcController extends Controller
1111
{
12-
protected OidcService $oidcService;
13-
14-
public function __construct(OidcService $oidcService)
15-
{
16-
$this->oidcService = $oidcService;
12+
public function __construct(
13+
protected OidcService $oidcService
14+
) {
1715
$this->middleware('guard:oidc');
1816
}
1917

@@ -30,7 +28,7 @@ public function login()
3028
return redirect('/login');
3129
}
3230

33-
session()->flash('oidc_state', $loginDetails['state']);
31+
session()->put('oidc_state', time() . ':' . $loginDetails['state']);
3432

3533
return redirect($loginDetails['url']);
3634
}
@@ -41,10 +39,16 @@ public function login()
4139
*/
4240
public function callback(Request $request)
4341
{
44-
$storedState = session()->pull('oidc_state');
4542
$responseState = $request->query('state');
43+
$splitState = explode(':', session()->pull('oidc_state', ':'), 2);
44+
if (count($splitState) !== 2) {
45+
$splitState = [null, null];
46+
}
47+
48+
[$storedStateTime, $storedState] = $splitState;
49+
$threeMinutesAgo = time() - 3 * 60;
4650

47-
if ($storedState !== $responseState) {
51+
if (!$storedState || $storedState !== $responseState || intval($storedStateTime) < $threeMinutesAgo) {
4852
$this->showErrorNotification(trans('errors.oidc_fail_authed', ['system' => config('oidc.name')]));
4953

5054
return redirect('/login');
@@ -62,7 +66,7 @@ public function callback(Request $request)
6266
}
6367

6468
/**
65-
* Log the user out then start the OIDC RP-initiated logout process.
69+
* Log the user out, then start the OIDC RP-initiated logout process.
6670
*/
6771
public function logout()
6872
{

tests/Auth/OidcTest.php

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public function test_login_success_flow()
138138
{
139139
// Start auth
140140
$this->post('/oidc/login');
141-
$state = session()->get('oidc_state');
141+
$state = explode(':', session()->get('oidc_state'), 2)[1];
142142

143143
$transactions = $this->mockHttpClient([$this->getMockAuthorizationResponse([
144144
'email' => '[email protected]',
@@ -190,6 +190,35 @@ public function test_callback_fails_if_no_state_present_or_matching()
190190
$this->assertSessionError('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
191191
}
192192

193+
public function test_callback_works_even_if_other_request_made_by_session()
194+
{
195+
$this->mockHttpClient([$this->getMockAuthorizationResponse([
196+
'email' => '[email protected]',
197+
'sub' => 'benny1010101',
198+
])]);
199+
200+
$this->post('/oidc/login');
201+
$state = explode(':', session()->get('oidc_state'), 2)[1];
202+
203+
$this->get('/');
204+
205+
$resp = $this->get("/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state={$state}");
206+
$resp->assertRedirect('/');
207+
}
208+
209+
public function test_callback_fails_if_state_timestamp_is_too_old()
210+
{
211+
$this->post('/oidc/login');
212+
$state = explode(':', session()->get('oidc_state'), 2)[1];
213+
session()->put('oidc_state', (time() - 60 * 4) . ':' . $state);
214+
215+
$this->get('/');
216+
217+
$resp = $this->get("/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state={$state}");
218+
$resp->assertRedirect('/login');
219+
$this->assertSessionError('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
220+
}
221+
193222
public function test_dump_user_details_option_outputs_as_expected()
194223
{
195224
config()->set('oidc.dump_user_details', true);
@@ -797,7 +826,7 @@ public function test_pkce_used_on_authorize_and_access()
797826
{
798827
// Start auth
799828
$resp = $this->post('/oidc/login');
800-
$state = session()->get('oidc_state');
829+
$state = explode(':', session()->get('oidc_state'), 2)[1];
801830

802831
$pkceCode = session()->get('oidc_pkce_code');
803832
$this->assertGreaterThan(30, strlen($pkceCode));
@@ -825,7 +854,7 @@ public function test_userinfo_endpoint_used_if_missing_claims_in_id_token()
825854
{
826855
config()->set('oidc.display_name_claims', 'first_name|last_name');
827856
$this->post('/oidc/login');
828-
$state = session()->get('oidc_state');
857+
$state = explode(':', session()->get('oidc_state'), 2)[1];
829858

830859
$client = $this->mockHttpClient([
831860
$this->getMockAuthorizationResponse(['name' => null]),
@@ -973,7 +1002,7 @@ public function test_userinfo_endpoint_not_called_if_empty_groups_array_provided
9731002
]);
9741003

9751004
$this->post('/oidc/login');
976-
$state = session()->get('oidc_state');
1005+
$state = explode(':', session()->get('oidc_state'), 2)[1];
9771006
$client = $this->mockHttpClient([$this->getMockAuthorizationResponse([
9781007
'groups' => [],
9791008
])]);
@@ -999,7 +1028,7 @@ protected function withAutodiscovery(): void
9991028
protected function runLogin($claimOverrides = [], $additionalHttpResponses = []): TestResponse
10001029
{
10011030
$this->post('/oidc/login');
1002-
$state = session()->get('oidc_state');
1031+
$state = explode(':', session()->get('oidc_state'), 2)[1] ?? '';
10031032
$this->mockHttpClient([$this->getMockAuthorizationResponse($claimOverrides), ...$additionalHttpResponses]);
10041033

10051034
return $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);

0 commit comments

Comments
 (0)