Started refactor for merge of OIDC

- Made oidc config more generic to not be overly reliant on the library
  based upon learnings from saml2 auth.
- Removed any settings that are redundant or not deemed required for
  initial implementation.
- Reduced some methods down where not needed.
- Renamed OpenID to OIDC
- Updated .env.example.complete to align with all options and their
  defaults

Related to #2169
This commit is contained in:
Dan Brown 2021-10-06 17:12:01 +01:00
parent 193d7fb3fe
commit 2ec0aa85ca
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 283 additions and 338 deletions

View File

@ -240,12 +240,15 @@ SAML2_GROUP_ATTRIBUTE=group
SAML2_REMOVE_FROM_GROUPS=false SAML2_REMOVE_FROM_GROUPS=false
# OpenID Connect authentication configuration # OpenID Connect authentication configuration
OPENID_CLIENT_ID=null OIDC_NAME=SSO
OPENID_CLIENT_SECRET=null OIDC_DISPLAY_NAME_CLAIMS=name
OPENID_ISSUER=https://example.com OIDC_CLIENT_ID=null
OPENID_PUBLIC_KEY=file:///my/public.key OIDC_CLIENT_SECRET=null
OPENID_URL_AUTHORIZE=https://example.com/authorize OIDC_ISSUER=null
OPENID_URL_TOKEN=https://example.com/token OIDC_PUBLIC_KEY=null
OIDC_AUTH_ENDPOINT=null
OIDC_TOKEN_ENDPOINT=null
OIDC_DUMP_USER_DETAILS=false
# Disable default third-party services such as Gravatar and Draw.IO # Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option # Service-specific options will override this option

View File

@ -4,8 +4,8 @@ namespace BookStack\Auth\Access;
use BookStack\Auth\Role; use BookStack\Auth\Role;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Exceptions\UserRegistrationException;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class ExternalAuthService class ExternalAuthService

View File

@ -5,9 +5,11 @@ use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\OpenIdException; use BookStack\Exceptions\OpenIdException;
use BookStack\Exceptions\UserRegistrationException; use BookStack\Exceptions\UserRegistrationException;
use Exception; use Exception;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token; use Lcobucci\JWT\Token;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use OpenIDConnectClient\AccessToken; use OpenIDConnectClient\AccessToken;
use OpenIDConnectClient\Exception\InvalidTokenException;
use OpenIDConnectClient\OpenIDConnectProvider; use OpenIDConnectClient\OpenIDConnectProvider;
/** /**
@ -25,12 +27,12 @@ class OpenIdService extends ExternalAuthService
{ {
parent::__construct($registrationService, $user); parent::__construct($registrationService, $user);
$this->config = config('openid'); $this->config = config('oidc');
} }
/** /**
* Initiate a authorization flow. * Initiate an authorization flow.
* @throws Error * @throws Exception
*/ */
public function login(): array public function login(): array
{ {
@ -43,7 +45,6 @@ class OpenIdService extends ExternalAuthService
/** /**
* Initiate a logout flow. * Initiate a logout flow.
* @throws Error
*/ */
public function logout(): array public function logout(): array
{ {
@ -56,7 +57,7 @@ class OpenIdService extends ExternalAuthService
/** /**
* Refresh the currently logged in user. * Refresh the currently logged in user.
* @throws Error * @throws Exception
*/ */
public function refresh(): bool public function refresh(): bool
{ {
@ -79,7 +80,7 @@ class OpenIdService extends ExternalAuthService
// Try to obtain refreshed access token // Try to obtain refreshed access token
try { try {
$newAccessToken = $this->refreshAccessToken($accessToken); $newAccessToken = $this->refreshAccessToken($accessToken);
} catch (\Exception $e) { } catch (Exception $e) {
// Log out if an unknown problem arises // Log out if an unknown problem arises
$this->actionLogout(); $this->actionLogout();
throw $e; throw $e;
@ -110,7 +111,7 @@ class OpenIdService extends ExternalAuthService
/** /**
* Generate an updated access token, through the associated refresh token. * Generate an updated access token, through the associated refresh token.
* @throws Error * @throws Exception
*/ */
protected function refreshAccessToken(AccessToken $accessToken): ?AccessToken protected function refreshAccessToken(AccessToken $accessToken): ?AccessToken
{ {
@ -135,11 +136,8 @@ class OpenIdService extends ExternalAuthService
* return the matching, or new if registration active, user matched to * return the matching, or new if registration active, user matched to
* the authorization server. * the authorization server.
* Returns null if not authenticated. * Returns null if not authenticated.
* @throws Error * @throws Exception
* @throws OpenIdException * @throws InvalidTokenException
* @throws ValidationError
* @throws JsonDebugException
* @throws UserRegistrationException
*/ */
public function processAuthorizeResponse(?string $authorizationCode): ?User public function processAuthorizeResponse(?string $authorizationCode): ?User
{ {
@ -164,87 +162,50 @@ class OpenIdService extends ExternalAuthService
/** /**
* Load the underlying OpenID Connect Provider. * Load the underlying OpenID Connect Provider.
* @throws Error
* @throws Exception
*/ */
protected function getProvider(): OpenIDConnectProvider protected function getProvider(): OpenIDConnectProvider
{ {
// Setup settings // Setup settings
$settings = $this->config['openid']; $settings = [
$overrides = $this->config['openid_overrides'] ?? []; 'clientId' => $this->config['client_id'],
'clientSecret' => $this->config['client_secret'],
if ($overrides && is_string($overrides)) { 'idTokenIssuer' => $this->config['issuer'],
$overrides = json_decode($overrides, true); 'redirectUri' => url('/openid/redirect'),
} 'urlAuthorize' => $this->config['authorization_endpoint'],
'urlAccessToken' => $this->config['token_endpoint'],
$openIdSettings = $this->loadOpenIdDetails(); 'urlResourceOwnerDetails' => null,
$settings = array_replace_recursive($settings, $openIdSettings, $overrides); 'publicKey' => $this->config['jwt_public_key'],
'scopes' => 'profile email',
];
// Setup services // Setup services
$services = $this->loadOpenIdServices(); $services = [
$overrides = $this->config['openid_services'] ?? []; 'signer' => new Sha256(),
];
$services = array_replace_recursive($services, $overrides);
return new OpenIDConnectProvider($settings, $services); return new OpenIDConnectProvider($settings, $services);
} }
/**
* Load services utilized by the OpenID Connect provider.
*/
protected function loadOpenIdServices(): array
{
return [
'signer' => new \Lcobucci\JWT\Signer\Rsa\Sha256(),
];
}
/**
* Load dynamic service provider options required by the OpenID Connect provider.
*/
protected function loadOpenIdDetails(): array
{
return [
'redirectUri' => url('/openid/redirect'),
];
}
/** /**
* Calculate the display name * Calculate the display name
*/ */
protected function getUserDisplayName(Token $token, string $defaultValue): string protected function getUserDisplayName(Token $token, string $defaultValue): string
{ {
$displayNameAttr = $this->config['display_name_attributes']; $displayNameAttr = $this->config['display_name_claims'];
$displayName = []; $displayName = [];
foreach ($displayNameAttr as $dnAttr) { foreach ($displayNameAttr as $dnAttr) {
$dnComponent = $token->getClaim($dnAttr, ''); $dnComponent = $token->claims()->get($dnAttr, '');
if ($dnComponent !== '') { if ($dnComponent !== '') {
$displayName[] = $dnComponent; $displayName[] = $dnComponent;
} }
} }
if (count($displayName) == 0) { if (count($displayName) == 0) {
$displayName = $defaultValue; $displayName[] = $defaultValue;
} else {
$displayName = implode(' ', $displayName);
} }
return $displayName; return implode(' ', $displayName);;
}
/**
* Get the value to use as the external id saved in BookStack
* used to link the user to an existing BookStack DB user.
*/
protected function getExternalId(Token $token, string $defaultValue)
{
$userNameAttr = $this->config['external_id_attribute'];
if ($userNameAttr === null) {
return $defaultValue;
}
return $token->getClaim($userNameAttr, $defaultValue);
} }
/** /**
@ -252,16 +213,11 @@ class OpenIdService extends ExternalAuthService
*/ */
protected function getUserDetails(Token $token): array protected function getUserDetails(Token $token): array
{ {
$email = null; $id = $token->claims()->get('sub');
$emailAttr = $this->config['email_attribute'];
if ($token->hasClaim($emailAttr)) {
$email = $token->getClaim($emailAttr);
}
return [ return [
'external_id' => $token->getClaim('sub'), 'external_id' => $id,
'email' => $email, 'email' => $token->claims()->get('email'),
'name' => $this->getUserDisplayName($token, $email), 'name' => $this->getUserDisplayName($token, $id),
]; ];
} }

View File

@ -26,7 +26,7 @@ class Saml2Service extends ExternalAuthService
/** /**
* Saml2Service constructor. * Saml2Service constructor.
*/ */
public function __construct(RegistrationService $registrationService, LoginService $loginService, User $user), public function __construct(RegistrationService $registrationService, LoginService $loginService, User $user)
{ {
parent::__construct($registrationService, $user); parent::__construct($registrationService, $user);

30
app/Config/oidc.php Normal file
View File

@ -0,0 +1,30 @@
<?php
return [
// Display name, shown to users, for OpenId option
'name' => env('OIDC_NAME', 'SSO'),
// Dump user details after a login request for debugging purposes
'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),
// Attribute, within a OpenId token, to find the user's display name
'display_name_claims' => explode('|', env('OIDC_DISPLAY_NAME_CLAIMS', 'name')),
// OAuth2/OpenId client id, as configured in your Authorization server.
'client_id' => env('OIDC_CLIENT_ID', null),
// OAuth2/OpenId client secret, as configured in your Authorization server.
'client_secret' => env('OIDC_CLIENT_SECRET', null),
// The issuer of the identity token (id_token) this will be compared with what is returned in the token.
'issuer' => env('OIDC_ISSUER', null),
// Public key that's used to verify the JWT token with.
// Can be the key value itself or a local 'file://public.key' reference.
'jwt_public_key' => env('OIDC_PUBLIC_KEY', null),
// OAuth2 endpoints.
'authorization_endpoint' => env('OIDC_AUTH_ENDPOINT', null),
'token_endpoint' => env('OIDC_TOKEN_ENDPOINT', null),
];

View File

@ -1,46 +0,0 @@
<?php
return [
// Display name, shown to users, for OpenId option
'name' => env('OPENID_NAME', 'SSO'),
// Dump user details after a login request for debugging purposes
'dump_user_details' => env('OPENID_DUMP_USER_DETAILS', false),
// Attribute, within a OpenId token, to find the user's email address
'email_attribute' => env('OPENID_EMAIL_ATTRIBUTE', 'email'),
// Attribute, within a OpenId token, to find the user's display name
'display_name_attributes' => explode('|', env('OPENID_DISPLAY_NAME_ATTRIBUTES', 'name')),
// Attribute, within a OpenId token, to use to connect a BookStack user to the OpenId user.
'external_id_attribute' => env('OPENID_EXTERNAL_ID_ATTRIBUTE', null),
// Overrides, in JSON format, to the configuration passed to underlying OpenIDConnectProvider library.
'openid_overrides' => env('OPENID_OVERRIDES', null),
// Custom service instances, used by the underlying OpenIDConnectProvider library
'openid_services' => [],
'openid' => [
// OAuth2/OpenId client id, as configured in your Authorization server.
'clientId' => env('OPENID_CLIENT_ID', ''),
// OAuth2/OpenId client secret, as configured in your Authorization server.
'clientSecret' => env('OPENID_CLIENT_SECRET', ''),
// OAuth2 scopes that are request, by default the OpenId-native profile and email scopes.
'scopes' => 'profile email',
// The issuer of the identity token (id_token) this will be compared with what is returned in the token.
'idTokenIssuer' => env('OPENID_ISSUER', ''),
// Public key that's used to verify the JWT token with.
'publicKey' => env('OPENID_PUBLIC_KEY', ''),
// OAuth2 endpoints.
'urlAuthorize' => env('OPENID_URL_AUTHORIZE', ''),
'urlAccessToken' => env('OPENID_URL_TOKEN', ''),
'urlResourceOwnerDetails' => env('OPENID_URL_RESOURCE', ''),
],
];

418
composer.lock generated

File diff suppressed because it is too large Load Diff