{"record":{"id":"ed30643388faeeb5","repo":"anomalyco/sst","slug":"client-id-mismatch","errorCode":null,"errorMessage":"client_id mismatch","messagePattern":"client_id mismatch","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"sdk/js/src/auth/handler.ts","lineNumber":251,"sourceCode":"      c.status(400);\n      return c.text(\"Invalid grant_type\");\n    }\n    const code = form.get(\"code\");\n    if (!code) {\n      c.status(400);\n      return c.text(\"Missing code\");\n    }\n\n    const { payload } = await jwtVerify(\n      code as string,\n      await options.signing.publicKey()\n    );\n    if (payload.redirect_uri !== form.get(\"redirect_uri\")) {\n      c.status(400);\n      return c.text(\"redirect_uri mismatch\");\n    }\n    if (payload.client_id !== form.get(\"client_id\")) {\n      c.status(400);\n      return c.text(\"client_id mismatch\");\n    }\n\n    return c.json({\n      access_token: payload.token,\n    });\n  });\n\n  app.use(\"/:provider/authorize\", async (c, next) => {\n    const provider = c.req.param(\"provider\");\n    console.log(\"authorize request for\", provider);\n    const response_type =\n      c.req.query(\"response_type\") || getCookie(c, \"response_type\");\n    const redirect_uri =\n      c.req.query(\"redirect_uri\") || getCookie(c, \"redirect_uri\");\n    const state = c.req.query(\"state\") || getCookie(c, \"state\");\n    const client_id = c.req.query(\"client_id\") || getCookie(c, \"client_id\");\n","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/anomalyco/sst/blob/a0bd20f762883e72a35caccb4896c42ce5b3f707/sdk/js/src/auth/handler.ts#L233-L269","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n// authorize used client_id=web-app, token exchange sends client_id=mobile-app\n\n// after\nconst CLIENT_ID = \"web-app\";\n// authorize: ...?client_id=${CLIENT_ID}&response_type=code...\n// token: body: new URLSearchParams({ grant_type: \"authorization_code\", code, redirect_uri: REDIRECT_URI, client_id: CLIENT_ID })","handlingStrategy":"validation","validationCode":"const CLIENT_ID = \"web-app\"; // single source of truth\n// authorize: ?client_id=${CLIENT_ID}&response_type=code...\n// token body must include: client_id: CLIENT_ID\nif (!CLIENT_ID) throw new Error(\"client_id required in token exchange\");","typeGuard":"function clientIdMatches(authorizeId: string, tokenId: string): boolean {\n  return authorizeId === tokenId; // handler compares code claim to form field\n}","tryCatchPattern":"const res = await fetch(`${authUrl}/token`, { method: \"POST\", body });\nif (res.status === 400) {\n  const msg = await res.text();\n  if (msg === \"client_id mismatch\") {\n    throw new Error(\"Use the same client_id as the /authorize request\");\n  }\n}","preventionTips":["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."],"tags":["oauth","auth","security","client-id"],"backgroundTag":"oauth-client-id-mismatch","analyzedSha":"a0bd20f762883e72a35caccb4896c42ce5b3f707","analyzedAt":"2026-08-30T11:26:00.383Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}