{"record":{"id":"dd6f51ead8a203d6","repo":"immich-app/immich","slug":"oauth-code-verifier-is-missing","errorCode":null,"errorMessage":"OAuth code verifier is missing","messagePattern":"OAuth code verifier is missing","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"server/src/services/auth.service.ts","lineNumber":302,"sourceCode":"      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\n    // link by email\n    if (!user && normalizedEmail) {\n      const emailUser = await this.userRepository.getByEmail(normalizedEmail);\n      if (emailUser) {\n        if (emailUser.oauthId) {","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/immich-app/immich/blob/f48d4b332127ad365ba256108799ca8f571d2dd5/server/src/services/auth.service.ts#L284-L320","documentation":"Guard in the OAuth callback flow: before exchanging the authorization code, the service requires a PKCE code verifier, taken from the request DTO or the oauth state cookie. If neither supplies one, the PKCE token exchange would fail server-side, so the request is rejected early with 400. Fires when a client completes the OAuth redirect without the verifier it generated at authorization start (lost/cleared cookies, stateless or misconfigured client).","triggerScenarios":"callback() called without codeVerifier in the DTO and without the verifier cookie; cookie lost between authorize and callback; separate HTTP clients for each leg.","commonSituations":"Headless scripts calling callback directly; cookie purged by redirect/proxy; HTTP clients that do not share cookie jars; PKCE mismatch after server upgrade.","solutions":["Pass dto.codeVerifier explicitly, captured from the authorize() response","Use the same HTTP client/session for authorize and callback so cookies persist","Ensure the proxy forwards cookies","Restart the full OAuth flow to get a fresh verifier"],"exampleFix":"// before\nawait api.callback({ url });\n// after\nconst { codeVerifier } = await api.authorize(dto);\nawait api.callback({ url, codeVerifier });","handlingStrategy":"validation","validationCode":"const verifier = dto.codeVerifier ?? getCookie('code_verifier'); if (!verifier) throw new Error('Missing PKCE code verifier; call authorize() first and keep its verifier');","typeGuard":"const hasVerifier = (d: { codeVerifier?: string }) => typeof d.codeVerifier === 'string' && d.codeVerifier.length > 0;","tryCatchPattern":"try { await api.oauthCallback(dto, headers) } catch (e) { if (e.status === 400 && /code verifier is missing/.test(e.message)) { /* redo authorize() then callback */ } throw e; }","preventionTips":["Persist the PKCE verifier for the whole flow","Share cookie jars across redirect steps","Test OAuth behind cookie-forwarding proxies"],"tags":["oauth","pkce","code-verifier","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"}