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
- 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.
- Only call exchangeCode when req.query.code is present; otherwise re-initiate the flow via buildAuthorizationUrl.
- 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
- Check req.query.error before processing the callback.
- Only invoke exchangeCode when a code query param exists.
- Forward the entire provider query string, not hand-picked fields.
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
- A 'state' value is required to build the authorization URL
- --paths must be a directory - ${additionalPath}
- --base must be a file - ${baseFile}
- insufficient_inventory
- not_found
AI-assisted analysis of medusajs/medusa@5e06e544a2 (2026-08-27).
Data as JSON: /api/errors/2fa078af4f052637.
Report an issue: GitHub.