]> BookStack Code Mirror - bookstack/blobdiff - app/Auth/Access/Oidc/OidcService.php
Guest create page: name field autofocus
[bookstack] / app / Auth / Access / Oidc / OidcService.php
index b8e017b4b135c85fd3b7aee9afbce252b0b372c1..a9323d4233109e544bd94ec044fa4e57530a2cd9 100644 (file)
@@ -2,22 +2,18 @@
 
 namespace BookStack\Auth\Access\Oidc;
 
-use function auth;
+use BookStack\Auth\Access\GroupSyncService;
 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\Arr;
 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 trans;
-use function url;
 
 /**
  * Class OpenIdConnectService
@@ -25,30 +21,37 @@ use function url;
  */
 class OidcService
 {
-    protected $registrationService;
-    protected $loginService;
-    protected $httpClient;
+    protected RegistrationService $registrationService;
+    protected LoginService $loginService;
+    protected HttpClient $httpClient;
+    protected GroupSyncService $groupService;
 
     /**
      * OpenIdService constructor.
      */
-    public function __construct(RegistrationService $registrationService, LoginService $loginService, HttpClient $httpClient)
-    {
+    public function __construct(
+        RegistrationService $registrationService,
+        LoginService $loginService,
+        HttpClient $httpClient,
+        GroupSyncService $groupService
+    ) {
         $this->registrationService = $registrationService;
         $this->loginService = $loginService;
         $this->httpClient = $httpClient;
+        $this->groupService = $groupService;
     }
 
     /**
      * 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(),
             'state' => $provider->getState(),
@@ -57,14 +60,15 @@ class OidcService
 
     /**
      * 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);
@@ -78,8 +82,7 @@ class OidcService
     }
 
     /**
-     * @throws OidcIssuerDiscoveryException
-     * @throws ClientExceptionInterface
+     * @throws OidcException
      */
     protected function getProviderSettings(): OidcProviderSettings
     {
@@ -100,7 +103,11 @@ class OidcService
 
         // 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();
@@ -113,10 +120,31 @@ class OidcService
      */
     protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
     {
-        return new OidcOAuthProvider($settings->arrayForProvider(), [
+        $provider = new OidcOAuthProvider($settings->arrayForProvider(), [
             'httpClient'     => $this->httpClient,
             'optionProvider' => new HttpBasicAuthOptionProvider(),
         ]);
+
+        foreach ($this->getAdditionalScopes() as $scope) {
+            $provider->addScope($scope);
+        }
+
+        return $provider;
+    }
+
+    /**
+     * Get any user-defined addition/custom scopes to apply to the authentication request.
+     *
+     * @return string[]
+     */
+    protected function getAdditionalScopes(): array
+    {
+        $scopeConfig = $this->config()['additional_scopes'] ?: '';
+
+        $scopeArr = explode(',', $scopeConfig);
+        $scopeArr = array_map(fn (string $scope) => trim($scope), $scopeArr);
+
+        return array_filter($scopeArr);
     }
 
     /**
@@ -141,10 +169,32 @@ class OidcService
         return implode(' ', $displayName);
     }
 
+    /**
+     * Extract the assigned groups from the id token.
+     *
+     * @return string[]
+     */
+    protected function getUserGroups(OidcIdToken $token): array
+    {
+        $groupsAttr = $this->config()['groups_claim'];
+        if (empty($groupsAttr)) {
+            return [];
+        }
+
+        $groupsList = Arr::get($token->getAllClaims(), $groupsAttr);
+        if (!is_array($groupsList)) {
+            return [];
+        }
+
+        return array_values(array_filter($groupsList, function ($val) {
+            return is_string($val);
+        }));
+    }
+
     /**
      * Extract the details of a user from an ID token.
      *
-     * @return array{name: string, email: string, external_id: string}
+     * @return array{name: string, email: string, external_id: string, groups: string[]}
      */
     protected function getUserDetails(OidcIdToken $token): array
     {
@@ -154,6 +204,7 @@ class OidcService
             'external_id' => $id,
             'email'       => $token->getClaim('email'),
             'name'        => $this->getUserDisplayName($token, $id),
+            'groups'      => $this->getUserGroups($token),
         ];
     }
 
@@ -161,9 +212,8 @@ class OidcService
      * 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
@@ -182,28 +232,34 @@ class OidcService
         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']
-        );
+        try {
+            $user = $this->registrationService->findOrRegister(
+                $userDetails['name'],
+                $userDetails['email'],
+                $userDetails['external_id']
+            );
+        } catch (UserRegistrationException $exception) {
+            throw new OidcException($exception->getMessage());
+        }
 
-        if ($user === null) {
-            throw new OpenIdConnectException(trans('errors.oidc_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
+        if ($this->shouldSyncGroups()) {
+            $groups = $userDetails['groups'];
+            $detachExisting = $this->config()['remove_from_groups'];
+            $this->groupService->syncUserWithFoundGroups($user, $groups, $detachExisting);
         }
 
         $this->loginService->login($user, 'oidc');
@@ -218,4 +274,12 @@ class OidcService
     {
         return config('oidc');
     }
+
+    /**
+     * Check if groups should be synced.
+     */
+    protected function shouldSyncGroups(): bool
+    {
+        return $this->config()['user_to_groups'] !== false;
+    }
 }