{"record":{"id":"0af8fd5bc77ca008","repo":"immich-app/immich","slug":"invalid-logout-token-no-claims-found","errorCode":null,"errorMessage":"Invalid logout token: no claims found","messagePattern":"Invalid logout token: no claims found","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":111,"sourceCode":"\n  async backchannelLogout(dto: OAuthBackchannelLogoutDto): Promise<void> {\n    const { oauth } = await this.getConfig({ withCache: false });\n    if (!oauth.enabled) {\n      throw new BadRequestException('Received backchannel logout request but OAuth is not enabled');\n    }\n\n    let claims;\n    try {\n      claims = await this.oauthRepository.validateLogoutToken(oauth, dto.logout_token);\n    } catch (error: Error | any) {\n      this.logger.error(`Error backchannel logout: ${error.message}`);\n      this.logger.error(error);\n\n      throw new BadRequestException('Error backchannel logout: token validation failed');\n    }\n\n    if (!claims) {\n      throw new BadRequestException('Invalid logout token: no claims found');\n    }\n\n    if (!claims.sub && !claims.sid) {\n      throw new BadRequestException('Invalid logout token: it must contain either a sub or a sid claim');\n    }\n\n    const deletedSessionIds = await this.sessionRepository.invalidateOAuth({\n      oauthSid: claims.sid,\n      oauthId: claims.sub,\n    });\n\n    for (const sessionId of deletedSessionIds) {\n      await this.eventRepository.emit('SessionDelete', { sessionId });\n    }\n  }\n\n  async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {\n    const { password, newPassword } = dto;","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L93-L129","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// Keycloak: enable \"Front-channel/backchannel logout\" with session ids\n// Client > Advanced Settings > Backchannel Logout URL set AND\n// 'Logout service POST' / OIDC logout including sid enabled, so the token carries:\n// { \"sub\": \"user-uuid\", \"sid\": \"session-id\", \"events\": { \"http://schemas.openid.net/event/backchannel-logout\": {} } }","handlingStrategy":"validation","validationCode":"const payload = JSON.parse(Buffer.from(logoutToken.split('.')[1], 'base64url').toString());\nif (!payload.sub && !payload.sid) throw new Error('logout_token lacks sub and sid; fix IdP config first');","typeGuard":null,"tryCatchPattern":"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'); }","preventionTips":["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"],"tags":["oauth","oidc","jwt","claims","logout","immich"],"backgroundTag":"unexpected-response-shape","analyzedSha":"f48d4b332127ad365ba256108799ca8f571d2dd5","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}