immich-app/immich · error · BadRequestException
Error backchannel logout: token validation failed
Error message
Error backchannel logout: token validation failed
What it means
backchannelLogout validates the IdP's logout_token via oauthRepository.validateLogoutToken. If validation throws (bad signature, wrong issuer/audience, expired token, missing required claims per OIDC Back-Channel Logout spec, key fetch failure), the error is logged and rethrown as this generic 400 so token details are not leaked to the caller.
Solutions
- Check the Immich server log line 'Error backchannel logout: <message>' for the underlying validation reason.
- Verify Immich's OAuth issuer, clientId, and metadata URL match the IdP exactly; re-save the OAuth config to refresh cached discovery/JWKS.
- Sync server clocks (NTP) between Immich and the identity provider.
- Ensure the IdP is configured to send a proper logout_token (OIDC Back-Channel Logout), not an id_token or access token.
- If the IdP rotated keys, restart Immich or wait for JWKS cache expiry and resend the logout request.
Example fix
// diagnose by reading the underlying reason in server logs // immich log: "Error backchannel logout: jwt issuer invalid. expected: https://idp.example.com" // after: fix oauth.issuer in Immich settings to https://idp.example.com/realms/main
Defensive patterns
Strategy: try-catch
Validate before calling
// decode the logout_token without verifying to sanity-check iss/aud before resending
const payload = JSON.parse(Buffer.from(logoutToken.split('.')[1], 'base64url').toString());
if (payload.iss !== expectedIssuer) throw new Error('Issuer mismatch before sending'); Try / catch
try { await api.oauthBackchannelLogout({ logout_token }); } catch (e) { if (e.status === 400) { readServerLogForCause(); refreshJwksOrConfig(); } } Prevention
- Keep issuer/clientId in Immich exactly matching the IdP
- Run NTP time sync on both servers
- Re-save OAuth config after IdP key rotation
- Verify the IdP sends a logout_token, not an id_token
When it happens
Trigger: POST to the back-channel logout endpoint with a logout_token that fails validation: signed by an unexpected key, wrong iss/aud, expired, reused (replay), or malformed JWT.
Common situations: IdP rotated signing keys and Immich cached old JWKS; clock skew between Immich and the IdP making tokens appear expired; misconfigured issuer/audience/clientId in Immich's OAuth settings; the IdP sending an id_token instead of a logout_token; network/DNS failures fetching the IdP's discovery/JWKS document.
Related errors
- Invalid logout token: no claims found
- Error validating JWT logout token
- Received backchannel logout request but OAuth is not enabled
- Error in OAuth discovery
- Invalid logout token: it must contain either a sub or a sid…
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/a1636adccf3112d4.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/auth.service.ts:107
successful: true,
redirectUri: await this.getLogoutEndpoint(authType, oauthBearerToken),
};
}
async backchannelLogout(dto: OAuthBackchannelLogoutDto): Promise<void> {
const { oauth } = await this.getConfig({ withCache: false });
if (!oauth.enabled) {
throw new BadRequestException('Received backchannel logout request but OAuth is not enabled');
}
let claims;
try {
claims = await this.oauthRepository.validateLogoutToken(oauth, dto.logout_token);
} catch (error: Error | any) {
this.logger.error(`Error backchannel logout: ${error.message}`);
this.logger.error(error);
throw new BadRequestException('Error backchannel logout: token validation failed');
}
if (!claims) {
throw new BadRequestException('Invalid logout token: no claims found');
}
if (!claims.sub && !claims.sid) {
throw new BadRequestException('Invalid logout token: it must contain either a sub or a sid claim');
}
const deletedSessionIds = await this.sessionRepository.invalidateOAuth({
oauthSid: claims.sid,
oauthId: claims.sub,
});
for (const sessionId of deletedSessionIds) {
await this.eventRepository.emit('SessionDelete', { sessionId });
}View on GitHub (pinned to f48d4b3321)