{"record":{"id":"e0fef8dfa8835823","repo":"anomalyco/sst","slug":"redirect-uri-mismatch","errorCode":null,"errorMessage":"redirect_uri mismatch","messagePattern":"redirect_uri mismatch","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"sdk/js/src/auth/handler.ts","lineNumber":247,"sourceCode":"  app.post(\"/token\", async (c) => {\n    console.log(\"token request\");\n    const form = await c.req.formData();\n    if (form.get(\"grant_type\") !== \"authorization_code\") {\n      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 =","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/anomalyco/sst/blob/a0bd20f762883e72a35caccb4896c42ce5b3f707/sdk/js/src/auth/handler.ts#L229-L265","documentation":"The /token endpoint verifies the authorization-code JWT and compares its embedded redirect_uri claim against the redirect_uri in the token request form. On mismatch it returns HTTP 400 with \"redirect_uri mismatch\". This is a standard OAuth security check preventing code interception by swapping redirect targets.","triggerScenarios":"Exchanging a code at /token while posting a redirect_uri that differs from the one used in the original /authorize request — different scheme, host, path, trailing slash, or port.","commonSituations":"Using localhost during authorize but 127.0.0.1 at token exchange; adding/removing a trailing slash or port between the two requests; a client behind a proxy that rewrites the callback URL; forgetting to send redirect_uri at all in the token request.","solutions":["Send the exact same redirect_uri string in the /token request as was used in /authorize (scheme, host, port, path).","Normalize the callback URL construction so both requests derive it from one constant.","Check for trailing-slash, localhost-vs-127.0.0.1, and proxy-rewritten host differences.","Log both values (payload.redirect_uri from the decoded code vs the form field) to spot the exact difference."],"exampleFix":"// before\n// authorize: redirect_uri=http://localhost:3000/callback\n// token:     redirect_uri=http://127.0.0.1:3000/callback/\n\n// after\nconst REDIRECT_URI = \"http://localhost:3000/callback\"; // single constant\n// authorize: ...&redirect_uri=${encodeURIComponent(REDIRECT_URI)}\n// token: body: new URLSearchParams({ grant_type: \"authorization_code\", code, redirect_uri: REDIRECT_URI, client_id })","handlingStrategy":"validation","validationCode":"// ensure identical redirect_uri in both steps\nconst REDIRECT_URI = \"http://localhost:3000/callback\";\nif (new URL(callbackUrl).toString() !== new URL(REDIRECT_URI).toString()) {\n  throw new Error(\"Callback URL diverged from REDIRECT_URI\");\n}","typeGuard":"function redirectMatches(authorizeUri: string, tokenUri: string): boolean {\n  return authorizeUri === tokenUri; // handler uses exact string comparison\n}","tryCatchPattern":"const res = await fetch(`${authUrl}/token`, { method: \"POST\", body });\nif (res.status === 400) {\n  const msg = await res.text();\n  if (msg === \"redirect_uri mismatch\") {\n    throw new Error(\"Use the exact redirect_uri from the /authorize request\");\n  }\n}","preventionTips":["Define redirect_uri once in a shared constant used by both authorize and token steps.","Beware localhost vs 127.0.0.1, trailing slashes, and default-port differences.","Don't let proxies rewrite the Host of the callback URL.","The handler compares exact strings — byte-identical, not URL-equivalent."],"tags":["oauth","auth","security","redirect-uri"],"backgroundTag":"oauth-redirect-uri-mismatch","analyzedSha":"a0bd20f762883e72a35caccb4896c42ce5b3f707","analyzedAt":"2026-08-30T11:26:00.383Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}