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 function config;
-use Exception;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
-use Psr\Http\Client\ClientExceptionInterface;
+use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Psr\Http\Client\ClientInterface as HttpClient;
+use function auth;
+use function config;
use function trans;
use function url;
*/
class OidcService
{
- protected $registrationService;
- protected $loginService;
- protected $httpClient;
+ protected RegistrationService $registrationService;
+ protected LoginService $loginService;
+ protected HttpClient $httpClient;
/**
* OpenIdService constructor.
* Initiate an authorization flow.
*
* @return array{url: string, state: string}
+ * @throws OidcException
*/
public function login(): array
{
/**
* 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.
+ * 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 Exception
- * @throws ClientExceptionInterface
+ * @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);
return $this->processAccessTokenCallback($accessToken, $settings);
}
+
/**
- * @throws OidcIssuerDiscoveryException
- * @throws ClientExceptionInterface
+ * @throws OidcException
*/
protected function getProviderSettings(): OidcProviderSettings
{
// Run discovery
if ($config['discover'] ?? false) {
- $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
+ try {
+ $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
+ } catch (OidcIssuerDiscoveryException $exception) {
+ throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
+ }
}
$settings->validate();
* 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
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 (empty($userDetails['email'])) {
- throw new OpenIdConnectException(trans('errors.oidc_no_email_address'));
+ 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');
namespace BookStack\Http\Controllers\Auth;
use BookStack\Auth\Access\Oidc\OidcService;
+use BookStack\Auth\Access\Oidc\OidcException;
use BookStack\Http\Controllers\Controller;
use Illuminate\Http\Request;
class OidcController extends Controller
{
- protected $oidcService;
+ protected OidcService $oidcService;
/**
* OpenIdController constructor.
*/
public function login()
{
- $loginDetails = $this->oidcService->login();
+ try {
+ $loginDetails = $this->oidcService->login();
+ } catch (OidcException $exception) {
+ $this->showErrorNotification($exception->getMessage());
+ return redirect('/login');
+ }
+
session()->flash('oidc_state', $loginDetails['state']);
return redirect($loginDetails['url']);
return redirect('/login');
}
- $this->oidcService->processAuthorizeResponse($request->query('code'));
+ try {
+ $this->oidcService->processAuthorizeResponse($request->query('code'));
+ } catch (OidcException $oidcException) {
+ $this->showErrorNotification($oidcException->getMessage());
+ return redirect('/login');
+ }
return redirect()->intended();
}
use BookStack\Auth\User;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
-use Illuminate\Filesystem\Cache;
use Tests\Helpers\OidcJwtHelper;
use Tests\TestCase;
use Tests\TestResponse;
class OidcTest extends TestCase
{
- protected $keyFilePath;
+ protected string $keyFilePath;
protected $keyFile;
protected function setUp(): void
$this->assertFalse(auth()->check());
- $this->runLogin([
+ $resp = $this->runLogin([
'email' => $editor->email,
'sub' => 'benny505',
]);
+ $resp = $this->followRedirects($resp);
- $this->assertSessionError('A user with the email ' . $editor->email . ' already exists but with different credentials.');
+ $resp->assertSeeText('A user with the email ' . $editor->email . ' already exists but with different credentials.');
$this->assertFalse(auth()->check());
}
public function test_auth_login_with_invalid_token_fails()
{
- $this->runLogin([
+ $resp = $this->runLogin([
'sub' => null,
]);
+ $resp = $this->followRedirects($resp);
- $this->assertSessionError('ID token validate failed with error: Missing token subject value');
+ $resp->assertSeeText('ID token validate failed with error: Missing token subject value');
$this->assertFalse(auth()->check());
}
new Response(404, [], 'Not found'),
]);
- $this->runLogin();
+ $resp = $this->followRedirects($this->runLogin());
$this->assertFalse(auth()->check());
- $this->assertSessionError('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
+ $resp->assertSeeText('Login using SingleSignOn-Testing failed, system did not provide successful authorization');
}
public function test_autodiscovery_calls_are_cached()