immich-app/immich · error · BadRequestException
OAuth code verifier is missing
Error message
OAuth code verifier is missing
What it means
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).
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
Example fix
// before
await api.callback({ url });
// after
const { codeVerifier } = await api.authorize(dto);
await api.callback({ url, codeVerifier }); Defensive patterns
Strategy: validation
Validate before calling
const verifier = dto.codeVerifier ?? getCookie('code_verifier'); if (!verifier) throw new Error('Missing PKCE code verifier; call authorize() first and keep its verifier'); Type guard
const hasVerifier = (d: { codeVerifier?: string }) => typeof d.codeVerifier === 'string' && d.codeVerifier.length > 0; Try / catch
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; } Prevention
- Persist the PKCE verifier for the whole flow
- Share cookie jars across redirect steps
- Test OAuth behind cookie-forwarding proxies
When it happens
Trigger: callback() called without codeVerifier in the DTO and without the verifier cookie; cookie lost between authorize and callback; separate HTTP clients for each leg.
Common situations: Headless scripts calling callback directly; cookie purged by redirect/proxy; HTTP clients that do not share cookie jars; PKCE mismatch after server upgrade.
Related errors
- OAuth state is missing
- Error backchannel logout: token validation failed
- Error in OAuth discovery
- Failed to fetch picture
- Invalid logout token: it must contain either a sub or a sid…
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/dd6f51ead8a203d6.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/auth.service.ts:302
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 f48d4b3321)