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
# OpenID Connect authentication configuration
OPENID_CLIENT_ID=null
OPENID_CLIENT_SECRET=null
OPENID_ISSUER=https://example.com
OPENID_PUBLIC_KEY=file:///my/public.key
OPENID_URL_AUTHORIZE=https://example.com/authorize
OPENID_URL_TOKEN=https://example.com/token
OIDC_NAME=SSO
OIDC_DISPLAY_NAME_CLAIMS=name
OIDC_CLIENT_ID=null
OIDC_CLIENT_SECRET=null
OIDC_ISSUER=null
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
# Service-specific options will override this option

View File

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

View File

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

View File

@ -26,7 +26,7 @@ class Saml2Service extends ExternalAuthService
/**
* Saml2Service constructor.
*/
public function __construct(RegistrationService $registrationService, LoginService $loginService, User $user),
public function __construct(RegistrationService $registrationService, LoginService $loginService, User $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