medusajs/medusa · error · MedusaError

Could not validate the identity provider's response: ${error

Error message

Could not validate the identity provider's response: ${error.message}

What it means

The engine wraps every failure from openid-client's client.callback() (token request, state/nonce mismatch, expired code, network error) as an UNAUTHORIZED MedusaError with the underlying message appended. It means the IdP's response could not be validated.

Source

Thrown at packages/modules/providers/auth-oidc/src/engine/engine.ts:169

        "An authorization 'code' is required to exchange for tokens"
      )
    }

    const client = await this.getClient_()
    const redirectUri = input.callbackUrl ?? this.options_.callback_url

    let tokenSet: TokenSet
    try {
      // Forward every authorization-response parameter so
      // openid-client can enforce all applicable checks. The `checks` argument
      // carries the values we stored ourselves (PKCE verifier, nonce, state).
      tokenSet = await client.callback(redirectUri, params, {
        code_verifier: input.codeVerifier,
        nonce: input.nonce,
        state: input.state,
      })
    } catch (error) {
      throw new MedusaError(
        MedusaError.Types.UNAUTHORIZED,
        `Could not validate the identity provider's response: ${error.message}`
      )
    }

    // Without the `openid` scope, the token endpoint returns no ID token and
    // `tokenSet.claims()` would throw an unhelpful TypeError.
    if (!tokenSet.id_token) {
      throw new MedusaError(
        MedusaError.Types.UNAUTHORIZED,
        "The identity provider did not return an ID token; ensure the 'openid' scope is requested"
      )
    }

    const claims = tokenSet.claims()

    return {
      claims: { ...claims },

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Read the appended underlying error.message — it names the exact cause (e.g. 'state mismatch', 'invalid_grant').
  2. Verify the cookie/session that stored state and nonce survives the round trip (SameSite=None; Secure in production, shared session store behind load balancers).
  3. Confirm client_secret, client_id and callback_url in medusa-config.js still match the IdP app registration.
  4. Ensure codeVerifier and nonce from the authorization step are passed unchanged to exchangeCode, and that each code is exchanged exactly once.

Example fix

// before
const result = await engine.exchangeCode({ params, state, nonce })
// after
try {
  const result = await engine.exchangeCode({ params, state, nonce, codeVerifier })
} catch (e) {
  logger.error(`OIDC callback failed: ${e.message}`) // inspect underlying reason
  throw e
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const result = await engine.exchangeCode(input)
} catch (e) {
  if (e instanceof MedusaError && e.type === MedusaError.Types.UNAUTHORIZED) {
    logger.warn(`OIDC callback rejected: ${e.message}`)
    return res.redirect(`/auth?error=oidc_callback_failed`)
  }
  throw e
}

Prevention

When it happens

Trigger: The state or nonce sent back by the provider does not match what was issued; the authorization code was already used or expired; the token endpoint rejected the client (bad client_secret/redirect URI); PKCE code_verifier missing or wrong; network/TLS failure reaching the token endpoint.

Common situations: Cookies holding state/nonce were dropped (SameSite issues in cross-domain callbacks, Safari ITP, load balancer stripping cookies); double callback invocation reusing a code; client_secret rotated in the IdP but not in config; clock skew causing JWT validation failures.

Related errors


AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27). Data as JSON: /api/errors/32ff8b88553d2ce9. Report an issue: GitHub.