medusajs/medusa · error · MedusaError

An authorization 'code' is required to exchange for tokens

Error message

An authorization 'code' is required to exchange for tokens

What it means

exchangeCode is the step that swaps the authorization code for tokens, so a code is mandatory. The engine throws INVALID_DATA when params.code is missing before contacting the token endpoint.

Source

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

      code_challenge_method: "S256",
    })

    return { url, nonce, codeVerifier }
  }

  /**
   * Exchanges the authorization code for tokens and performs full ID-token
   * validation via `openid-client` (signature through JWKS, `iss`, `aud`/`azp`,
   * `exp`/`iat`/`nbf` with clock tolerance, and `nonce`). Returns the validated
   * claims plus the tokens.
   */
  async exchangeCode(
    input: OidcExchangeCodeInput
  ): Promise<OidcExchangeCodeResult> {
    const params = input?.params ?? {}

    if (!params.code) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "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,
      })

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Inspect the callback request's query string: if code is absent there is usually an error or error_description query param — surface that to the user instead.
  2. Only call exchangeCode when req.query.code is present; otherwise re-initiate the flow via buildAuthorizationUrl.
  3. Pass the full provider query params object through: exchangeCode({ params: req.query as Record<string, string>, ... }).

Example fix

// before
const result = await engine.exchangeCode({ params: req.query as any, state, nonce, codeVerifier })
// after
if (!req.query.code) {
  return res.status(400).json({ error: req.query.error ?? "missing_authorization_code" })
}
const result = await engine.exchangeCode({ params: req.query as Record<string, string>, state, nonce, codeVerifier })
Defensive patterns

Strategy: type-guard

Validate before calling

const code = (req.query as Record<string,string>).code
if (!code) return res.redirect("/auth?error=missing_code")
await engine.exchangeCode({ params: req.query as Record<string,string>, ... })

Type guard

const hasCode = (p: Record<string, unknown> | undefined): p is Record<string, string> & { code: string } =>
  typeof p?.code === "string" && p.code.length > 0

Try / catch

try { await engine.exchangeCode(input) } catch (e) { if (e instanceof MedusaError && e.type === MedusaError.Types.INVALID_DATA && /'code'/.test(e.message)) { res.status(400).json({ error: "missing_code" }); return } throw e }

Prevention

When it happens

Trigger: Calling engine.exchangeCode({ params: {...} }) where params has no code key, e.g. the callback route was hit without ?code= in the query, or the query params were not forwarded from the provider redirect.

Common situations: The identity provider redirected with an error (e.g. access_denied) instead of a code and the callback handler blindly calls exchangeCode; a callback route that reads the wrong query field; integration tests that mock the callback without a code.

Related errors


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