/**
* Initiate an authorization flow.
+ * Provides back an authorize redirect URL, in addition to other
+ * details which may be required for the auth flow.
*
* @throws OidcException
*
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
+
+ $url = $provider->getAuthorizationUrl();
+ session()->put('oidc_pkce_code', $provider->getPkceCode() ?? '');
+
return [
- 'url' => $provider->getAuthorizationUrl(),
+ 'url' => $url,
'state' => $provider->getState(),
];
}
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
+ // Set PKCE code flashed at login
+ $pkceCode = session()->pull('oidc_pkce_code', '');
+ $provider->setPkceCode($pkceCode);
+
// Try to exchange authorization code for access token
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $authorizationCode,
'authorizationEndpoint' => $config['authorization_endpoint'],
'tokenEndpoint' => $config['token_endpoint'],
'endSessionEndpoint' => is_string($config['end_session_endpoint']) ? $config['end_session_endpoint'] : null,
+ 'userinfoEndpoint' => $config['userinfo_endpoint'],
]);
// Use keys if configured
session()->put("oidc_id_token", $idTokenText);
+ if (!empty($settings->userinfoEndpoint)) {
+ $provider = $this->getProvider($settings);
+ $request = $provider->getAuthenticatedRequest('GET', $settings->userinfoEndpoint, $accessToken->getToken());
+ $response = $provider->getParsedResponse($request);
+ $claims = $idToken->getAllClaims();
+ foreach ($response as $key => $value) {
+ $claims[$key] = $value;
+ }
+ $idToken->replaceClaims($claims);
+ }
+
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
'access_token' => $accessToken->getToken(),
'expires_in' => $accessToken->getExpires(),
{
config()->set(['oidc.end_session_endpoint' => 'https://example.com/logout']);
- $this->runLogin();
+ // Fix times so our token is predictable
+ $claimOverrides = [
+ 'iat' => time(),
+ 'exp' => time() + 720,
+ 'auth_time' => time()
+ ];
+ $this->runLogin($claimOverrides);
$resp = $this->asEditor()->post('/oidc/logout');
- $query = 'id_token_hint=' . urlencode(OidcJwtHelper::idToken()) . '&post_logout_redirect_uri=' . urlencode(url('/'));
+ $query = 'id_token_hint=' . urlencode(OidcJwtHelper::idToken($claimOverrides)) . '&post_logout_redirect_uri=' . urlencode(url('/'));
$resp->assertRedirect('https://example.com/logout?' . $query);
}
]);
}
+ public function test_pkce_used_on_authorize_and_access()
+ {
+ // Start auth
+ $resp = $this->post('/oidc/login');
+ $state = session()->get('oidc_state');
+
+ $pkceCode = session()->get('oidc_pkce_code');
+ $this->assertGreaterThan(30, strlen($pkceCode));
+
+ $expectedCodeChallenge = trim(strtr(base64_encode(hash('sha256', $pkceCode, true)), '+/', '-_'), '=');
+ $redirect = $resp->headers->get('Location');
+ $redirectParams = [];
+ parse_str(parse_url($redirect, PHP_URL_QUERY), $redirectParams);
+ $this->assertEquals($expectedCodeChallenge, $redirectParams['code_challenge']);
+ $this->assertEquals('S256', $redirectParams['code_challenge_method']);
+
+ $transactions = $this->mockHttpClient([$this->getMockAuthorizationResponse([
+ 'sub' => 'benny1010101',
+ ])]);
+
+ $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);
+ $tokenRequest = $transactions->latestRequest();
+ $bodyParams = [];
+ parse_str($tokenRequest->getBody(), $bodyParams);
+ $this->assertEquals($pkceCode, $bodyParams['code_verifier']);
+ }
+
protected function withAutodiscovery()
{
config()->set([
protected function runLogin($claimOverrides = []): TestResponse
{
+ // These two variables should perhaps be arguments instead of
+ // assuming that they're tied to whether discovery is enabled,
+ // but that's how the tests are written for now.
+ $claimsInIdToken = !config('oidc.discover');
+ $tokenEndpoint = config('oidc.discover')
+ ? OidcJwtHelper::defaultIssuer() . '/oidc/token'
+ : 'https://oidc.local/token';
+
$this->post('/oidc/login');
$state = session()->get('oidc_state');
- $this->mockHttpClient([$this->getMockAuthorizationResponse($claimOverrides)]);
- return $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);
+ $providerResponses = [$this->getMockAuthorizationResponse($claimsInIdToken ? $claimOverrides : [])];
+ if (!$claimsInIdToken) {
+ $providerResponses[] = new Response(200, [
+ 'Content-Type' => 'application/json',
+ 'Cache-Control' => 'no-cache, no-store',
+ 'Pragma' => 'no-cache',
+ ], json_encode($claimOverrides));
+ }
+
+ $transactions = $this->mockHttpClient($providerResponses);
+
+ $response = $this->get('/oidc/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=' . $state);
+
+ if (auth()->check()) {
+ $this->assertEquals($claimsInIdToken ? 1 : 2, $transactions->requestCount());
+ $tokenRequest = $transactions->requestAt(0);
+ $this->assertEquals($tokenEndpoint, (string) $tokenRequest->getUri());
+ $this->assertEquals('POST', $tokenRequest->getMethod());
+ if (!$claimsInIdToken) {
+ $userinfoRequest = $transactions->requestAt(1);
+ $this->assertEquals(OidcJwtHelper::defaultIssuer() . '/oidc/userinfo', (string) $userinfoRequest->getUri());
+ $this->assertEquals('GET', $userinfoRequest->getMethod());
+ $this->assertEquals('Bearer abc123', $userinfoRequest->getHeader('Authorization')[0]);
+ }
+ }
+
+ return $response;
}
protected function getAutoDiscoveryResponse($responseOverrides = []): Response
], json_encode(array_merge([
'token_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/token',
'authorization_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/authorize',
+ 'userinfo_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/userinfo',
'jwks_uri' => OidcJwtHelper::defaultIssuer() . '/oidc/keys',
'issuer' => OidcJwtHelper::defaultIssuer(),
'end_session_endpoint' => OidcJwtHelper::defaultIssuer() . '/oidc/logout',