feat(backend): derive JWT issuer and OAuth redirect_uri from request host#1498
Conversation
…host When the backend serves both api.stack-auth.com and api.hexclave.com from the same deployment, signed tokens and OAuth redirect URIs need to match the host the customer's SDK actually talks to — otherwise customers with hardcoded issuer checks or registered OAuth callback URLs break when the SDK upgrades. This change: - Adds apps/backend/src/lib/request-api-url.ts with getApiUrlForRequest() that maps known cloud hosts (stack-auth.com, hexclave.com, plus dev/staging variants) to their own URLs; unknown hosts (localhost, previews, self-host) fall back to NEXT_PUBLIC_STACK_API_URL. - Threads apiUrl through getIssuer + signJWT in tokens.tsx so the iss claim follows the request host. getAllowedIssuers stays env-driven with the existing alias map, so tokens cross-validate between the two hosts. - Refactors all 12 OAuth providers + Mock to take apiUrl, used to build the redirect_uri sent to Google/GitHub/etc. getProvider() forwards the value from the route handler. - Converts the OAuth2Server singleton to a per-request createOAuthServer() factory so OAuthModel.generateAccessToken signs with the right issuer. - Wires getApiUrlForRequest() into all token-minting and provider-instantiating route handlers (10 createAuthTokens sites, the authorize/callback/token/ cross-domain-authorize routes, connected-accounts crud). For the SDK side: - Reverts packages/template defaultBaseUrl / defaultAnalyticsBaseUrl back to stack-auth.com so the next @stackframe/* publish keeps existing customers pointed at the legacy host. The @hexclave/* mirror publishing pipeline (rewrite-packages-to-hexclave.ts) is unchanged here; flipping it to substitute these literals during republish lands in a follow-up. For the docs side: - Rewrites docs-mintlify/migration.mdx to clearly call out the two host- visible changes (JWT iss claim and OAuth callback URLs) that require pre-deploy action when migrating to @hexclave/*, with the GitHub-OAuth-App single-URL caveat spelled out. Verification: pnpm typecheck and pnpm lint pass clean across all 29 packages.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR threads request-derived API URLs (apiUrl) through token creation and OAuth initialization. It adds a request-api-url helper with host-pair allowlisting, updates token issuer construction/validation to accept apiUrl, replaces the oauthServer singleton with a per-request factory, and changes all OAuth providers to build redirect URIs from apiUrl. ChangesRequest-aware API URL and token issuer rebrand
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR makes the JWT
Confidence Score: 4/5The core mechanism is implemented consistently across all call sites; the two findings are a documentation gap and a comment clarification with no runtime impact. The logic changes are comprehensive and internally consistent: CLOUD_HOST_PAIRS drives both the signing allowlist and the validation alias map, preventing the drift bug called out in the PR description. All 10+ token-minting call sites pass apiUrl, and getAllowedIssuers correctly expands to both hosts for every supported cloud-host pair including staging. The two findings are a documentation gap in the migration guide and a comment clarification — neither affects runtime correctness. The migration guide (docs-mintlify/migration.mdx) dropped the @stackframe/emails row from the package table. apps/backend/src/lib/request-api-url.ts has a trust-model comment that should be clarified for non-Vercel deployers. Important Files Changed
Sequence DiagramsequenceDiagram
participant SDK as SDK (api.stack-auth.com)
participant BE as Backend
participant Helper as getApiUrlForRequest
participant Tokens as tokens.tsx / getIssuer
participant Provider as OAuthProvider
participant ExtOAuth as External OAuth Provider
Note over SDK,BE: Password/anonymous/session sign-in flows
SDK->>BE: POST /auth/password/sign-in (Host: api.stack-auth.com)
BE->>Helper: getApiUrlForRequest(fullReq)
Helper-->>BE: https://api.stack-auth.com
BE->>Tokens: "createAuthTokens({ apiUrl })"
Tokens-->>SDK: "JWT with iss=https://api.stack-auth.com/api/v1/projects/..."
Note over SDK,ExtOAuth: OAuth flow
SDK->>BE: GET /auth/oauth/authorize/github (Host: api.stack-auth.com)
BE->>Helper: getApiUrlForRequest(fullReq)
Helper-->>BE: https://api.stack-auth.com
BE->>Provider: "getProvider(cfg, { apiUrl })"
Provider-->>BE: "redirectUri = https://api.stack-auth.com/.../callback/github"
BE-->>SDK: Redirect to GitHub with redirect_uri
ExtOAuth->>BE: GET /auth/oauth/callback/github (Host: api.stack-auth.com)
BE->>Helper: getApiUrlForRequest(callbackReq)
Helper-->>BE: https://api.stack-auth.com
BE->>Provider: "getProvider(cfg, { apiUrl }) — redirectUri matches authorize step"
BE->>Tokens: "createAuthTokens({ apiUrl })"
Tokens-->>SDK: "JWT with iss=https://api.stack-auth.com/..."
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
docs-mintlify/migration.mdx:37-39
The `@stackframe/emails` → `@hexclave/emails` row was present in the previous version of this table but has been dropped in the rewrite. Customers who use Hexclave's stored email templates import from `@stackframe/emails` (a virtual module injected at build time), so without this row they have no documentation guidance to update that import path and would have a silent breakage after migration.
```suggestion
| `@stackframe/stack-sc` | `@hexclave/sc` |
| `@stackframe/stack-cli` | `@hexclave/cli` |
| `@stackframe/dashboard-ui-components` | `@hexclave/dashboard-ui-components` |
| `@stackframe/emails` *(virtual module in stored email templates)* | `@hexclave/emails` |
```
### Issue 2 of 2
apps/backend/src/lib/request-api-url.ts:39-45
The comment states that `x-forwarded-host` "cannot be spoofed by a client" on Vercel. This is true for Vercel edge-terminated deployments, but the code is also used in self-hosted scenarios without an edge proxy. In those deployments a client-controlled `x-forwarded-host: api.stack-auth.com` would cause a token to be minted with an `iss` from the cloud allowlist rather than the deployment's `NEXT_PUBLIC_STACK_API_URL`. The minted token would then fail validation on that same server (since `getAllowedIssuers` is env-var–based), so the practical blast radius is zero — but the comment's "cannot be spoofed" claim could mislead future readers who add new trust checks here.
```suggestion
* Trust model: on Vercel, `x-forwarded-host` is set by the edge from the
* customer-facing hostname and cannot be spoofed by a client. In self-hosted
* deployments without an edge proxy a client *can* set this header, but the
* blast radius is bounded: a spoofed cloud host (e.g. api.stack-auth.com)
* causes a token to be minted under that iss, which then fails validation on
* the self-hosted backend because getAllowedIssuers uses the deployment's
* NEXT_PUBLIC_STACK_API_URL (a different hostname) — the token is unusable.
* A spoofed host that isn't in the allowlist falls back to the env-var default.
* The helper does NOT gate on a trusted-proxy signal; it assumes the
* deployment's proxy chain sets `x-forwarded-host` from a trusted source.
```
Reviews (1): Last reviewed commit: "feat(backend): derive JWT issuer and OAu..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (13)
apps/backend/src/oauth/providers/bitbucket.tsx (1)
11-12: ⚡ Quick winUse the standard URL helper for
redirectUri.Switch this callback URL construction from string concatenation to
urlString\`` (or the project-standard URL utility).As per coding guidelines: "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 18-18, 20-20
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/bitbucket.tsx` around lines 11 - 12, The redirectUri construction in the static async create method (function create) currently builds the callback URL via string interpolation; replace that string concatenation with the project-standard URL helper (use the urlString`...` template or the project's URL utility) when forming redirectUri so parameters are correctly encoded and consistent; update all occurrences in create where redirectUri or callback URLs are built (including the instances noted around lines 18 and 20) to use urlString`...` instead of normal template strings or concatenation.apps/backend/src/oauth/providers/gitlab.tsx (1)
11-12: ⚡ Quick winUse the standard URL helper for
redirectUri.For consistency with repository conventions, build this callback URL using
urlString\`` (or the shared URL utility), not direct string concatenation.As per coding guidelines: "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 19-19, 21-21
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/gitlab.tsx` around lines 11 - 12, In the static async create method of the GitLab provider (create in apps/backend/src/oauth/providers/gitlab.tsx) replace direct string interpolation when building redirectUri and any other callback URLs with the repository's URL helper (use urlString`...` or the shared URL utility) instead of normal concatenation; locate where redirectUri is constructed (and related usages around the same block) and refactor to use urlString template literals so the callback URL is encoded/constructed consistently with project conventions.apps/backend/src/oauth/providers/facebook.tsx (1)
16-16: ⚡ Quick winUse the standard URL helper for
redirectUri.This should use
urlString\`(or equivalent shared URL builder) instead of concatenatingapiUrl` with a string path.As per coding guidelines: "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 18-18, 23-23, 30-30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/facebook.tsx` at line 16, The code is building redirect and endpoint URLs by concatenating apiUrl with path strings; replace those concatenations to use the shared URL builder (urlString``) so URLs are constructed consistently and safely. Locate where the apiUrl parameter is combined (the redirectUri and other endpoint strings referenced in this file, e.g., where redirectUri is set and where API endpoints are formed) and change them to use urlString template calls (and wrap query values with encodeURIComponent if present) instead of normal string interpolation; keep the same path segments and query keys but construct them via urlString`` for all occurrences noted (including the lines flagged at 16, 18, 23, 30) so the file uses the standard URL helper consistently.apps/backend/src/oauth/providers/apple.tsx (1)
13-14: ⚡ Quick winUse the standard URL helper for
redirectUri.Please avoid
apiUrl + "/..."here and construct the callback URL withurlString\`` (or the project-standard URL utility) for consistency.As per coding guidelines: "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 20-20, 26-26
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/apple.tsx` around lines 13 - 14, The redirectUri is being built via string concatenation inside the static async create method; replace any occurrences of concatenating apiUrl + "/..." (in the create method and the other two spots where redirect/callback URLs are assembled) with the project-standard URL helper (e.g., use urlString`...` or the shared URL utility) to construct the callback URL safely and consistently, updating the redirectUri construction in the static async create function and the two other concatenation sites to use that helper.apps/backend/src/oauth/providers/github.tsx (1)
16-16: ⚡ Quick winUse the standard URL helper for
redirectUri.Please construct this URL with
urlString\`(or the project URL helper) instead ofapiUrl + "/..."`.As per coding guidelines: "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 18-18, 25-25, 38-38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/github.tsx` at line 16, Replace all string concatenations that build redirectUri using apiUrl + "/..." with the project's URL helper or the urlString`` template tag: locate the redirectUri constructions (references to apiUrl and redirectUri in this file, e.g., where apiUrl + "/..." is used around the GitHub OAuth provider code) and change them to urlString`{apiUrl}/path` (or the canonical project URL helper) so the URLs are constructed via the helper/template instead of plain concatenation; update every occurrence mentioned (uses at the redirectUri assignments and related places around apiUrl at the top of the GitHub provider file).apps/backend/src/oauth/providers/discord.tsx (1)
14-14: ⚡ Quick winUse the standard URL helper for
redirectUri.Please replace
apiUrl + "/..."withurlString\`` (or the shared URL helper used in this codebase) for callback URL construction.As per coding guidelines: "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency".Also applies to: 16-16, 21-21, 23-23
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/discord.tsx` at line 14, The code is concatenating callback URLs using apiUrl + "/..." — update all places where redirectUri (and any callback URL construction) is built from apiUrl (including occurrences around the current declarations) to use the standardized URL helper template (e.g., urlString`...`) or the shared URL helper used in this codebase instead of string concatenation; locate the redirectUri construction(s) that reference the apiUrl parameter (and the functions/methods that consume it) and replace patterns like apiUrl + "/path" with urlString`{apiUrl}/path` (or the equivalent project helper) for each occurrence noted (lines ~14, ~16, ~21, ~23).apps/backend/src/oauth/providers/x.tsx (1)
12-21: ⚡ Quick winUse structured URL construction for X callback URI.
Please avoid direct string concatenation for the redirect URI.
♻️ Proposed change
- redirectUri: apiUrl + "/api/v1/auth/oauth/callback/x", + redirectUri: new URL("/api/v1/auth/oauth/callback/x", apiUrl).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/x.tsx` around lines 12 - 21, The redirectUri in XProvider.create is being built via string concatenation; update the construction in the call to OAuthBaseProvider.createConstructorArgs so the callback URL is built using a proper URL API (e.g., new URL or a urlString helper) or by encoding path segments with encodeURIComponent instead of apiUrl + "/api/v1/auth/oauth/callback/x"; modify the redirectUri value to derive the full callback URL by resolving the path against the apiUrl (preserving any trailing slash handling) so XProvider.create and OAuthBaseProvider.createConstructorArgs get a well-formed, encoded URL.apps/backend/src/oauth/providers/mock.tsx (1)
12-16: ⚡ Quick winEncode
providerIdwhen buildingredirectUri.
providerIdis a dynamic path segment and should be URL-encoded before inclusion.♻️ Proposed change
- redirectUri: `${options.apiUrl}/api/v1/auth/oauth/callback/${providerId}`, + redirectUri: new URL( + `/api/v1/auth/oauth/callback/${encodeURIComponent(providerId)}`, + options.apiUrl + ).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/mock.tsx` around lines 12 - 16, The redirectUri is built with an unconstrained providerId; update MockProvider.create to URL-encode providerId before interpolation (when calling OAuthBaseProvider.createConstructorArgs) — e.g., compute an encodedId using encodeURIComponent(providerId) or a url-building helper and use that encodedId in the redirectUri template string (`redirectUri: \`${options.apiUrl}/api/v1/auth/oauth/callback/${encodedId}\``) so dynamic path segments are safely encoded; change the reference in the create method of MockProvider and ensure any helper (urlString) is used consistently if available.apps/backend/src/oauth/providers/linkedin.tsx (1)
13-27: ⚡ Quick winConstruct
redirectUriwith a URL builder, not string concat.This keeps redirect URI generation consistent and avoids subtle path/base formatting issues.
♻️ Proposed change
- redirectUri: apiUrl + "/api/v1/auth/oauth/callback/linkedin", + redirectUri: new URL("/api/v1/auth/oauth/callback/linkedin", apiUrl).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/linkedin.tsx` around lines 13 - 27, The redirectUri is built via string concatenation in LinkedInProvider.create; update it to use a URL builder helper (e.g., urlString or new URL) so paths and slashes are handled consistently: construct the base URL from apiUrl and append the path "/api/v1/auth/oauth/callback/linkedin" using the chosen URL helper, then pass that built string into OAuthBaseProvider.createConstructorArgs (referencing LinkedInProvider.create and OAuthBaseProvider.createConstructorArgs) instead of apiUrl + "/api/v1/auth/oauth/callback/linkedin".apps/backend/src/oauth/providers/google.tsx (1)
11-23: ⚡ Quick winUse structured URL construction for
redirectUri.
redirectUriis currently built via string concatenation. Please switch to structured URL construction to keep URL handling consistent and robust.♻️ Proposed change
- redirectUri: apiUrl + "/api/v1/auth/oauth/callback/google", + redirectUri: new URL("/api/v1/auth/oauth/callback/google", apiUrl).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/google.tsx` around lines 11 - 23, The redirectUri is currently built via string concatenation inside GoogleProvider.create when calling OAuthBaseProvider.createConstructorArgs; change it to a structured URL build (e.g., use the project's urlString helper or the URL constructor) to join apiUrl and the path "/api/v1/auth/oauth/callback/google" and ensure proper encoding (use encodeURIComponent for any dynamic segments if needed). Update the redirectUri argument passed to OAuthBaseProvider.createConstructorArgs accordingly so it reliably handles trailing slashes and encodings.apps/backend/src/oauth/providers/spotify.tsx (1)
11-24: ⚡ Quick winAvoid concatenated redirect URI strings.
Use structured URL construction for consistency and safer base/path handling.
♻️ Proposed change
- redirectUri: apiUrl + "/api/v1/auth/oauth/callback/spotify", + redirectUri: new URL("/api/v1/auth/oauth/callback/spotify", apiUrl).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/spotify.tsx` around lines 11 - 24, The redirectUri is being built by simple string concatenation in SpotifyProvider.create when calling OAuthBaseProvider.createConstructorArgs; replace the concatenation with structured URL construction (e.g., use the URL constructor or your project's urlString helper) to safely join apiUrl and the path and ensure proper encoding—update the redirectUri argument passed to OAuthBaseProvider.createConstructorArgs and keep other fields unchanged.apps/backend/src/oauth/providers/microsoft.tsx (1)
11-30: ⚡ Quick winUse consistent URL construction for Microsoft callback URI.
Please avoid concatenating URL strings directly here.
♻️ Proposed change
- redirectUri: apiUrl + "/api/v1/auth/oauth/callback/microsoft", + redirectUri: new URL("/api/v1/auth/oauth/callback/microsoft", apiUrl).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/microsoft.tsx` around lines 11 - 30, The redirectUri is built via string concatenation (redirectUri: apiUrl + "/api/v1/auth/oauth/callback/microsoft") inside MicrosoftProvider.create; replace this with a proper URL construction using the project's URL helper (e.g., urlString) or the URL constructor to join apiUrl and the callback path so the callback URI is encoded/normalized correctly before passing into OAuthBaseProvider.createConstructorArgs and the MicrosoftProvider.apps/backend/src/oauth/providers/twitch.tsx (1)
11-25: ⚡ Quick winBuild Twitch callback URI via URL API.
This avoids manual string joining and keeps URI generation consistent.
♻️ Proposed change
- redirectUri: apiUrl + "/api/v1/auth/oauth/callback/twitch", + redirectUri: new URL("/api/v1/auth/oauth/callback/twitch", apiUrl).toString(),As per coding guidelines, "Use
urlString`` orencodeURIComponent()` for URL construction instead of normal string interpolation, for consistency."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/backend/src/oauth/providers/twitch.tsx` around lines 11 - 25, The redirectUri is built via string concatenation inside TwitchProvider.create when calling OAuthBaseProvider.createConstructorArgs; replace that manual join with the URL API by constructing the callback path against the provided apiUrl (e.g., new URL('/api/v1/auth/oauth/callback/twitch', apiUrl).toString()) and supply that result as the redirectUri to ensure correct encoding and consistent URI formation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/backend/src/route-handlers/verification-code-handler.tsx`:
- Line 219: The post-handler is bypassing type-safety by passing requestBody as
any to options.handler (and similar casts in options.validate and
options.details); instead ensure requestBody is typed/validated as RequestBody
before the handoff—either refine the inferred type when parsing the incoming
body or run/return a transformation that yields RequestBody and pass that
validated/typed value (no any) into options.validate, options.handler and
options.details (references: requestBody, options.validate, options.handler,
options.details, validatedMethod, validatedData, auth.tenancy, auth.user,
getApiUrlForRequest, fullReq); update the parse/validate code so requestBody is
of type RequestBody or map it to that type prior to the calls.
---
Nitpick comments:
In `@apps/backend/src/oauth/providers/apple.tsx`:
- Around line 13-14: The redirectUri is being built via string concatenation
inside the static async create method; replace any occurrences of concatenating
apiUrl + "/..." (in the create method and the other two spots where
redirect/callback URLs are assembled) with the project-standard URL helper
(e.g., use urlString`...` or the shared URL utility) to construct the callback
URL safely and consistently, updating the redirectUri construction in the static
async create function and the two other concatenation sites to use that helper.
In `@apps/backend/src/oauth/providers/bitbucket.tsx`:
- Around line 11-12: The redirectUri construction in the static async create
method (function create) currently builds the callback URL via string
interpolation; replace that string concatenation with the project-standard URL
helper (use the urlString`...` template or the project's URL utility) when
forming redirectUri so parameters are correctly encoded and consistent; update
all occurrences in create where redirectUri or callback URLs are built
(including the instances noted around lines 18 and 20) to use urlString`...`
instead of normal template strings or concatenation.
In `@apps/backend/src/oauth/providers/discord.tsx`:
- Line 14: The code is concatenating callback URLs using apiUrl + "/..." —
update all places where redirectUri (and any callback URL construction) is built
from apiUrl (including occurrences around the current declarations) to use the
standardized URL helper template (e.g., urlString`...`) or the shared URL helper
used in this codebase instead of string concatenation; locate the redirectUri
construction(s) that reference the apiUrl parameter (and the functions/methods
that consume it) and replace patterns like apiUrl + "/path" with
urlString`{apiUrl}/path` (or the equivalent project helper) for each occurrence
noted (lines ~14, ~16, ~21, ~23).
In `@apps/backend/src/oauth/providers/facebook.tsx`:
- Line 16: The code is building redirect and endpoint URLs by concatenating
apiUrl with path strings; replace those concatenations to use the shared URL
builder (urlString``) so URLs are constructed consistently and safely. Locate
where the apiUrl parameter is combined (the redirectUri and other endpoint
strings referenced in this file, e.g., where redirectUri is set and where API
endpoints are formed) and change them to use urlString template calls (and wrap
query values with encodeURIComponent if present) instead of normal string
interpolation; keep the same path segments and query keys but construct them via
urlString`` for all occurrences noted (including the lines flagged at 16, 18,
23, 30) so the file uses the standard URL helper consistently.
In `@apps/backend/src/oauth/providers/github.tsx`:
- Line 16: Replace all string concatenations that build redirectUri using apiUrl
+ "/..." with the project's URL helper or the urlString`` template tag: locate
the redirectUri constructions (references to apiUrl and redirectUri in this
file, e.g., where apiUrl + "/..." is used around the GitHub OAuth provider code)
and change them to urlString`{apiUrl}/path` (or the canonical project URL
helper) so the URLs are constructed via the helper/template instead of plain
concatenation; update every occurrence mentioned (uses at the redirectUri
assignments and related places around apiUrl at the top of the GitHub provider
file).
In `@apps/backend/src/oauth/providers/gitlab.tsx`:
- Around line 11-12: In the static async create method of the GitLab provider
(create in apps/backend/src/oauth/providers/gitlab.tsx) replace direct string
interpolation when building redirectUri and any other callback URLs with the
repository's URL helper (use urlString`...` or the shared URL utility) instead
of normal concatenation; locate where redirectUri is constructed (and related
usages around the same block) and refactor to use urlString template literals so
the callback URL is encoded/constructed consistently with project conventions.
In `@apps/backend/src/oauth/providers/google.tsx`:
- Around line 11-23: The redirectUri is currently built via string concatenation
inside GoogleProvider.create when calling
OAuthBaseProvider.createConstructorArgs; change it to a structured URL build
(e.g., use the project's urlString helper or the URL constructor) to join apiUrl
and the path "/api/v1/auth/oauth/callback/google" and ensure proper encoding
(use encodeURIComponent for any dynamic segments if needed). Update the
redirectUri argument passed to OAuthBaseProvider.createConstructorArgs
accordingly so it reliably handles trailing slashes and encodings.
In `@apps/backend/src/oauth/providers/linkedin.tsx`:
- Around line 13-27: The redirectUri is built via string concatenation in
LinkedInProvider.create; update it to use a URL builder helper (e.g., urlString
or new URL) so paths and slashes are handled consistently: construct the base
URL from apiUrl and append the path "/api/v1/auth/oauth/callback/linkedin" using
the chosen URL helper, then pass that built string into
OAuthBaseProvider.createConstructorArgs (referencing LinkedInProvider.create and
OAuthBaseProvider.createConstructorArgs) instead of apiUrl +
"/api/v1/auth/oauth/callback/linkedin".
In `@apps/backend/src/oauth/providers/microsoft.tsx`:
- Around line 11-30: The redirectUri is built via string concatenation
(redirectUri: apiUrl + "/api/v1/auth/oauth/callback/microsoft") inside
MicrosoftProvider.create; replace this with a proper URL construction using the
project's URL helper (e.g., urlString) or the URL constructor to join apiUrl and
the callback path so the callback URI is encoded/normalized correctly before
passing into OAuthBaseProvider.createConstructorArgs and the MicrosoftProvider.
In `@apps/backend/src/oauth/providers/mock.tsx`:
- Around line 12-16: The redirectUri is built with an unconstrained providerId;
update MockProvider.create to URL-encode providerId before interpolation (when
calling OAuthBaseProvider.createConstructorArgs) — e.g., compute an encodedId
using encodeURIComponent(providerId) or a url-building helper and use that
encodedId in the redirectUri template string (`redirectUri:
\`${options.apiUrl}/api/v1/auth/oauth/callback/${encodedId}\``) so dynamic path
segments are safely encoded; change the reference in the create method of
MockProvider and ensure any helper (urlString) is used consistently if
available.
In `@apps/backend/src/oauth/providers/spotify.tsx`:
- Around line 11-24: The redirectUri is being built by simple string
concatenation in SpotifyProvider.create when calling
OAuthBaseProvider.createConstructorArgs; replace the concatenation with
structured URL construction (e.g., use the URL constructor or your project's
urlString helper) to safely join apiUrl and the path and ensure proper
encoding—update the redirectUri argument passed to
OAuthBaseProvider.createConstructorArgs and keep other fields unchanged.
In `@apps/backend/src/oauth/providers/twitch.tsx`:
- Around line 11-25: The redirectUri is built via string concatenation inside
TwitchProvider.create when calling OAuthBaseProvider.createConstructorArgs;
replace that manual join with the URL API by constructing the callback path
against the provided apiUrl (e.g., new URL('/api/v1/auth/oauth/callback/twitch',
apiUrl).toString()) and supply that result as the redirectUri to ensure correct
encoding and consistent URI formation.
In `@apps/backend/src/oauth/providers/x.tsx`:
- Around line 12-21: The redirectUri in XProvider.create is being built via
string concatenation; update the construction in the call to
OAuthBaseProvider.createConstructorArgs so the callback URL is built using a
proper URL API (e.g., new URL or a urlString helper) or by encoding path
segments with encodeURIComponent instead of apiUrl +
"/api/v1/auth/oauth/callback/x"; modify the redirectUri value to derive the full
callback URL by resolving the path against the apiUrl (preserving any trailing
slash handling) so XProvider.create and OAuthBaseProvider.createConstructorArgs
get a well-formed, encoded URL.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3bc95fa7-d2c0-4a1a-8066-0dd37626cf44
📒 Files selected for processing (38)
apps/backend/src/app/api/latest/auth/anonymous/sign-up/route.tsapps/backend/src/app/api/latest/auth/cli/complete/route.tsxapps/backend/src/app/api/latest/auth/mfa/sign-in/verification-code-handler.tsxapps/backend/src/app/api/latest/auth/oauth/authorize/[provider_id]/route.tsxapps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsxapps/backend/src/app/api/latest/auth/oauth/callback/apple/native/route.tsxapps/backend/src/app/api/latest/auth/oauth/cross-domain/authorize/route.tsxapps/backend/src/app/api/latest/auth/oauth/token/route.tsxapps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsxapps/backend/src/app/api/latest/auth/passkey/sign-in/verification-code-handler.tsxapps/backend/src/app/api/latest/auth/password/sign-in/route.tsxapps/backend/src/app/api/latest/auth/password/sign-up/route.tsxapps/backend/src/app/api/latest/auth/sessions/current/refresh/route.tsxapps/backend/src/app/api/latest/auth/sessions/route.tsxapps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/[provider_account_id]/access-token/crud.tsxapps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsxapps/backend/src/lib/request-api-url.tsapps/backend/src/lib/tokens.tsxapps/backend/src/oauth/index.tsxapps/backend/src/oauth/model.tsxapps/backend/src/oauth/providers/apple.tsxapps/backend/src/oauth/providers/bitbucket.tsxapps/backend/src/oauth/providers/discord.tsxapps/backend/src/oauth/providers/facebook.tsxapps/backend/src/oauth/providers/github.tsxapps/backend/src/oauth/providers/gitlab.tsxapps/backend/src/oauth/providers/google.tsxapps/backend/src/oauth/providers/linkedin.tsxapps/backend/src/oauth/providers/microsoft.tsxapps/backend/src/oauth/providers/mock.tsxapps/backend/src/oauth/providers/spotify.tsxapps/backend/src/oauth/providers/twitch.tsxapps/backend/src/oauth/providers/x.tsxapps/backend/src/route-handlers/verification-code-handler.tsxapps/backend/src/stack.tsxdocs-mintlify/migration.mdxpackages/stack-shared/src/utils/urls.tsxpackages/template/src/lib/stack-app/apps/implementations/common.ts
- Remove the prominent top-of-page warning; the same information now lives in the relevant sections, where it applies to the smaller subset of customers doing manual JWT verification or running custom OAuth apps. - Section 2 (manual JWT verification): show only the new api.hexclave.com issuer in the After example. Customers migrating to @hexclave/* are explicitly opting into the new host, so there's no reason to teach the array-of-issuers compat pattern. - Section 3 (OAuth callback URLs): same simplification — instruct customers to set the new api.hexclave.com URL and stop there. Drop the byte-by-byte explanation and the GitHub-OAuth-Apps single-URL warning. Add a brief Note that most providers allow registering both the old and new URLs temporarily to avoid signup downtime during the SDK swap.
There was a problem hiding this comment.
1 issue found across 38 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs-mintlify/migration.mdx (1)
46-60:⚠️ Potential issue | 🟠 Major | ⚡ Quick winConsider guidance for gradual migration with mixed SDK versions.
The backend's
getAllowedIssuersuses an alias map to accept bothapi.stack-auth.comandapi.hexclave.comissuers (per PR summary), but the migration doc instructs users to update manual JWT verification to only check the new hexclave issuer. During a gradual rollout where some clients use@stackframe/*(issuing tokens withstack-auth.comissuer) and others use@hexclave/*(issuing tokens withhexclave.comissuer), manual verification that only checks the hexclave issuer will reject tokens from old SDK clients.The commit message indicates the array-of-issuers compatibility example was intentionally removed to simplify the doc. However, for users with multi-client deployments (web + mobile, multiple services, etc.) performing gradual rollouts, the simplified guidance may cause production authentication failures.
🔄 Suggested addition for transition period
Add a note after line 60:
<Note> During a gradual migration where some clients still use `@stackframe/*`, your manual JWT verification should accept **both** issuers: ```ts const { payload } = await jose.jwtVerify(token, jwks, { issuer: [ 'https://api.stack-auth.com/api/v1/projects/YOUR_PROJECT_ID', 'https://api.hexclave.com/api/v1/projects/YOUR_PROJECT_ID', ], audience: 'YOUR_PROJECT_ID', });Once all clients are migrated, you can remove the
stack-auth.comissuer.
</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs-mintlify/migration.mdxaround lines 46 - 60, Add guidance for gradual
migration to the migration.mdx around the jose.jwtVerify example: explain that
during rollouts you should accept both issuers (referencing the backend helper
getAllowedIssuers and the jose.jwtVerify call) by showing that issuer can be an
array containing both 'https://api.stack-auth.com/.../projects/YOUR_PROJECT_ID'
and 'https://api.hexclave.com/.../projects/YOUR_PROJECT_ID', and note to remove
the old stack-auth.com issuer once all clients use@hexclave/*; keep the
existing single-issuer example but add this transitional note immediately after
it.</details> </blockquote></details> </blockquote></details>🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. Inline comments: In `@docs-mintlify/migration.mdx`: - Line 62: Add concrete example issuer strings for the anonymous and restricted issuer variants in the migration docs: insert two example lines showing the exact issuer values to use when verifying JWTs, e.g. "issuer: 'https://api.hexclave.com/api/v1/projects-anonymous-users/YOUR_PROJECT_ID'" for anonymous and "issuer: 'https://api.hexclave.com/api/v1/projects-restricted-users/YOUR_PROJECT_ID'" for restricted so readers can copy the exact formats. --- Outside diff comments: In `@docs-mintlify/migration.mdx`: - Around line 46-60: Add guidance for gradual migration to the migration.mdx around the jose.jwtVerify example: explain that during rollouts you should accept both issuers (referencing the backend helper getAllowedIssuers and the jose.jwtVerify call) by showing that issuer can be an array containing both 'https://api.stack-auth.com/.../projects/YOUR_PROJECT_ID' and 'https://api.hexclave.com/.../projects/YOUR_PROJECT_ID', and note to remove the old stack-auth.com issuer once all clients use `@hexclave/`*; keep the existing single-issuer example but add this transitional note immediately after it.🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID:
670baccb-8027-49fe-ab35-54add96fab7f📒 Files selected for processing (1)
docs-mintlify/migration.mdx
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Resolves conflicts from #1498 (request-host-derived JWT iss / OAuth redirect_uri) by taking dev's structural changes wholesale, then rewriting @stackframe/stack-shared -> @hexclave/shared on the merged-in files to match this branch's PR-3 rename convention. tokens.tsx adopts dev's CLOUD_HOST_PAIRS-derived issuerHostAliases (adds the staging host pair the hand-rolled map was missing). pnpm typecheck: 28/28 pass pnpm lint: 28/28 pass
Summary
When the backend serves both
api.stack-auth.comandapi.hexclave.comfrom the same deployment, signed JWTissclaims and OAuthredirect_urivalues need to match the host the customer's SDK actually talks to — otherwise customers with hardcoded issuer checks or registered OAuth callback URLs break when their SDK upgrades. This PR makes both follow the request host.Closes the "Interpretation B" plan from our earlier discussion.
Changes
Backend — request-host-derived
issandredirect_uriapps/backend/src/lib/request-api-url.tsexportsgetApiUrlForRequest(req)andgetApiUrlForHost(host). A consolidatedCLOUD_HOST_PAIRSconstant is the single source of truth for the stack-auth ↔ hexclave host pairs (prod, dev, staging). Both the allowlist here and the validator alias map intokens.tsxderive from it, so they can never drift again.apps/backend/src/lib/tokens.tsx) —getIssuernow takesapiUrl.generateAccessTokenFromRefreshTokenIfValidandcreateAuthTokensaccept anapiUrlparameter that flows into theissclaim.getAllowedIssuersstays env-driven with the bidirectional alias map, so tokens cross-validate across hosts.redirect_uriper request — all 12 providers +MockProvidernow takeapiUrland use it to buildredirect_uri = apiUrl + "/api/v1/auth/oauth/callback/<provider>".getProvider()accepts an{ apiUrl }option and forwards it.oauthServersingleton became a per-requestcreateOAuthServer({ apiUrl })factory soOAuthModel.generateAccessTokenmints tokens with the rightiss. Used in the callback route, the token route, and the cross-domain-authorize helper.createAuthTokensinvocations (password sign-up/sign-in, sessions create, sessions/current/refresh, anonymous sign-up, apple-native callback, MFA/OTP/passkey sign-in, OAuth model token exchange), plus the CLI-completegenerateAccessTokenFromRefreshTokenIfValiddirect call, now passgetApiUrlForRequest(fullReq).createVerificationCodeHandlerrefactored to passapiUrlas a 6th positional arg to the user's handler, so MFA/OTP/passkey sign-in flows get the same per-requestissas the rest. The other 8 callers (password-reset, contact-channels-verify, etc.) don't need changes — they accept fewer args and TS function-arity compatibility makes that fine.SDK — freeze
@stackframe/*defaults at stack-auth.compackages/template/src/lib/stack-app/apps/implementations/common.tsrevertsdefaultBaseUrlanddefaultAnalyticsBaseUrltohttps://api.stack-auth.com/https://r.stack-auth.com. A customer who upgrades their@stackframe/*package to the latest version without explicitly migrating to@hexclave/*keeps hittingapi.stack-auth.comand never sees their JWTissor OAuthredirect_urichange.@hexclave/*mirror packages are unaffected because they were published from source whendefaultBaseUrl = "https://api.hexclave.com"; v1.0.0 already targets the hexclave host on npm. Extendingscripts/rewrite-packages-to-hexclave.tsto substitute these literals during future republishes is a separate follow-up.Docs — migration guide rewrite
docs-mintlify/migration.mdxrewritten concisely. Spells out the two host-visible changes that require pre-deploy action when migrating to@hexclave/*: updating manual JWT verifier code (with the array-of-issuers pattern) and updating OAuth callback URLs at each provider (with the GitHub-OAuth-Apps single-URL caveat explicitly called out).What was deliberately left out
@hexclave/*republishes — separate follow-up.api.hexclave.com) — out of scope per discussion.Verification
pnpm typecheck— 29/29 packages pass.pnpm lint— 29/29 packages pass.api.staging.*entries inissuerHostAliases(would have broken cross-host token validation on staging).apps/backend/src/stack.tsx.getHardcodedFallbackUrls.issinstead of the request host.Summary by cubic
Makes JWT issuers and OAuth redirect_uri values follow the incoming request’s host so tokens and redirects always match
api.stack-auth.comorapi.hexclave.com. Also freezes@stackframe/*SDK defaults to Stack Auth and tightens the migration guide to focus on OAuth callbacks.New Features
request-api-urlhelper with an allowlist of cloud hosts; unknown hosts fall back to the deployment’s API URL.issnow uses the per-request API URL; validation accepts both brands via aliases derived from oneCLOUD_HOST_PAIRSsource.redirect_urifrom the request host;getProvider()now takes{ apiUrl }.createOAuthServer({ apiUrl })so token exchange mints with the right issuer.@stackframe/*defaults point tohttps://api.stack-auth.com;@hexclave/*mirrors remain hexclave-branded.Migration
https://api.hexclave.com/api/v1/auth/oauth/callback/<provider>when migrating to@hexclave/*.Written for commit e42dfaf. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
New Features
Updates
Documentation