{"record":{"id":"86e6c2ede882730f","repo":"immich-app/immich","slug":"invalid-logout-token-it-must-contain-either-a-sub","errorCode":null,"errorMessage":"Invalid logout token: it must contain either a sub or a sid claim","messagePattern":"Invalid logout token: it must contain either a sub or a sid claim","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/auth.service.ts","lineNumber":114,"sourceCode":"      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;\n    const user = await this.userRepository.getForChangePassword(auth.user.id);\n    const isValid = this.validateSecret(password, user.password);\n    if (!isValid) {\n      throw new BadRequestException('Wrong password');","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L96-L132","documentation":"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.","triggerScenarios":"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.","commonSituations":"IdP back-channel logout client not configured to include session id; subject claim renamed/mismatched; IdP bug emitting tokens without identifiers.","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."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Ensure the token carries sub or sid before relying on it.\nfunction parseJwtPayload(token: string): any {\n  const part = token.split('.')[1];\n  return JSON.parse(Buffer.from(part, 'base64').toString('utf8'));\n}\nconst claims = parseJwtPayload(logout_token);\nif (!claims?.sub && !claims?.sid) {\n  throw new Error('Logout token must include sub or sid.');\n}","typeGuard":"function hasSessionIdentifier(claims: any): claims is { sub?: string; sid?: string } {\n  return Boolean(claims && (claims.sub || claims.sid));\n}","tryCatchPattern":"try {\n  await api.post('/oauth/backchannel-logout', { logout_token });\n} catch (e) {\n  if (e.response?.status === 400 && /sub or a sid/i.test(e.response?.data?.message)) {\n    reconfigureIdpToIncludeSid();\n  } else throw e;\n}","preventionTips":["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."],"tags":["auth","oauth","logout","backchannel","token","claims","session"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}