immich-app/immich · error · BadRequestException
OAuth state is missing
Error message
OAuth state is missing
What it means
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.
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
Example fix
// before
await api.callback({ url: redirectUrl });
// after
await api.callback({ url: redirectUrl, state: new URL(redirectUrl).searchParams.get('state') }); Defensive patterns
Strategy: validation
Validate before calling
const state = dto.state ?? getCookie('oauth_state'); if (!state) throw new Error('Missing OAuth state: forward the state param or keep cookies enabled'); Type guard
const hasState = (d: { state?: string }) => typeof d.state === 'string' && d.state.length > 0; Try / catch
try { await api.oauthCallback(dto, headers) } catch (e) { if (e.status === 400 && /state is missing/.test(e.message)) { /* restart flow from authorize() */ } throw e; } Prevention
- Pass state explicitly in headless clients
- Configure proxies to forward cookies
- Keep app and server origins aligned for cookies
When it happens
Trigger: callback() invoked with dto.state undefined and no oauth_state cookie in headers; cookies stripped or expired; state from a different domain.
Common situations: 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.
Related errors
- OAuth code verifier is missing
- Admin setup is not available
- Album not shared with user
- Error backchannel logout: token validation failed
- Error in OAuth discovery
AI-assisted analysis of immich-app/immich@f48d4b3321 (2026-09-15).
Data as JSON: /api/errors/789903507ee579a7.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/auth.service.ts:297
}
return await this.oauthRepository.authorize(
oauth,
this.resolveRedirectUri(oauth, dto.redirectUri),
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);
View on GitHub (pinned to f48d4b3321)