{"record":{"id":"789903507ee579a7","repo":"immich-app/immich","slug":"oauth-state-is-missing","errorCode":null,"errorMessage":"OAuth state is missing","messagePattern":"OAuth state is missing","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"server/src/services/auth.service.ts","lineNumber":293,"sourceCode":"    }\n\n    return await this.oauthRepository.authorize(\n      oauth,\n      this.resolveRedirectUri(oauth, dto.redirectUri),\n      dto.state,\n      dto.codeChallenge,\n    );\n  }\n\n  async callback(dto: OAuthCallbackDto, headers: IncomingHttpHeaders, loginDetails: LoginDetails) {\n    const { oauth } = await this.getConfig({ withCache: false });\n    if (!oauth.enabled) {\n      throw new BadRequestException('OAuth is not enabled');\n    }\n\n    const expectedState = dto.state ?? this.getCookieOauthState(headers);\n    if (!expectedState?.length) {\n      throw new BadRequestException('OAuth state is missing');\n    }\n\n    const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers);\n    if (!codeVerifier?.length) {\n      throw new BadRequestException('OAuth code verifier is missing');\n    }\n\n    const url = this.resolveRedirectUri(oauth, dto.url);\n    const {\n      profile,\n      sid: oauthSid,\n      idToken: oauthBearerToken,\n    } = await this.oauthRepository.getProfileAndOAuthSid(oauth, url, expectedState, codeVerifier);\n    const normalizedEmail = profile.email ? profile.email.trim().toLowerCase() : undefined;\n    const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;\n    this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);\n    let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);\n","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/auth.service.ts#L275-L311","documentation":"BadRequestException (HTTP 400) thrown by AuthService.callback when neither dto.state nor the immich_oauth_state cookie has a non-empty value. The state parameter is required to mitigate CSRF during the OAuth code exchange. Immich stores it in a cookie during authorize; on callback it reads dto.state first, then the cookie.","triggerScenarios":"POST /oauth/callback with a body that omits `state`, sent by a client whose cookies do not contain immich_oauth_state. Common when the user cleared cookies mid-flow, used a different browser, or the callback URL was opened in a private window.","commonSituations":"User started OAuth in one browser tab and finished in another; cookies blocked by browser policy; the authorize response cookies were never set because of a same-site/secure mismatch; reverse proxy stripped cookies.","solutions":["Restart the flow with POST /oauth/authorize so a fresh state cookie is set, then complete the callback in the same browser session.","Ensure cookies are sent with the callback request (credentials: 'include' on the fetch).","Check the secure/sameSite attributes on immich_oauth_state against whether the site is HTTPS.","Confirm the reverse proxy forwards the Cookie header to the callback handler."],"exampleFix":"// before\nawait fetch('/oauth/callback', { method: 'POST', body: JSON.stringify({ url }) });\n// cookies not sent\n\n// after\nawait fetch('/oauth/authorize', { method: 'POST', body: JSON.stringify({ redirectUri }), credentials: 'include' });\n// ...user authorizes on IdP...\nawait fetch('/oauth/callback', { method: 'POST', body: JSON.stringify({ url }), credentials: 'include' });","handlingStrategy":"validation","validationCode":"function hasOauthState(dto: { state?: string }, cookies: Record<string, string>): boolean {\n  return Boolean((dto.state && dto.state.length) || cookies.immich_oauth_state);\n}","typeGuard":"function hasStateParam(dto: { state?: string }, cookie: string | null): dto is { state: string } {\n  return Boolean(dto.state && dto.state.length) || Boolean(cookie);\n}","tryCatchPattern":"try {\n  await axios.post('/oauth/callback', { url }, { withCredentials: true });\n} catch (e) {\n  if (e.response?.data?.message === 'OAuth state is missing') {\n    await restartOauthFlow();\n  } else throw e;\n}","preventionTips":["Always send credentials: 'include' on authorize and callback so the state cookie round-trips.","Restart the flow if the user switched tabs/browsers mid-OAuth.","Verify Secure/SameSite cookie attributes match your HTTPS origin."],"tags":["auth","oauth","csrf","cookies","nestjs","immich"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}