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

  1. Check the Immich server log line 'Error backchannel logout: <message>' for the underlying validation reason.
  2. Verify Immich's OAuth issuer, clientId, and metadata URL match the IdP exactly; re-save the OAuth config to refresh cached discovery/JWKS.
  3. Sync server clocks (NTP) between Immich and the identity provider.
  4. Ensure the IdP is configured to send a proper logout_token (OIDC Back-Channel Logout), not an id_token or access token.
  5. 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

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


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)