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)
{
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.
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;
*/
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;
}
/**
$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());
}
*/
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.
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;
config()->set([
'oidc.external_id_claim' => 'super_awesome_id',
]);
- $roleA = Role::factory()->create(['display_name' => 'Wizards']);
$resp = $this->runLogin([
$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([
+ '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], [
+ 'sub' => 'lenny1010101',
+ 'name' => 'Lenny',
+ ]);
+ };
+ Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $callback);
+
+ $resp = $this->runLogin([
+ 'sub' => 'benny1010101',
+ 'name' => 'Benny',
+ ]);
+ $resp->assertRedirect('/');
+
+ $this->assertDatabaseHas('users', [
+ 'external_auth_id' => 'lenny1010101',
+ 'name' => 'Lenny',
+ ]);
+ }
+
protected function withAutodiscovery()
{
config()->set([
class ThemeTest extends TestCase
{
- protected $themeFolderName;
- protected $themeFolderPath;
+ protected string $themeFolderName;
+ protected string $themeFolderPath;
public function test_translation_text_can_be_overridden_via_theme()
{