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
- In the IdP, enable inclusion of 'sid' (session id) and/or 'sub' in back-channel logout tokens.
- Verify the IdP's logout token template against the OIDC back-channel logout spec.
- 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
- Enable 'sid' (session id) emission in the IdP's back-channel logout client config.
- Validate sub/sid presence client-side before forwarding the token.
- Decode the payload to see exactly which claims the IdP sends.
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
- Invalid logout token: no claims found
- Error backchannel logout: token validation failed
- Received backchannel logout request but OAuth is not enabled
- Password login has been disabled
- Unauthorized
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/86e6c2ede882730f.
Report an issue: GitHub.