[BUG] OIDC discovery fails for issuers with a trailing slash
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.tsCurrently the URL is built as:
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
- Configure an Authentik OAuth2/OpenID provider for Ghostfolio.
- Verify the discovery document:
curl -s \
https://auth.example.com/application/o/ghostfolio/.well-known/openid-configuration \
| jq '{issuer, authorization_endpoint, token_endpoint, userinfo_endpoint}'Example response:
{
"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/"
}- Configure Ghostfolio using the exact issuer:
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.
Start Ghostfolio.
Ghostfolio attempts to fetch the discovery document using a URL containing
//.well-known/...and fails during startup.
If the trailing slash is removed instead:
OIDC_ISSUER=https://auth.example.com/application/o/ghostfolioGhostfolio 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 issuerWith 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:
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:
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:
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-configurationwhile 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
Source: ghostfolio/ghostfolio