immich-app/immich · warning · BadRequestException

Invalid logout token: it must contain either a sub or a sid

Error message

Invalid logout token: it must contain either a sub or a sid claim

What it means

Thrown by backchannelLogout when the validated claims object is present but contains neither a sub (subject) nor a sid (session id) claim. The server uses sub/sid to find which local sessions to invalidate via sessionRepository.invalidateOAuth; without either, it cannot map the logout to any session, so it rejects with 400 BadRequest.

Source

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

      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;
    const user = await this.userRepository.getForChangePassword(auth.user.id);
    const isValid = this.validateSecret(password, user.password);
    if (!isValid) {
      throw new BadRequestException('Wrong password');

View on GitHub (pinned to 199723261c)

Solutions

  1. In the IdP, enable inclusion of 'sid' (session id) and/or 'sub' in back-channel logout tokens.
  2. Verify the IdP's logout token template against the OIDC back-channel logout spec.
  3. Decode the token payload to confirm which claims are actually present.
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the token carries sub or sid before relying on it.
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?.sub && !claims?.sid) {
  throw new Error('Logout token must include sub or sid.');
}

Type guard

function hasSessionIdentifier(claims: any): claims is { sub?: string; sid?: string } {
  return Boolean(claims && (claims.sub || claims.sid));
}

Try / catch

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

Prevention

When it happens

Trigger: The IdP's logout_token is valid and has claims but omits both sub and sid; IdP was configured with pairwise/anonymous subjects and no session id; claim mapping on the IdP side is incomplete.

Common situations: IdP back-channel logout client not configured to include session id; subject claim renamed/mismatched; IdP bug emitting tokens without identifiers.

Related errors


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