moeru-ai/airi · error · Error
OIDC flow status has expired or is no longer valid.
Error message
OIDC flow status has expired or is no longer valid.
What it means
completeOIDCSignIn throws this when the callback URL contains a code and state but consumeFlowState() returns nothing, meaning no PKCE/flow state was persisted (or it was consumed or expired) for this sign-in attempt. The OIDC Authorization Code + PKCE flow requires the locally stored state (code verifier, params) to match the callback; without it the code cannot be exchanged safely.
Source
Thrown at packages/stage-ui/src/libs/auth.ts:103
callbackURL: authorizationUrl,
})
}
/**
* Completes an OIDC sign-in from a platform callback URL.
*
* The function returns false when the URL is not an OIDC callback.
*/
export async function completeOIDCSignIn(callbackUrl: string): Promise<boolean> {
const url = new URL(callbackUrl)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
if (!code || !state)
return false
const persisted = consumeFlowState()
if (!persisted)
throw new Error('OIDC flow status has expired or is no longer valid.')
const tokens = await exchangeCodeForTokens(code, persisted.flowState, persisted.params, state)
await applyOIDCTokens(tokens, persisted.params.clientId)
return true
}
/**
* Initiate OIDC Authorization Code + PKCE sign-in flow.
* Builds the authorization URL, persists PKCE state, and navigates.
*/
export async function signInOIDC(params: OIDCFlowParams) {
const handler = authorizationHandler
if (!handler)
throw new Error('No authorization handler is registered for this app runtime.')
const { provider, ...oidcParams } = params
const { url, flowState } = await buildAuthorizationURL(oidcParams)
persistFlowState(flowState, params)View on GitHub (pinned to 0616eabd5b)
Solutions
- Restart the flow: call signInOIDC again to build a fresh authorization URL and persisted state, then complete sign-in from that attempt.
- Ensure completeOIDCSignIn runs in the same window/session that called signInOIDC (check storage access in iframes/private mode/Electron partitions).
- Make sure the redirect is handled exactly once — remove duplicate route handlers or listeners that consume the flow state first.
- Check that storage is enabled and the app origin is stable between initiation and callback.
Example fix
// before
const ok = await completeOIDCSignIn(window.location)
if (!ok) return
// after
try {
const ok = await completeOIDCSignIn(window.location)
if (!ok) return
} catch (err) {
// stale callback (reload/bookmark): restart the flow
await signInOIDC(params)
} Defensive patterns
Strategy: fallback
Validate before calling
const hasFlowState = readFlowState() != null // or expose a probe from the auth lib
if (!hasFlowState) {
// stale/duplicate callback: restart instead of completing
await signInOIDC(params)
} else {
await completeOIDCSignIn(url)
} Type guard
null
Try / catch
try {
await completeOIDCSignIn(window.location)
} catch (err) {
if (err instanceof Error && err.message.includes('expired or is no longer valid')) {
await signInOIDC(params) // restart the flow once; surface UI if it repeats
} else throw err
} Prevention
- Handle the OIDC redirect exactly once — remove duplicate listeners/route guards that consume flow state.
- Avoid reloading or bookmarking callback URLs; treat callbacks as single-use.
- Run initiation and completion in the same window/session/storage partition.
- Show a 'session expired, sign in again' UI instead of an error page on stale callbacks.
When it happens
Trigger: signInOIDC was never called (or ran in another window/session) before the redirect callback hit completeOIDCSignIn; the flow state was already consumed by a duplicate callback handling; storage was cleared or is unavailable (localStorage disabled, private mode, different origin/window); the user bookmarked or reloaded the callback URL after the state was consumed.
Common situations: Deep-linking the callback URL in an Electron app where the state was persisted in a different window; reloading or re-opening the OIDC redirect URI; browser storage partitioning or third-party storage blocking; two components both handling the OAuth redirect and the first consuming the state.
Related errors
- Token exchange failed: ${response.status} ${error}
- No authorization handler is registered for this app runtime.
- OIDC state mismatch — possible CSRF attack
- voice packs upstream ${res.status}: ${await res.text().catch
- updateUser failed
AI-assisted analysis of moeru-ai/airi@0616eabd5b (2026-08-28).
Data as JSON: /api/errors/b33a13c2b0630859.
Report an issue: GitHub.