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
- Send the same client_id in the /token request that was used in the /authorize request.
- Store the client_id in one shared constant/config so both steps use the identical value.
- Verify you're not mixing codes issued to a different app (each code is bound to the client that requested it).
- 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
- Keep one CLIENT_ID constant shared by the authorize and token code paths.
- Never let multiple apps exchange each other's codes — each code is bound to its client.
- Don't change/regenerate client ids between the authorize redirect and the token call.
- Check environment-specific configs (staging vs prod) aren't mixing client ids.
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
- redirect_uri mismatch
- Missing token parameter
- error
- Unsupported response_type: ${response_type}
- Invalid grant_type
AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30).
Data as JSON: /api/errors/ed30643388faeeb5.
Report an issue: GitHub.