diff --git a/app/Auth/Access/Oidc/OidcIdToken.php b/app/Auth/Access/Oidc/OidcIdToken.php index c955c3b09..ca2c85d09 100644 --- a/app/Auth/Access/Oidc/OidcIdToken.php +++ b/app/Auth/Access/Oidc/OidcIdToken.php @@ -4,35 +4,16 @@ namespace BookStack\Auth\Access\Oidc; class OidcIdToken { - /** - * @var array - */ - protected $header; - - /** - * @var array - */ - protected $payload; - - /** - * @var string - */ - protected $signature; + protected array $header; + protected array $payload; + protected string $signature; + protected string $issuer; + protected array $tokenParts = []; /** * @var array[]|string[] */ - protected $keys; - - /** - * @var string - */ - protected $issuer; - - /** - * @var array - */ - protected $tokenParts = []; + protected array $keys; public function __construct(string $token, string $issuer, array $keys) { @@ -106,6 +87,14 @@ class OidcIdToken return $this->payload; } + /** + * Replace the existing claim data of this token with that provided. + */ + public function replaceClaims(array $claims): void + { + $this->payload = $claims; + } + /** * Validate the structure of the given token and ensure we have the required pieces. * As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2. diff --git a/app/Auth/Access/Oidc/OidcService.php b/app/Auth/Access/Oidc/OidcService.php index 1ca5e19a2..3da8b76eb 100644 --- a/app/Auth/Access/Oidc/OidcService.php +++ b/app/Auth/Access/Oidc/OidcService.php @@ -9,6 +9,8 @@ use BookStack\Auth\User; use BookStack\Exceptions\JsonDebugException; use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Exceptions\UserRegistrationException; +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider; @@ -21,24 +23,12 @@ use Psr\Http\Client\ClientInterface as HttpClient; */ class OidcService { - protected RegistrationService $registrationService; - protected LoginService $loginService; - protected HttpClient $httpClient; - protected GroupSyncService $groupService; - - /** - * OpenIdService constructor. - */ public function __construct( - RegistrationService $registrationService, - LoginService $loginService, - HttpClient $httpClient, - GroupSyncService $groupService + protected RegistrationService $registrationService, + protected LoginService $loginService, + protected HttpClient $httpClient, + protected GroupSyncService $groupService ) { - $this->registrationService = $registrationService; - $this->loginService = $loginService; - $this->httpClient = $httpClient; - $this->groupService = $groupService; } /** @@ -226,6 +216,16 @@ class OidcService $settings->keys, ); + $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [ + 'access_token' => $accessToken->getToken(), + 'expires_in' => $accessToken->getExpires(), + 'refresh_token' => $accessToken->getRefreshToken(), + ]); + + if (!is_null($returnClaims)) { + $idToken->replaceClaims($returnClaims); + } + if ($this->config()['dump_user_details']) { throw new JsonDebugException($idToken->getAllClaims()); } diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 91f4fcd67..aacef80cf 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -70,6 +70,19 @@ class ThemeEvents */ const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure'; + /** + * OIDC ID token pre-validate event. + * Runs just before BookStack validates the user ID token data upon login. + * Provides the existing found set of claims for the user as a key-value array, + * along with an array of the proceeding access token data provided by the identity platform. + * If the listener returns a non-null value, that will replace the existing ID token claim data. + * + * @param array $idTokenData + * @param array $accessTokenData + * @returns array|null + */ + const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate'; + /** * Page include parse event. * Runs when a page include tag is being parsed, typically when page content is being processed for viewing. diff --git a/tests/Auth/OidcTest.php b/tests/Auth/OidcTest.php index 35acb7752..41727e7b7 100644 --- a/tests/Auth/OidcTest.php +++ b/tests/Auth/OidcTest.php @@ -5,6 +5,8 @@ namespace Tests\Auth; use BookStack\Actions\ActivityType; use BookStack\Auth\Role; use BookStack\Auth\User; +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use Illuminate\Testing\TestResponse; @@ -397,7 +399,6 @@ class OidcTest extends TestCase config()->set([ 'oidc.external_id_claim' => 'super_awesome_id', ]); - $roleA = Role::factory()->create(['display_name' => 'Wizards']); $resp = $this->runLogin([ 'email' => 'benny@example.com', @@ -464,6 +465,60 @@ class OidcTest extends TestCase $this->assertTrue($user->hasRole($roleA->id)); } + public function test_oidc_id_token_pre_validate_theme_event_without_return() + { + $args = []; + $callback = function (...$eventArgs) use (&$args) { + $args = $eventArgs; + }; + Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback); + + $resp = $this->runLogin([ + 'email' => 'benny@example.com', + 'sub' => 'benny1010101', + 'name' => 'Benny', + ]); + $resp->assertRedirect('/'); + + $this->assertDatabaseHas('users', [ + 'external_auth_id' => 'benny1010101', + ]); + + $this->assertArrayHasKey('iss', $args[0]); + $this->assertArrayHasKey('sub', $args[0]); + $this->assertEquals('Benny', $args[0]['name']); + $this->assertEquals('benny1010101', $args[0]['sub']); + + $this->assertArrayHasKey('access_token', $args[1]); + $this->assertArrayHasKey('expires_in', $args[1]); + $this->assertArrayHasKey('refresh_token', $args[1]); + } + + public function test_oidc_id_token_pre_validate_theme_event_with_return() + { + $callback = function (...$eventArgs) { + return array_merge($eventArgs[0], [ + 'email' => 'lenny@example.com', + 'sub' => 'lenny1010101', + 'name' => 'Lenny', + ]); + }; + Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback); + + $resp = $this->runLogin([ + 'email' => 'benny@example.com', + 'sub' => 'benny1010101', + 'name' => 'Benny', + ]); + $resp->assertRedirect('/'); + + $this->assertDatabaseHas('users', [ + 'email' => 'lenny@example.com', + 'external_auth_id' => 'lenny1010101', + 'name' => 'Lenny', + ]); + } + protected function withAutodiscovery() { config()->set([ diff --git a/tests/ThemeTest.php b/tests/ThemeTest.php index 03ae7b307..bc8163056 100644 --- a/tests/ThemeTest.php +++ b/tests/ThemeTest.php @@ -23,8 +23,8 @@ use League\CommonMark\Environment\Environment; class ThemeTest extends TestCase { - protected $themeFolderName; - protected $themeFolderPath; + protected string $themeFolderName; + protected string $themeFolderPath; public function test_translation_text_can_be_overridden_via_theme() {