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
- Restart the OAuth flow from POST /oauth/authorize so a new code_verifier cookie is set, then immediately call /oauth/callback.
- Ensure credentials: 'include' is set on the callback fetch so the cookie is sent.
- Alternatively, pass codeVerifier explicitly in the callback body if the client stored it from the authorize response.
- 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
- Capture the code_verifier from authorize and store it client-side as a backup to the cookie.
- Keep authorize and callback in the same browser session.
- Use sticky sessions on the load balancer if cookies are not shared.
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
- OAuth state is missing
- OAuth is not enabled
- OAuth authentication failed
- OAuth profile does not have an email address
- This OAuth account has already been linked to another user.
AI-assisted analysis of immich-app/immich@199723261c (2026-08-12).
Data as JSON: /api/errors/dd6f51ead8a203d6.
Report an issue: GitHub.