medusajs/medusa · error · MedusaError

A 'state' value is required to build the authorization URL

Error message

A 'state' value is required to build the authorization URL

What it means

buildAuthorizationUrl requires a state value because OIDC uses it to protect against CSRF in the callback flow. The engine throws INVALID_DATA when input.state is falsy before it can construct the provider's authorization URL.

Source

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

    this.options_ = options
    this.discoveryCacheTtlMs_ =
      options.discovery_cache_ttl_ms ?? DEFAULT_DISCOVERY_CACHE_TTL_MS
    this.httpTimeoutMs_ = options.http_timeout_ms ?? DEFAULT_HTTP_TIMEOUT_MS
    this.cache_ = cache
  }

  /**
   * Builds the authorization URL to redirect the browser to, generating a fresh
   * PKCE code verifier/challenge (S256) and nonce. The returned `nonce` and
   * `codeVerifier` must be persisted alongside the state so they can be replayed
   * when validating the callback.
   */
  async buildAuthorizationUrl(
    input: OidcBuildAuthorizationUrlInput
  ): Promise<OidcAuthorizationUrlResult> {
    if (!input?.state) {
      throw new MedusaError(
        MedusaError.Types.INVALID_DATA,
        "A 'state' value is required to build the authorization URL"
      )
    }

    const client = await this.getClient_()

    const codeVerifier = generators.codeVerifier()
    const codeChallenge = generators.codeChallenge(codeVerifier)
    const nonce = generators.nonce()

    const scopes = input.scopes ?? this.options_.scopes ?? DEFAULT_SCOPES

    const url = client.authorizationUrl({
      scope: scopes.join(" "),
      redirect_uri: input.callbackUrl ?? this.options_.callback_url,
      state: input.state,
      nonce,

View on GitHub (pinned to 5e06e544a2)

Solutions

  1. Generate a random state value (e.g. crypto.randomUUID() or crypto.randomBytes(16).toString('hex')) and pass it in the input; persist it (cookie/session) so the callback can be validated.
  2. If you are not writing a custom flow, use the framework-provided OIDC authenticate route, which generates state for you.
  3. Check that the field is named exactly state in the OidcBuildAuthorizationUrlInput object.

Example fix

// before
const { url } = await engine.buildAuthorizationUrl({ /* no state */ })
// after
const state = crypto.randomUUID()
res.cookie("oidc_state", state, { httpOnly: true })
const { url } = await engine.buildAuthorizationUrl({ state })
Defensive patterns

Strategy: validation

Validate before calling

const state = crypto.randomUUID()
if (!state) throw new Error("state generation failed")
await engine.buildAuthorizationUrl({ state })

Type guard

const hasState = (i: OidcBuildAuthorizationUrlInput | undefined): i is OidcBuildAuthorizationUrlInput & { state: string } =>
  typeof i?.state === "string" && i.state.length > 0

Try / catch

try { await engine.buildAuthorizationUrl(input) } catch (e) { if (e instanceof MedusaError && /'state' value is required/.test(e.message)) { /* regenerate state and retry once */ } throw e }

Prevention

When it happens

Trigger: Calling engine.buildAuthorizationUrl({}) or with an undefined/empty state field, e.g. a route handler that forwards the request but forgot to generate or pass the state parameter.

Common situations: A custom auth route controller that manually calls buildAuthorizationUrl and assumes state is optional; a refactor that dropped the state generation step; passing the whole request body when state lives elsewhere.

Related errors


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