Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC using Zitadel does not work #4682

Closed
megastary opened this issue Nov 19, 2023 · 9 comments
Closed

OIDC using Zitadel does not work #4682

megastary opened this issue Nov 19, 2023 · 9 comments
Labels

Comments

@megastary
Copy link

Describe the Bug

When trying to use Zitadel Identity server for OIDC login to bookstack, it always fails as it does not expect audience claim to be array. According to standard, aud should usually be array, only in special case, when only one audience is available, it may present it as string. Source: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
It may not be usually presented as an array, but sadly Zitadel always sends aud as an array and there is currently no way to disable that behaviour, though it's kinda expected as they do not break the standard with that implementation.

Stack trace in log:

[2023-11-19 19:30:28] production.ERROR: invalid_grant {"exception":"[object] (League\\OAuth2\\Client\\Provider\\Exception\\IdentityProviderException(code: 400): invalid_grant at /var/www/bookstack/app/Access/Oidc/OidcOAuthProvider.php:104)
[stacktrace]
#0 /var/www/bookstack/vendor/league/oauth2-client/src/Provider/AbstractProvider.php(726): BookStack\\Access\\Oidc\\OidcOAuthProvider->checkResponse()
#1 /var/www/bookstack/vendor/league/oauth2-client/src/Provider/AbstractProvider.php(635): League\\OAuth2\\Client\\Provider\\AbstractProvider->getParsedResponse()
#2 /var/www/bookstack/app/Access/Oidc/OidcService.php(67): League\\OAuth2\\Client\\Provider\\AbstractProvider->getAccessToken()
#3 /var/www/bookstack/app/Access/Controllers/OidcController.php(57): BookStack\\Access\\Oidc\\OidcService->processAuthorizeResponse()
#4 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(54): BookStack\\Access\\Controllers\\OidcController->callback()
#5 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(43): Illuminate\\Routing\\Controller->callAction()
#6 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Route.php(259): Illuminate\\Routing\\ControllerDispatcher->dispatch()
#7 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Route.php(205): Illuminate\\Routing\\Route->runController()
#8 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Router.php(798): Illuminate\\Routing\\Route->run()
#9 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\\Routing\\Router->Illuminate\\Routing\\{closure}()
#10 /var/www/bookstack/app/Http/Middleware/CheckGuard.php(27): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#11 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\CheckGuard->handle()
#12 /var/www/bookstack/app/Http/Middleware/Localization.php(45): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#13 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\Localization->handle()
#14 /var/www/bookstack/app/Http/Middleware/RunThemeActions.php(26): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#15 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\RunThemeActions->handle()
#16 /var/www/bookstack/app/Http/Middleware/CheckEmailConfirmed.php(47): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#17 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\CheckEmailConfirmed->handle()
#18 /var/www/bookstack/app/Http/Middleware/PreventAuthenticatedResponseCaching.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#19 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\PreventAuthenticatedResponseCaching->handle()
#20 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php(78): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#21 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken->handle()
#22 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php(49): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#23 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\View\\Middleware\\ShareErrorsFromSession->handle()
#24 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(121): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#25 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(64): Illuminate\\Session\\Middleware\\StartSession->handleStatefulRequest()
#26 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Session\\Middleware\\StartSession->handle()
#27 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php(37): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#28 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse->handle()
#29 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php(67): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#30 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Cookie\\Middleware\\EncryptCookies->handle()
#31 /var/www/bookstack/app/Http/Middleware/ApplyCspRules.php(33): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#32 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\ApplyCspRules->handle()
#33 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#34 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Router.php(797): Illuminate\\Pipeline\\Pipeline->then()
#35 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Router.php(776): Illuminate\\Routing\\Router->runRouteWithinStack()
#36 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Router.php(740): Illuminate\\Routing\\Router->runRoute()
#37 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Routing/Router.php(729): Illuminate\\Routing\\Router->dispatchToRoute()
#38 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(190): Illuminate\\Routing\\Router->dispatch()
#39 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(141): Illuminate\\Foundation\\Http\\Kernel->Illuminate\\Foundation\\Http\\{closure}()
#40 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php(39): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#41 /var/www/bookstack/app/Http/Middleware/TrustProxies.php(41): Illuminate\\Http\\Middleware\\TrustProxies->handle()
#42 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): BookStack\\Http\\Middleware\\TrustProxies->handle()
#43 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php(21): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#44 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php(40): Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest->handle()
#45 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\TrimStrings->handle()
#46 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php(27): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#47 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize->handle()
#48 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php(86): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#49 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(180): Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance->handle()
#50 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\\Pipeline\\Pipeline->Illuminate\\Pipeline\\{closure}()
#51 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(165): Illuminate\\Pipeline\\Pipeline->then()
#52 /var/www/bookstack/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(134): Illuminate\\Foundation\\Http\\Kernel->sendRequestThroughRouter()
#53 /var/www/bookstack/public/index.php(52): Illuminate\\Foundation\\Http\\Kernel->handle()
#54 {main}

Steps to Reproduce

  1. Configure OIDC according to docs
  2. Click on sign in with SSO
  3. See error: ID token validate failewd with error: Token audience value has 2 values, Expected 1

Expected Behaviour

  1. Configure OIDC according to docs
  2. Click on sign in with SSO
  3. Successfully log in

Screenshots or Additional Context

bookstack_bug

Browser Details

Brave 1.60.118 Chromium: 119.0.6045.163 on Windows 11

Exact BookStack Version

v23.10.2

@ssddanbrown
Copy link
Member

Hi @megastary,
Please see #4147 for a lot of prior context and conversation on this.

My comment in #4200 provides an example of a workaround that can be use to make zitadel's behavior compatible.

@megastary
Copy link
Author

Hi @ssddanbrown,

thank you very much for responding with all I needed! Sorry that I did not find mentioned issue myself.
On #4147 I personally side with @the-voidl and think that Bookstack should be able to handle array on its own as it is imho clearly stated in RFC 7519, but you made working workaround and it's great!
The only thing I think could be improved is to have those tips how to setup Zitadel SSO in docs. I guess the reason is that currently it is clearly new and not that big, but as stated by @the-voidl, Zitadel may not be the only identity server that sends array in audience, so maybe some general heads up could be included in https://www.bookstackapp.com/docs/admin/oidc-auth/ docs page?
Also to be fair, I wish Zitadel could be the flexible one and allow us to send aud as string as there are many applications that do not support array in the aud claim. I will try to chat with maintainers to see if there are any plans to implement this.

Now to results. I got it working!

  1. In Zitadel, create new project
  2. Create Web application
  3. Select Code configuration
  4. Copy Client Id and Client Secret for later use
  5. In Token settings, enable User Info inside ID Token authtoken option
  6. In Bookstack, create following file (and folder structure) /var/www/bookstack/themes/custom/functions.php
<?php

use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;

Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, function (array $idTokenData, array $accessTokenData) {
    if (is_array($idTokenData['aud']) && in_array($idTokenData['azp'], $idTokenData['aud'])) {
        return array_merge($idTokenData, [
            'aud' => [$idTokenData['azp']]
        ]);
    }
});
  1. Add following options to /var/www/bookstack/.env
# Set OIDC to be the authentication method
AUTH_METHOD=oidc

# Control if BookStack automatically initiates login via your OIDC system
# if it's the only authentication method. Prevents the need for the
# user to click the "Login with x" button on the login page.
# Setting this to true enables auto-initiation.
AUTH_AUTO_INITIATE=true

# Set the display name to be shown on the login button.
# (Login with <name>)
OIDC_NAME="SSO"

# Name of the claims(s) to use for the user's display name.
# Can have multiple attributes listed, separated with a '|' in which
# case those values will be joined with a space.
# Example: OIDC_DISPLAY_NAME_CLAIMS=given_name|family_name
OIDC_DISPLAY_NAME_CLAIMS=name

# OAuth Client ID to access the identity provider
OIDC_CLIENT_ID=ClientId

# OAuth Client Secret to access the identity provider
OIDC_CLIENT_SECRET=ClientSecret

# Issuer URL
# Must start with 'https://'
OIDC_ISSUER=https://your-zitadel-instance.example.com

# Enable auto-discovery of endpoints and token keys.
# As per the standard, expects the service to serve a
# `<issuer>/.well-known/openid-configuration` endpoint.
OIDC_ISSUER_DISCOVER=true

# Load custom functions from custom template
APP_THEME=custom
  1. Login with Zitadel SSO to Bookstack

So overall, it is indeed doable and quite easy to do! As a low priority improvement could be function to that pairs Bookstack's Email Confirmation with Zitadels info in token, which states if e-mail is verified, in other words to delegate that check to identtiy server.
Example response from Zitadel (last line):

{
  "iss": "https:\/\/your-zitadel-instance.example.com",
  "sub": "12345",
  "aud": [
    "12345@bookstack"
  ],
  "exp": 12345,
  "iat": 12345,
  "auth_time": 12345,
  "amr": [
    "password",
    "pwd",
    "mfa",
    "user"
  ],
  "azp": "12345@bookstack",
  "client_id": "12345@bookstack",
  "at_hash": "hash",
  "c_hash": "hash",
  "name": "Name Surname",
  "given_name": "Name",
  "family_name": "Surname",
  "locale": "en",
  "updated_at": 12345,
  "preferred_username": "name@example.com",
  "email": "name@example.com",
  "email_verified": true
}

@ssddanbrown
Copy link
Member

Good to hear the workaround works for you here!

think that Bookstack should be able to handle array on its own as it is imho clearly stated in RFC 7519

Just to confirm, BookStack does accept an array or string value as per the RFC, it's just that it also validates that property to my strict interpretation of the OIDC spec, so rejects when there's more that one value since that's never expected in the OIDC flow scenario for BookStack.

@Chaz6
Copy link

Chaz6 commented Dec 17, 2023

@megastary thanks for the tips, I was able to get login working with Zitadel! Did you have any luck with group sync? I cannot seem to figure out how to get my Zitadel roles working. I have created a role called "Wiki Admin" and I have an equivalent role in BookStack, but it is no getting applied when a user logs in.

@ssddanbrown
Copy link
Member

@Chaz6 You can use the OIDC_DUMP_USER_DETAILS=true option to help see if the details are being provided by Zitadel and, if so, how they are named.

Details in our docs: https://www.bookstackapp.com/docs/admin/oidc-auth/#debugging
Example in video of using this to debug: https://youtu.be/TJQ4NJrMvkw?t=1154 (19:14 mark)

@megastary
Copy link
Author

megastary commented Dec 17, 2023

@megastary thanks for the tips, I was able to get login working with Zitadel! Did you have any luck with group sync? I cannot seem to figure out how to get my Zitadel roles working. I have created a role called "Wiki Admin" and I have an equivalent role in BookStack, but it is no getting applied when a user logs in.

@Chaz6 I think the trick part was to enable Assert Roles on Authentication

This is my config which works:

image

image

And .env for bookstack

OIDC_USER_TO_GROUPS=true
OIDC_GROUPS_CLAIM=groups
OIDC_REMOVE_FROM_GROUPS=false

@baua1310
Copy link

baua1310 commented Jun 3, 2024

Hi,

I successfully setup OIDC with Zitadel using the steps from @megastary.
But after some time, after 24 hours at the latest, I get this error:

image
ID token validation failed with error: Token signature could not be validated using the provided keys.

Deleting the bookstack docker container and recreating it fixes the error for some hours.

Anybody else having this error? Am I missing a configuration?

Now to results. I got it working!

  1. In Zitadel, create new project
  2. Create Web application
  3. Select Code configuration
  4. Copy Client Id and Client Secret for later use
  5. In Token settings, enable User Info inside ID Token authtoken option
  6. In Bookstack, create following file (and folder structure) /var/www/bookstack/themes/custom/functions.php
<?php

use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;

Theme::listen(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, function (array $idTokenData, array $accessTokenData) {
    if (is_array($idTokenData['aud']) && in_array($idTokenData['azp'], $idTokenData['aud'])) {
        return array_merge($idTokenData, [
            'aud' => [$idTokenData['azp']]
        ]);
    }
});
  1. Add following options to /var/www/bookstack/.env
# Set OIDC to be the authentication method
AUTH_METHOD=oidc

# Control if BookStack automatically initiates login via your OIDC system
# if it's the only authentication method. Prevents the need for the
# user to click the "Login with x" button on the login page.
# Setting this to true enables auto-initiation.
AUTH_AUTO_INITIATE=true

# Set the display name to be shown on the login button.
# (Login with <name>)
OIDC_NAME="SSO"

# Name of the claims(s) to use for the user's display name.
# Can have multiple attributes listed, separated with a '|' in which
# case those values will be joined with a space.
# Example: OIDC_DISPLAY_NAME_CLAIMS=given_name|family_name
OIDC_DISPLAY_NAME_CLAIMS=name

# OAuth Client ID to access the identity provider
OIDC_CLIENT_ID=ClientId

# OAuth Client Secret to access the identity provider
OIDC_CLIENT_SECRET=ClientSecret

# Issuer URL
# Must start with 'https://'
OIDC_ISSUER=https://your-zitadel-instance.example.com

# Enable auto-discovery of endpoints and token keys.
# As per the standard, expects the service to serve a
# `<issuer>/.well-known/openid-configuration` endpoint.
OIDC_ISSUER_DISCOVER=true

# Load custom functions from custom template
APP_THEME=custom
  1. Login with Zitadel SSO to Bookstack

@ssddanbrown
Copy link
Member

@baua1310 We do some caching of auto-discovery findings in BookStack which could lead to something like that, especially as it looks like Zitadel has frequent key rotation by default, but our caching is only intended for 15 minutes.

Feel free to raise as a seperate support issue for potential debug/workaround options, as it's something different to what was originally discussed in this closed thread.

@baua1310
Copy link

baua1310 commented Jun 4, 2024

Hi @ssddanbrown thank you for your message. I created a new issue #5049

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

4 participants