anomalyco/sst · error

client_id mismatch

Error message

client_id mismatch

What it means

The /token endpoint also compares the code JWT's client_id claim against the client_id form field and returns HTTP 400 with "client_id mismatch" when they differ. This ensures the token exchange is performed by the same client that initiated the authorization.

Source

Thrown at sdk/js/src/auth/handler.ts:251

      c.status(400);
      return c.text("Invalid grant_type");
    }
    const code = form.get("code");
    if (!code) {
      c.status(400);
      return c.text("Missing code");
    }

    const { payload } = await jwtVerify(
      code as string,
      await options.signing.publicKey()
    );
    if (payload.redirect_uri !== form.get("redirect_uri")) {
      c.status(400);
      return c.text("redirect_uri mismatch");
    }
    if (payload.client_id !== form.get("client_id")) {
      c.status(400);
      return c.text("client_id mismatch");
    }

    return c.json({
      access_token: payload.token,
    });
  });

  app.use("/:provider/authorize", async (c, next) => {
    const provider = c.req.param("provider");
    console.log("authorize request for", provider);
    const response_type =
      c.req.query("response_type") || getCookie(c, "response_type");
    const redirect_uri =
      c.req.query("redirect_uri") || getCookie(c, "redirect_uri");
    const state = c.req.query("state") || getCookie(c, "state");
    const client_id = c.req.query("client_id") || getCookie(c, "client_id");

View on GitHub (pinned to a0bd20f762)

Solutions

  1. Send the same client_id in the /token request that was used in the /authorize request.
  2. Store the client_id in one shared constant/config so both steps use the identical value.
  3. Verify you're not mixing codes issued to a different app (each code is bound to the client that requested it).
  4. Log the decoded payload.client_id vs the submitted form value to identify the discrepancy.

Example fix

// before
// authorize used client_id=web-app, token exchange sends client_id=mobile-app

// after
const CLIENT_ID = "web-app";
// authorize: ...?client_id=${CLIENT_ID}&response_type=code...
// token: body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID })
Defensive patterns

Strategy: validation

Validate before calling

const CLIENT_ID = "web-app"; // single source of truth
// authorize: ?client_id=${CLIENT_ID}&response_type=code...
// token body must include: client_id: CLIENT_ID
if (!CLIENT_ID) throw new Error("client_id required in token exchange");

Type guard

function clientIdMatches(authorizeId: string, tokenId: string): boolean {
  return authorizeId === tokenId; // handler compares code claim to form field
}

Try / catch

const res = await fetch(`${authUrl}/token`, { method: "POST", body });
if (res.status === 400) {
  const msg = await res.text();
  if (msg === "client_id mismatch") {
    throw new Error("Use the same client_id as the /authorize request");
  }
}

Prevention

When it happens

Trigger: Exchanging a code at /token with a client_id form value that differs from the client_id used in the original /authorize request, or omitting client_id entirely.

Common situations: Multiple OAuth clients/apps sharing one auth handler but exchanging codes with the wrong client_id; a renamed or regenerated client id between authorize and token steps; forgetting to forward client_id in the token request; environment-specific client ids (staging vs prod) mixing requests.

Related errors


AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30). Data as JSON: /api/errors/ed30643388faeeb5. Report an issue: GitHub.