decolua/9router · error · Error

OIDC provider did not return an id_token

Error message

OIDC provider did not return an id_token

What it means

During the OIDC authorization-code callback, after exchanging the code at the provider's token endpoint, 9Router requires an id_token (it verifies issuer, audience and nonce via verifyOidcIdToken before issuing a dashboard session). If the token response has no id_token field, the callback throws this message and redirects to /login?error=OIDC+provider+did+not+return+an+id_token.

Source

Thrown at src/app/api/auth/oidc/callback/route.js:63

    if (!config) {
      clearOidcCookies(cookieStore);
      return NextResponse.redirect(new URL("/login?error=oidc_not_configured", getPublicOrigin(request)));
    }

    const discovery = await fetchOidcDiscovery(config.issuerUrl);
    const discoveredIssuer = discovery.issuer || config.issuerUrl;
    const redirectUri = `${getPublicOrigin(request)}/api/auth/oidc/callback`;
    const tokenData = await exchangeOidcCode({
      tokenEndpoint: discovery.token_endpoint,
      clientId: config.clientId,
      clientSecret: config.clientSecret,
      code,
      redirectUri,
      codeVerifier,
    });

    if (!tokenData.id_token) {
      throw new Error("OIDC provider did not return an id_token");
    }

    const payload = await verifyOidcIdToken({
      idToken: tokenData.id_token,
      issuer: discoveredIssuer,
      audience: config.clientId,
      jwksUri: discovery.jwks_uri,
      nonce: storedNonce,
    });

    clearOidcCookies(cookieStore);
    await setDashboardAuthCookie(cookieStore, request, {
      oidc: true,
      oidcSub: payload.sub || null,
      oidcEmail: pickOidcEmail(payload) || null,
      oidcName: pickOidcDisplayName(payload),
    });

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Add 'openid' (plus profile/email) to the requested scopes in the OIDC settings — without it providers omit id_token.
  2. Confirm the identity provider actually supports OIDC (issues ID tokens) and the issuerUrl points to its OIDC discovery document.
  3. Check the provider's app/client configuration grants the authorization_code flow with ID tokens for this redirect URI.
  4. Capture the token response (server logs) to verify which fields came back and confirm scope handling.

Example fix

// before
scopes: "profile email"
// after
scopes: "openid profile email"
Defensive patterns

Strategy: validation

Validate before calling

const config = await getOidcRuntimeConfig();
if (!config?.issuerUrl || !config?.clientId) throw new Error("OIDC not configured");
const discovery = await fetchOidcDiscovery(config.issuerUrl);
if (!discovery.token_endpoint) throw new Error("Discovery missing token_endpoint");
if (!/(^|\s)openid(\s|$)/.test(config.scopes || "")) throw new Error("openid scope required to receive an id_token");

Type guard

const hasIdToken = (t) => t !== null && typeof t === "object" && typeof t.id_token === "string" && t.id_token.split(".").length === 3;

Try / catch

const tokenData = await exchangeOidcCode({ /* ... */ });
if (!hasIdToken(tokenData)) {
  return NextResponse.redirect(new URL("/login?error=oidc_missing_id_token_check_scopes", getPublicOrigin(request)));
}

Prevention

When it happens

Trigger: exchangeOidcCode succeeds but the provider's token endpoint response lacks id_token — the client was configured without the openid scope, the provider only issues access tokens, or the discovery document's token_endpoint belongs to a non-OIDC OAuth2 flow.

Common situations: OIDC app configured without the 'openid' scope; provider is plain OAuth2 (GitHub-style) not OIDC; client registered for access-token-only grant; misconfigured issuerUrl pointing discovery at a generic OAuth2 server.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/6c8ab9eb4f367176. Report an issue: GitHub.