{"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":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":297,"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":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L279-L315","documentation":"Raised by AuthService.callback during the OAuth flow when no state parameter is available: dto.state is empty and the oauth state cookie (read via getCookieOauthState) is also absent. The state value is required to complete the authorization-code exchange safely, so the callback is rejected with a 400. Typically means the callback was opened in a different browser/session than the one that started the flow, cookies were blocked, or the state was omitted by the client.","triggerScenarios":"callback() invoked with dto.state undefined and no oauth_state cookie in headers; cookies stripped or expired; state from a different domain.","commonSituations":"Proxy not passing Cookie headers; browser blocking cookies; manually calling the callback API without capturing state from the redirect; mismatched external URL breaking cookie domain.","solutions":["Forward the state query param from the provider redirect into the callback call","Verify cookies survive end-to-end (proxy forwards Cookie, correct domain)","Complete the flow in one browser session so the oauth_state cookie persists","Match the server external URL to the browser-facing URL"],"exampleFix":"// before\nawait api.callback({ url: redirectUrl });\n// after\nawait api.callback({ url: redirectUrl, state: new URL(redirectUrl).searchParams.get('state') });","handlingStrategy":"validation","validationCode":"const state = dto.state ?? getCookie('oauth_state'); if (!state) throw new Error('Missing OAuth state: forward the state param or keep cookies enabled');","typeGuard":"const hasState = (d: { state?: string }) => typeof d.state === 'string' && d.state.length > 0;","tryCatchPattern":"try { await api.oauthCallback(dto, headers) } catch (e) { if (e.status === 400 && /state is missing/.test(e.message)) { /* restart flow from authorize() */ } throw e; }","preventionTips":["Pass state explicitly in headless clients","Configure proxies to forward cookies","Keep app and server origins aligned for cookies"],"tags":["oauth","csrf","state","cookies"],"backgroundTag":"oauth-token-exchange-failed","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"}