immich-app/immich · warning · BadRequestException

OAuth code verifier is missing

Error message

OAuth code verifier is missing

What it means

BadRequestException (HTTP 400) thrown by AuthService.callback when neither dto.codeVerifier nor the immich_oauth_code_verifier cookie is non-empty. Immich uses PKCE; the verifier generated during authorize must accompany the code exchange. Missing it means the IdP token request would fail, so Immich rejects early.

Source

Thrown at server/src/services/auth.service.ts:298

      dto.state,
      dto.codeChallenge,
    );
  }

  async callback(dto: OAuthCallbackDto, headers: IncomingHttpHeaders, loginDetails: LoginDetails) {
    const { oauth } = await this.getConfig({ withCache: false });
    if (!oauth.enabled) {
      throw new BadRequestException('OAuth is not enabled');
    }

    const expectedState = dto.state ?? this.getCookieOauthState(headers);
    if (!expectedState?.length) {
      throw new BadRequestException('OAuth state is missing');
    }

    const codeVerifier = dto.codeVerifier ?? this.getCookieCodeVerifier(headers);
    if (!codeVerifier?.length) {
      throw new BadRequestException('OAuth code verifier is missing');
    }

    const url = this.resolveRedirectUri(oauth, dto.url);
    const {
      profile,
      sid: oauthSid,
      idToken: oauthBearerToken,
    } = await this.oauthRepository.getProfileAndOAuthSid(oauth, url, expectedState, codeVerifier);
    const normalizedEmail = profile.email ? profile.email.trim().toLowerCase() : undefined;
    const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;
    this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
    let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);

    // link by email
    if (!user && normalizedEmail) {
      const emailUser = await this.userRepository.getByEmail(normalizedEmail);
      if (emailUser) {
        if (emailUser.oauthId) {

View on GitHub (pinned to 199723261c)

Solutions

  1. Restart the OAuth flow from POST /oauth/authorize so a new code_verifier cookie is set, then immediately call /oauth/callback.
  2. Ensure credentials: 'include' is set on the callback fetch so the cookie is sent.
  3. Alternatively, pass codeVerifier explicitly in the callback body if the client stored it from the authorize response.
  4. Verify the cookie's Secure attribute matches the HTTPS deployment.

Example fix

// before
await axios.post('/oauth/callback', { url });

// after
// store verifier from authorize and pass it explicitly
const { data: auth } = await axios.post('/oauth/authorize', { redirectUri });
// ...after IdP redirect...
await axios.post('/oauth/callback', { url, state, codeVerifier: storedVerifier });
Defensive patterns

Strategy: validation

Validate before calling

function hasCodeVerifier(dto: { codeVerifier?: string }, cookies: Record<string, string>): boolean {
  return Boolean((dto.codeVerifier && dto.codeVerifier.length) || cookies.immich_oauth_code_verifier);
}

Type guard

function hasVerifier(dto: { codeVerifier?: string }, cookie: string | null): dto is { codeVerifier: string } {
  return Boolean(dto.codeVerifier && dto.codeVerifier.length) || Boolean(cookie);
}

Try / catch

try {
  await axios.post('/oauth/callback', { url }, { withCredentials: true });
} catch (e) {
  if (e.response?.data?.message === 'OAuth code verifier is missing') {
    await restartOauthFlowFromAuthorize();
  } else throw e;
}

Prevention

When it happens

Trigger: POST /oauth/callback where the body omits codeVerifier and the cookie was lost. Reachable only after the state check passes, so the user passed CSRF but lost the PKCE verifier.

Common situations: Cookie expiration/clearance between authorize and callback; user pasted the callback URL into a fresh browser; load balancer sticky-session loss dropped the cookie; client implemented OAuth without forwarding the verifier explicitly.

Related errors


AI-assisted analysis of immich-app/immich@199723261c (2026-08-12). Data as JSON: /api/errors/dd6f51ead8a203d6. Report an issue: GitHub.