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
- Fix the identity provider's logout token configuration so it includes the `sid` (and preferably `sub`) claim in backchannel logout tokens.
- Check that no middleware or claim mapping on the Immich side strips or renames the `sub`/`sid` claims before backchannelLogout runs.
- 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
- Confirm the IdP includes sid/sub in backchannel logout tokens during provider setup.
- Write an integration test posting a spec-conformant logout token to the endpoint.
- Log unknown token claim shapes when integrating a new IdP.
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
- Invalid logout token: no claims found
- Received backchannel logout request but OAuth is not enabled
- Unexpected profile response, no `sub`
- Error backchannel logout: token validation failed
- Error in OAuth discovery
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)