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
- Inspect the decoded logout_token (jwt.io or IdP logs) and confirm which claims it carries.
- Configure the IdP to include `sid` in logout tokens (e.g. enable session identifiers / back-channel logout with session binding in Keycloak).
- Ensure user subjects are not anonymized/omitted by the IdP's token mapper configuration.
- 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
- Test backchannel logout tokens with jwt.io before wiring up the IdP
- Enable session-id (sid) emission in the IdP's logout token mappers
- Prefer IdPs with full OIDC Back-Channel Logout support
- Log decoded claims (never raw tokens) for troubleshooting
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
- Error backchannel logout: token validation failed
- Received backchannel logout request but OAuth is not enabled
- Error validating JWT logout token
- Invalid logout token: it must contain either a sub or a sid…
- Error in OAuth discovery
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)