-<?php namespace BookStack\Auth\Access\Oidc;
+<?php
+namespace BookStack\Auth\Access\Oidc;
+
+use function auth;
use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
-use BookStack\Exceptions\OpenIdConnectException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
-use Exception;
-use GuzzleHttp\Client;
-use Illuminate\Support\Facades\Cache;
-use Psr\Http\Client\ClientExceptionInterface;
-use function auth;
use function config;
+use Illuminate\Support\Facades\Cache;
+use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
+use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
+use Psr\Http\Client\ClientInterface as HttpClient;
use function trans;
use function url;
*/
class OidcService
{
- protected $registrationService;
- protected $loginService;
- protected $config;
+ protected RegistrationService $registrationService;
+ protected LoginService $loginService;
+ protected HttpClient $httpClient;
/**
* OpenIdService constructor.
*/
- public function __construct(RegistrationService $registrationService, LoginService $loginService)
+ public function __construct(RegistrationService $registrationService, LoginService $loginService, HttpClient $httpClient)
{
- $this->config = config('oidc');
$this->registrationService = $registrationService;
$this->loginService = $loginService;
+ $this->httpClient = $httpClient;
}
/**
* Initiate an authorization flow.
+ *
+ * @throws OidcException
+ *
* @return array{url: string, state: string}
*/
public function login(): array
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
+
return [
- 'url' => $provider->getAuthorizationUrl(),
+ 'url' => $provider->getAuthorizationUrl(),
'state' => $provider->getState(),
];
}
/**
* Process the Authorization response from the authorization server and
- * return the matching, or new if registration active, user matched to
- * the authorization server.
- * Returns null if not authenticated.
- * @throws Exception
- * @throws ClientExceptionInterface
+ * return the matching, or new if registration active, user matched to the
+ * authorization server. Throws if the user cannot be auth if not authenticated.
+ *
+ * @throws JsonDebugException
+ * @throws OidcException
+ * @throws StoppedAuthenticationException
+ * @throws IdentityProviderException
*/
- public function processAuthorizeResponse(?string $authorizationCode): ?User
+ public function processAuthorizeResponse(?string $authorizationCode): User
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
}
/**
- * @throws OidcIssuerDiscoveryException
- * @throws ClientExceptionInterface
+ * @throws OidcException
*/
protected function getProviderSettings(): OidcProviderSettings
{
+ $config = $this->config();
$settings = new OidcProviderSettings([
- 'issuer' => $this->config['issuer'],
- 'clientId' => $this->config['client_id'],
- 'clientSecret' => $this->config['client_secret'],
- 'redirectUri' => url('/oidc/redirect'),
- 'authorizationEndpoint' => $this->config['authorization_endpoint'],
- 'tokenEndpoint' => $this->config['token_endpoint'],
+ 'issuer' => $config['issuer'],
+ 'clientId' => $config['client_id'],
+ 'clientSecret' => $config['client_secret'],
+ 'redirectUri' => url('/oidc/callback'),
+ 'authorizationEndpoint' => $config['authorization_endpoint'],
+ 'tokenEndpoint' => $config['token_endpoint'],
]);
// Use keys if configured
- if (!empty($this->config['jwt_public_key'])) {
- $settings->keys = [$this->config['jwt_public_key']];
+ if (!empty($config['jwt_public_key'])) {
+ $settings->keys = [$config['jwt_public_key']];
}
// Run discovery
- if ($this->config['discover'] ?? false) {
- $settings->discoverFromIssuer(new Client(['timeout' => 3]), Cache::store(null), 15);
+ if ($config['discover'] ?? false) {
+ try {
+ $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
+ } catch (OidcIssuerDiscoveryException $exception) {
+ throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
+ }
}
$settings->validate();
*/
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
{
- return new OidcOAuthProvider($settings->arrayForProvider());
+ return new OidcOAuthProvider($settings->arrayForProvider(), [
+ 'httpClient' => $this->httpClient,
+ 'optionProvider' => new HttpBasicAuthOptionProvider(),
+ ]);
}
/**
- * Calculate the display name
+ * Calculate the display name.
*/
protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
{
- $displayNameAttr = $this->config['display_name_claims'];
+ $displayNameAttr = $this->config()['display_name_claims'];
$displayName = [];
foreach ($displayNameAttr as $dnAttr) {
/**
* Extract the details of a user from an ID token.
+ *
* @return array{name: string, email: string, external_id: string}
*/
protected function getUserDetails(OidcIdToken $token): array
{
$id = $token->getClaim('sub');
+
return [
'external_id' => $id,
- 'email' => $token->getClaim('email'),
- 'name' => $this->getUserDisplayName($token, $id),
+ 'email' => $token->getClaim('email'),
+ 'name' => $this->getUserDisplayName($token, $id),
];
}
/**
* Processes a received access token for a user. Login the user when
* they exist, optionally registering them automatically.
- * @throws OpenIdConnectException
+ *
+ * @throws OidcException
* @throws JsonDebugException
- * @throws UserRegistrationException
* @throws StoppedAuthenticationException
*/
protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User
$settings->keys,
);
- if ($this->config['dump_user_details']) {
+ if ($this->config()['dump_user_details']) {
throw new JsonDebugException($idToken->getAllClaims());
}
try {
$idToken->validate($settings->clientId);
} catch (OidcInvalidTokenException $exception) {
- throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}");
+ throw new OidcException("ID token validate failed with error: {$exception->getMessage()}");
}
$userDetails = $this->getUserDetails($idToken);
$isLoggedIn = auth()->check();
- if ($userDetails['email'] === null) {
- throw new OpenIdConnectException(trans('errors.oidc_no_email_address'));
+ if (empty($userDetails['email'])) {
+ throw new OidcException(trans('errors.oidc_no_email_address'));
}
if ($isLoggedIn) {
- throw new OpenIdConnectException(trans('errors.oidc_already_logged_in'), '/login');
+ throw new OidcException(trans('errors.oidc_already_logged_in'));
}
- $user = $this->registrationService->findOrRegister(
- $userDetails['name'], $userDetails['email'], $userDetails['external_id']
- );
-
- if ($user === null) {
- throw new OpenIdConnectException(trans('errors.oidc_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
+ try {
+ $user = $this->registrationService->findOrRegister(
+ $userDetails['name'],
+ $userDetails['email'],
+ $userDetails['external_id']
+ );
+ } catch (UserRegistrationException $exception) {
+ throw new OidcException($exception->getMessage());
}
$this->loginService->login($user, 'oidc');
+
return $user;
}
+
+ /**
+ * Get the OIDC config from the application.
+ */
+ protected function config(): array
+ {
+ return config('oidc');
+ }
}