immich-app/immich · warning · BadRequestException
OAuth state is missing
Error message
OAuth state is missing
What it means
BadRequestException (HTTP 400) thrown by AuthService.callback when neither dto.state nor the immich_oauth_state cookie has a non-empty value. The state parameter is required to mitigate CSRF during the OAuth code exchange. Immich stores it in a cookie during authorize; on callback it reads dto.state first, then the cookie.
Source
Thrown at server/src/services/auth.service.ts:293
}
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 199723261c)
Solutions
- Restart the flow with POST /oauth/authorize so a fresh state cookie is set, then complete the callback in the same browser session.
- Ensure cookies are sent with the callback request (credentials: 'include' on the fetch).
- Check the secure/sameSite attributes on immich_oauth_state against whether the site is HTTPS.
- Confirm the reverse proxy forwards the Cookie header to the callback handler.
Example fix
// before
await fetch('/oauth/callback', { method: 'POST', body: JSON.stringify({ url }) });
// cookies not sent
// after
await fetch('/oauth/authorize', { method: 'POST', body: JSON.stringify({ redirectUri }), credentials: 'include' });
// ...user authorizes on IdP...
await fetch('/oauth/callback', { method: 'POST', body: JSON.stringify({ url }), credentials: 'include' }); Defensive patterns
Strategy: validation
Validate before calling
function hasOauthState(dto: { state?: string }, cookies: Record<string, string>): boolean {
return Boolean((dto.state && dto.state.length) || cookies.immich_oauth_state);
} Type guard
function hasStateParam(dto: { state?: string }, cookie: string | null): dto is { state: string } {
return Boolean(dto.state && dto.state.length) || Boolean(cookie);
} Try / catch
try {
await axios.post('/oauth/callback', { url }, { withCredentials: true });
} catch (e) {
if (e.response?.data?.message === 'OAuth state is missing') {
await restartOauthFlow();
} else throw e;
} Prevention
- Always send credentials: 'include' on authorize and callback so the state cookie round-trips.
- Restart the flow if the user switched tabs/browsers mid-OAuth.
- Verify Secure/SameSite cookie attributes match your HTTPS origin.
When it happens
Trigger: POST /oauth/callback with a body that omits `state`, sent by a client whose cookies do not contain immich_oauth_state. Common when the user cleared cookies mid-flow, used a different browser, or the callback URL was opened in a private window.
Common situations: User started OAuth in one browser tab and finished in another; cookies blocked by browser policy; the authorize response cookies were never set because of a same-site/secure mismatch; reverse proxy stripped cookies.
Related errors
- OAuth code verifier 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/789903507ee579a7.
Report an issue: GitHub.