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

backchannelLogout validates OIDC backchannel logout tokens before invalidating the local session. Per the OIDC Back-Channel Logout spec, a logout token must identify the session via a `sub` (user) or `sid` (session ID) claim. If the token's claims carry neither, the token cannot be tied to any session and the service rejects it as a bad request rather than invalidating sessions blindly.

Solutions

  1. Fix the identity provider's logout token configuration so it includes the `sid` (and preferably `sub`) claim in backchannel logout tokens.
  2. Check that no middleware or claim mapping on the Immich side strips or renames the `sub`/`sid` claims before backchannelLogout runs.
  3. Verify the IdP actually supports OIDC Back-Channel Logout; if it only supports front-channel logout, disable backchannel logout instead of sending malformed tokens.

Example fix

// IdP logout token claims (before)
{ "iss": "https://idp", "aud": "client", "iat": 1700000000, "jti": "abc" }
// after
{ "iss": "https://idp", "aud": "client", "iat": 1700000000, "jti": "abc", "sid": "session-123", "sub": "user-42" }
Defensive patterns

Strategy: validation

Validate before calling

if (!tokenClaims || (!tokenClaims.sub && !tokenClaims.sid)) {
  throw new Error('Logout token missing sub/sid claim; check IdP backchannel logout config');
}

Type guard

function hasLogoutSubject(c: Record<string, unknown> | undefined): c is { sub: string } | { sid: string } & Record<string, unknown> {
  return !!c && (typeof c.sub === 'string' || typeof c.sid === 'string');
}

Prevention

When it happens

Trigger: An identity provider posts a backchannel logout request whose verified token payload lacks both `sub` and `sid` claims (e.g. an IdP sending only `events` or `iat`/`jti`).

Common situations: IdP misconfiguration or a non-conformant/custom IdP that emits logout tokens without standard claims; intermediate proxies or token-mapping middleware stripping claims; IdP version upgrades changing the token shape.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

      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 f48d4b3321)