immich-app/immich · warning · BadRequestException

Invalid logout token: no claims found

Error message

Invalid logout token: no claims found

What it means

Thrown by backchannelLogout after validateLogoutToken returns without throwing but yields a falsy claims object. Per the OIDC back-channel logout spec the token must carry identifiable claims; if the validated token has no usable claims the server rejects it with 400 BadRequest 'Invalid logout token: no claims found'. This is distinct from error 75 (validation threw) and error 77 (claims present but incomplete).

Source

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

  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 199723261c)

Solutions

  1. Decode the logout_token JWT locally (base64 of the payload) and inspect the claims.
  2. Have the IdP re-issue the logout token with a proper claims payload (sub/sid/events).
  3. Confirm the IdP implements OIDC back-channel logout (RFC) rather than a custom variant.
Defensive patterns

Strategy: try-catch

Validate before calling

// Inspect the logout_token payload before sending/forwarding.
function parseJwtPayload(token: string): any {
  const part = token.split('.')[1];
  return JSON.parse(Buffer.from(part, 'base64').toString('utf8'));
}
const claims = parseJwtPayload(logout_token);
if (!claims || Object.keys(claims).length === 0) {
  throw new Error('Logout token has no claims; request a new one from the IdP.');
}

Type guard

function hasClaims(claims: unknown): claims is Record<string, unknown> {
  return !!claims && typeof claims === 'object' && Object.keys(claims as object).length > 0;
}

Try / catch

try {
  await api.post('/oauth/backchannel-logout', { logout_token });
} catch (e) {
  if (e.response?.status === 400 && /no claims/i.test(e.response?.data?.message)) {
    requestNewLogoutToken();
  } else throw e;
}

Prevention

When it happens

Trigger: A logout_token that is structurally valid (signature/issuer ok) but whose payload decodes to null/empty claims; IdP issued a token with an empty payload section.

Common situations: Misconfigured IdP emitting minimal logout tokens; token corruption that drops the payload while preserving the header/signature; non-standard IdP behavior.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/0af8fd5bc77ca008. Report an issue: GitHub.