]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'development' into lukeshu/oidc-development
authorDan Brown <redacted>
Tue, 16 Apr 2024 13:57:36 +0000 (14:57 +0100)
committerDan Brown <redacted>
Tue, 16 Apr 2024 13:57:36 +0000 (14:57 +0100)
1  2 
app/Access/Oidc/OidcService.php
tests/Auth/OidcTest.php

index 244957991953510aa4f647c31d7f829b143c621e,036c9fc47efcf7ac32f0908c57337743d0f005b5..467e31417704931412ef4100b11ed03154a5d566
@@@ -33,6 -33,8 +33,8 @@@ class OidcServic
  
      /**
       * 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,
@@@ -85,7 -95,6 +95,7 @@@
              '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(),
diff --combined tests/Auth/OidcTest.php
index f47a201005df627a170f7429cef883229cd9b599,228c75e9eade8f29e5daf3adfa08e1d725f23c78..6617229838fd0fedf81a02de1d0cd4d9ae7ce86c
@@@ -594,10 -594,16 +594,16 @@@ class OidcTest extends TestCas
      {
          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([
+             'email' => '[email protected]',
+             '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',