immich-app/immich · error · BadRequestException

Invalid logout token: no claims found

Error message

Invalid logout token: no claims found

What it means

Per the OIDC Back-Channel Logout spec, a logout_token must carry either a `sub` (user) or `sid` (session) claim. After successful token validation, backchannelLogout throws this 400 when the validated claims object is empty or lacks both sub and sid, since there is nothing to identify which session(s) to log out.

Solutions

  1. Inspect the decoded logout_token (jwt.io or IdP logs) and confirm which claims it carries.
  2. Configure the IdP to include `sid` in logout tokens (e.g. enable session identifiers / back-channel logout with session binding in Keycloak).
  3. Ensure user subjects are not anonymized/omitted by the IdP's token mapper configuration.
  4. As a workaround, log users out by expiring Immich's OAuth sessions directly (e.g. revoke sessions in the IdP and shorten token lifetimes) if the IdP cannot emit sub/sid.

Example fix

// Keycloak: enable "Front-channel/backchannel logout" with session ids
// Client > Advanced Settings > Backchannel Logout URL set AND
// 'Logout service POST' / OIDC logout including sid enabled, so the token carries:
// { "sub": "user-uuid", "sid": "session-id", "events": { "http://schemas.openid.net/event/backchannel-logout": {} } }
Defensive patterns

Strategy: validation

Validate before calling

const payload = JSON.parse(Buffer.from(logoutToken.split('.')[1], 'base64url').toString());
if (!payload.sub && !payload.sid) throw new Error('logout_token lacks sub and sid; fix IdP config first');

Try / catch

try { await api.oauthBackchannelLogout({ logout_token }); } catch (e) { if (e.status === 400 && /sub or a sid/.test(e.message)) alertIdpAdmin('logout_token missing sub/sid'); }

Prevention

When it happens

Trigger: A backchannel-logout POST whose logout_token validates cryptographically but contains claims with neither `sub` nor `sid` — e.g. an IdP emitting only an events claim with no subject/session identifier.

Common situations: Identity providers with incomplete Back-Channel Logout implementations (only `events` claim); IdP configured to log out "all sessions" without emitting sid; custom/legacy IdPs that omit sid; Immich sessions created before sid tracking was stored, though the error here is about the token itself.

Related errors


AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15). Data as JSON: /api/errors/0af8fd5bc77ca008. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/auth.service.ts:111

  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 });
    }
  }

  async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
    const { password, newPassword } = dto;

View on GitHub (pinned to f48d4b3321)