#7885·ghostfolio

[BUG] OIDC discovery fails for issuers with a trailing slash

Author: seh-lenCreated Sep 16, 2026Updated Sep 16, 2026

Bug Description

OIDC authentication with Authentik only works when OIDC_ISSUER is set to the exact issuer including its trailing slash and all three OIDC endpoints (OIDC_AUTHORIZATION_URL, OIDC_TOKEN_URL, and OIDC_USER_INFO_URL) are configured explicitly.

Authentik reports an issuer such as:

https://auth.example.com/application/o/ghostfolio/

The trailing slash is significant because the iss claim in the ID token contains exactly this value. If the trailing slash is omitted from OIDC_ISSUER, the token exchange itself succeeds, but Ghostfolio rejects the ID token with:

Authentication with the oidc strategy was rejected:
ID token not issued by expected OpenID provider.

However, if the correct issuer including the trailing slash is configured without manually specifying all three endpoints, Ghostfolio fails during startup while fetching the OIDC discovery document.

The reason appears to be the discovery URL construction in:

apps/api/src/app/auth/auth.module.ts

Currently the URL is built as:

typescript
const response = await fetchService.fetch(
  `${issuer}/.well-known/openid-configuration`
);

For an issuer that already ends with /, this produces:

https://auth.example.com/application/o/ghostfolio//.well-known/openid-configuration
                                                 ^^

In my Authentik setup this URL returns HTML instead of the JSON discovery document, causing Ghostfolio to fail while parsing the response.

To Reproduce

  1. Configure an Authentik OAuth2/OpenID provider for Ghostfolio.
  2. Verify the discovery document:
bash
curl -s \
  https://auth.example.com/application/o/ghostfolio/.well-known/openid-configuration \
  | jq '{issuer, authorization_endpoint, token_endpoint, userinfo_endpoint}'

Example response:

json
{
  "issuer": "https://auth.example.com/application/o/ghostfolio/",
  "authorization_endpoint": "https://auth.example.com/application/o/authorize/",
  "token_endpoint": "https://auth.example.com/application/o/token/",
  "userinfo_endpoint": "https://auth.example.com/application/o/userinfo/"
}
  1. Configure Ghostfolio using the exact issuer:
env
ENABLE_FEATURE_AUTH_OIDC=true
OIDC_ISSUER=https://auth.example.com/application/o/ghostfolio/
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...

Do not configure the three manual endpoint overrides.

  1. Start Ghostfolio.

  2. Ghostfolio attempts to fetch the discovery document using a URL containing //.well-known/... and fails during startup.

If the trailing slash is removed instead:

env
OIDC_ISSUER=https://auth.example.com/application/o/ghostfolio

Ghostfolio starts, but authentication fails after a successful token exchange because the ID token issuer is:

https://auth.example.com/application/o/ghostfolio/

and therefore does not exactly match OIDC_ISSUER.

Actual Behavior

With the correct issuer including the trailing slash, Ghostfolio fails during startup:

ERROR [OidcStrategy] SyntaxError: Unexpected token '<', "
<!DOCTYPE "... is not valid JSON

ERROR [ExceptionHandler] Error: Failed to fetch OIDC configuration from issuer

With the trailing slash removed, startup succeeds, but OIDC login fails with:

Authentication with the oidc strategy was rejected:
ID token not issued by expected OpenID provider.

Expected Behavior

Ghostfolio should accept the issuer exactly as advertised by the OIDC provider and construct the discovery URL correctly regardless of whether the issuer ends with /.

It should therefore be possible to configure only:

env
OIDC_ISSUER=https://auth.example.com/application/o/ghostfolio/

together with the client ID and secret, without having to manually configure all OIDC endpoints.

Workaround

The login works when the exact issuer including the trailing slash is used and all three endpoint overrides are configured explicitly:

env
OIDC_ISSUER=https://auth.example.com/application/o/ghostfolio/

OIDC_AUTHORIZATION_URL=https://auth.example.com/application/o/authorize/
OIDC_TOKEN_URL=https://auth.example.com/application/o/token/
OIDC_USER_INFO_URL=https://auth.example.com/application/o/userinfo/

Because all three endpoints are present, Ghostfolio skips discovery and therefore does not construct the malformed URL.

Suggested Fix

The discovery URL could normalize only the separator used for the discovery request while leaving the actual issuer unchanged for ID token validation, for example:

typescript
const discoveryURL =
  `${issuer.replace(/\/+$/, '')}/.well-known/openid-configuration`;

const response = await fetchService.fetch(discoveryURL);

This would result in:

https://auth.example.com/application/o/ghostfolio/.well-known/openid-configuration

while still passing the original issuer including its trailing slash to passport-openidconnect for correct iss validation.

Environment

  • Ghostfolio 3.70.1
  • Self-hosted with Docker
  • Identity provider: Authentik