OpenID Connect: Use group details from user_info endpoint #3361

Closed
opened 2026-02-05 06:29:33 +03:00 by OVERLORD · 7 comments
Owner

Originally created by @107142 on GitHub (Nov 25, 2022).

Describe the Bug

It seems the application only parses the id_token when enumerating group claims but not the userinfo endpoint resulting in missing groups when user_info is in use.
We have a large amount of custom claims containing lots of groups making usage of id_token impossible (as its size would be simply too much).

Steps to Reproduce

  1. Make sure you IdP uses user_info to send claims with groups
  2. Configure OIDC to sync groups
  3. Dump user detail upon login

Expected Behaviour

A list of groups should be returned.

Browser Details

Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0

Exact BookStack Version

v22.10.2

PHP Version

8.1.12

Hosting Environment

Rancher Kubernetes

Docker image: solidnerd/bookstack:latest

Clean install

Originally created by @107142 on GitHub (Nov 25, 2022). ### Describe the Bug It seems the application only parses the `id_token` when enumerating group claims but not the `userinfo` endpoint resulting in missing groups when `user_info` is in use. We have a large amount of custom claims containing lots of groups making usage of `id_token` impossible (as its size would be simply too much). ### Steps to Reproduce 1. Make sure you IdP uses `user_info` to send claims with groups 2. Configure OIDC to sync groups 3. Dump user detail upon login ### Expected Behaviour A list of groups should be returned. ### Browser Details Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0 ### Exact BookStack Version v22.10.2 ### PHP Version 8.1.12 ### Hosting Environment Rancher Kubernetes Docker image: solidnerd/bookstack:latest Clean install
OVERLORD added the 🔨 Feature Request🚪 Authentication labels 2026-02-05 06:29:33 +03:00
Author
Owner

@107142 commented on GitHub (Nov 25, 2022):

Updated the description as at first I thought there is an issue with the groups claim parsing logic. But after going through the source it seems like the issue is we do not include groups in id_token, but in user_info and that does not seem to be used.

@107142 commented on GitHub (Nov 25, 2022): Updated the description as at first I thought there is an issue with the groups claim parsing logic. But after going through the source it seems like the issue is we do not include groups in `id_token`, but in `user_info` and that does not seem to be used.
Author
Owner

@ssddanbrown commented on GitHub (Nov 25, 2022):

I'll re-label this as an auth feature request, since we currently don't currently support gaining extra claims from the user_info endpoint as you have found, and it's not a bug in existing logic.

@ssddanbrown commented on GitHub (Nov 25, 2022): I'll re-label this as an auth feature request, since we currently don't currently support gaining extra claims from the `user_info` endpoint as you have found, and it's not a bug in existing logic.
Author
Owner

@felixschloesser commented on GitHub (Feb 22, 2023):

As this issue was a showstopper for my organisation I hacked something together which works for my needs. I know next to nothing about lavarel or php; so I dont consider this PR worthy, but maybe it can be a starting point for someone else.

I overhauled the get user details function to use the access token to query both name and groups form the userinfo endpoint as defined by the provider. As I didnt get it to work with the Psr\Http\Client\ClientInterface I just resorted to curl.

OidcService.php:
    /**
     * Query the user info endpoint for the user's details.
     *
     * @return array{name: string, email: string, external_id: string, groups: string[]}
     */
    protected function getUserInfo(OidcAccessToken $accessToken): ?array 
    {
        $settings = $this->getProviderSettings();
        $user_info_endpoint = $settings->userInfoEndpoint;

        $headers = [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json',
        ];

        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_URL => $user_info_endpoint,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
        ]);

        $responseBody = curl_exec($curl);

        curl_close($curl);

        $userinfo = json_decode($responseBody, true);

        return $userinfo ?? null;
    }
    


    /**
     * Get the user's name using the access token.
     *
     * @return string
     */
    protected function getUserName(OidcAccessToken $token): string
    {
        $nameAttr = $this->config()['display_name_claims'] ?? 'name';

        // Get the user info from the endpoint
        $userInfo = $this->getUserInfo($token);

        if (is_array($nameAttr)) {
            $name = '';
            foreach ($nameAttr as $attr) {
                if (isset($userInfo[$attr])) {
                    $name = $userInfo[$attr];
                    break;
                }
            }
        } else {
            $name = $userInfo[$nameAttr] ?? 'Anonymous';
        }

        return $name;
    }


    /**
     * Get the user's groups using the access token.
     *
     * @return string[]
     */
    protected function getUserGroups(OidcAccessToken $token): array
    {
        $groupsAttr = $this->config()['groups_claim'];
        if (empty($groupsAttr)) {
            return [];
        }

        // $groupsList = Arr::get($token->getAllClaims(), $groupsAttr);
        // Instead we use the user info endpoint
        $userInfo = $this->getUserInfo($token);
        $groupsList = $userInfo[$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, groups: string[]}
     */
    protected function getUserDetails(OidcIdToken $token, OidcAccessToken $accessToken): array
    {
        $idClaim = 'sub';
        $id = $token->getClaim($idClaim);

        return [
            'external_id' => $id,
            'email'       => $token->getClaim('email'),
            'name'        => $this->getUserName($accessToken),
            'groups'      => $this->getUserGroups($accessToken),
        ];
    }

I also adjusted AppServiceProvider.php to include the userinfo endpoint: See gist.

@felixschloesser commented on GitHub (Feb 22, 2023): As this issue was a showstopper for my organisation I hacked something together which works for my needs. I know next to nothing about lavarel or php; so I dont consider this PR worthy, but maybe it can be a starting point for someone else. I overhauled the get user details function to use the access token to query both name and groups form the userinfo endpoint as defined by the provider. As I didnt get it to work with the Psr\Http\Client\ClientInterface I just resorted to curl. <details> <summary>OidcService.php:</summary> ```php /** * Query the user info endpoint for the user's details. * * @return array{name: string, email: string, external_id: string, groups: string[]} */ protected function getUserInfo(OidcAccessToken $accessToken): ?array { $settings = $this->getProviderSettings(); $user_info_endpoint = $settings->userInfoEndpoint; $headers = [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json', ]; $curl = curl_init(); curl_setopt_array($curl, [ CURLOPT_URL => $user_info_endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, ]); $responseBody = curl_exec($curl); curl_close($curl); $userinfo = json_decode($responseBody, true); return $userinfo ?? null; } /** * Get the user's name using the access token. * * @return string */ protected function getUserName(OidcAccessToken $token): string { $nameAttr = $this->config()['display_name_claims'] ?? 'name'; // Get the user info from the endpoint $userInfo = $this->getUserInfo($token); if (is_array($nameAttr)) { $name = ''; foreach ($nameAttr as $attr) { if (isset($userInfo[$attr])) { $name = $userInfo[$attr]; break; } } } else { $name = $userInfo[$nameAttr] ?? 'Anonymous'; } return $name; } /** * Get the user's groups using the access token. * * @return string[] */ protected function getUserGroups(OidcAccessToken $token): array { $groupsAttr = $this->config()['groups_claim']; if (empty($groupsAttr)) { return []; } // $groupsList = Arr::get($token->getAllClaims(), $groupsAttr); // Instead we use the user info endpoint $userInfo = $this->getUserInfo($token); $groupsList = $userInfo[$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, groups: string[]} */ protected function getUserDetails(OidcIdToken $token, OidcAccessToken $accessToken): array { $idClaim = 'sub'; $id = $token->getClaim($idClaim); return [ 'external_id' => $id, 'email' => $token->getClaim('email'), 'name' => $this->getUserName($accessToken), 'groups' => $this->getUserGroups($accessToken), ]; } ``` </details> I also adjusted AppServiceProvider.php to include the userinfo endpoint: [See gist](https://gist.github.com/felixschloesser/a5e11a7ce2fac3111f8746accaf6a6dd).
Author
Owner

@107142 commented on GitHub (Feb 24, 2023):

@ssddanbrown
Would you accept a PR with added functionality provided you are satisfied with the code quality?
I'm not referring to the code above, but ATM we are possibly considering allocating a dev for this particular functionality (no promise though as the decision does not lie with me).

@107142 commented on GitHub (Feb 24, 2023): @ssddanbrown Would you accept a PR with added functionality provided you are satisfied with the code quality? I'm not referring to the code above, but ATM we are possibly considering allocating a dev for this particular functionality (no promise though as the decision does not lie with me).
Author
Owner

@ssddanbrown commented on GitHub (Feb 24, 2023):

@107142 I would be willing to review a PR for this, ideally with the following in mind:

  • The implementation should not affect existing OIDC usages/flows.
  • The scope and surface area of the addition should be minimal, and accept group info in the same format as we accept in the ID_TOKEN currently (Which I think is a simple array of strings, which could be nested in a JSON structure).
  • Feature additions should be covered by tests.

I'm happy to provide pointers/direction/general-help where needed.
Just shout if anything is started and I can assign this issue as required.

@ssddanbrown commented on GitHub (Feb 24, 2023): @107142 I would be willing to review a PR for this, ideally with the following in mind: - The implementation should not affect existing OIDC usages/flows. - The scope and surface area of the addition should be minimal, and accept group info in the same format as we accept in the ID_TOKEN currently (Which I think is a simple array of strings, which could be nested in a JSON structure). - Feature additions should be covered by tests. I'm happy to provide pointers/direction/general-help where needed. Just shout if anything is started and I can assign this issue as required.
Author
Owner

@ssddanbrown commented on GitHub (May 22, 2023):

Just a note on this, v23.05 added a new logical theme event which I believe could be used to call the user_info endpoint (Or any other endpoint/data-source) to supplement ID token data. I specifically ensured that the added event was passed access token data so this kind of thing was made possible.

Details in #4200.
Just shout if you'd like an example.

@ssddanbrown commented on GitHub (May 22, 2023): Just a note on this, v23.05 added a new [logical theme event](https://github.com/BookStackApp/BookStack/blob/development/dev/docs/logical-theme-system.md) which I believe could be used to call the `user_info` endpoint (Or any other endpoint/data-source) to supplement ID token data. I specifically ensured that the added event was passed access token data so this kind of thing was made possible. Details in #4200. Just shout if you'd like an example.
Author
Owner

@ssddanbrown commented on GitHub (Apr 19, 2024):

With work done in #4726 and #4955 BookStack will now use the userinfo endpoint, where expected details are missing from the IDToken. This will be part of the next feature release and I'll therefore close off this request.

Note: If the auth system still provides data for expected claims in the ID token then BookStack will just use ID Token data unless any expected claims are missing. If you need userinfo endpoint claims, but can't alter the ID token to stop that behaviour, it is possible to nullify ID token claims before the userinfo look using the logical theme event I shared above. Happy to provide an example of how that would look upon request.

@ssddanbrown commented on GitHub (Apr 19, 2024): With work done in #4726 and #4955 BookStack will now use the userinfo endpoint, where expected details are missing from the IDToken. This will be part of the next feature release and I'll therefore close off this request. Note: If the auth system still provides data for expected claims in the ID token then BookStack will just use ID Token data unless any expected claims are missing. If you need userinfo endpoint claims, but can't alter the ID token to stop that behaviour, it is possible to nullify ID token claims before the userinfo look using the logical theme event I shared above. Happy to provide an example of how that would look upon request.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/BookStack#3361