Cross-Domain SSO
The sm_client cookie is set on .sprintmode.ai — so it's automatically shared across all *.sprintmode.ai subdomains. But for products on different domains (e.g. app.privacyai.com), cross-domain SSO is required.
When You Need This
You need cross-domain SSO when your product app is not on .sprintmode.ai. Examples:
app.privacyai.com(different root domain)app.heyhub.ai- Any customer-facing domain that's not
*.sprintmode.ai
If your portal is on yourproduct.sprintmode.ai, the cookie works automatically — skip this guide.
How It Works
1. User visits app.privacyai.com/dashboard (no session)
2. Worker → redirects to /auth/login?redirect=/dashboard
3. Login page → user clicks "Continue with Google"
4. Login redirects to SM API SSO:
https://api.sprintmode.ai/auth/sso/google?redirect=https://app.privacyai.com/auth/callback&product=privacyai
5. SM API handles Google OAuth → creates session
6. Instead of setting .sprintmode.ai cookie, SM API:
- Signs a short-lived exchange token (5 min)
- Redirects to: https://app.privacyai.com/auth/callback?token=<exchange_token>
7. app.privacyai.com worker receives callback:
- POST https://api.sprintmode.ai/auth/sso/exchange { token }
- Gets session payload
- Signs a JWT with its own SESSION_SECRET
- Sets cookie on .privacyai.com domain
- Redirects to /dashboardWorker Callback Handler
// In app.privacyai.com _worker.js
import { signJWT } from '@nomadahq/sm-ui/auth'
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url)
// Handle SSO callback
if (url.pathname === '/auth/callback') {
const exchangeToken = url.searchParams.get('token')
if (!exchangeToken) return Response.redirect('/auth/login')
// Exchange token for session via SM API
const exchangeRes = await fetch('https://api.sprintmode.ai/auth/sso/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: exchangeToken })
})
if (!exchangeRes.ok) return Response.redirect('/auth/login')
const { data: session } = await exchangeRes.json()
// Sign a JWT for this domain
const jwt = await signJWT({
...session,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 7 * 24 * 3600
}, env.SESSION_SECRET)
const redirect = session.redirect || '/dashboard'
return new Response(null, {
status: 302,
headers: {
'Location': redirect,
'Set-Cookie': [
`sm_client=${jwt}`,
`Domain=.privacyai.com`, // ← your domain here
`Path=/`,
`HttpOnly`,
`Secure`,
`SameSite=Lax`,
`Max-Age=604800`
].join('; ')
}
})
}
// ... rest of worker
}
}Login Redirect
Configure the Login component to redirect to SM API SSO with your callback URL:
// On app.privacyai.com — custom Google SSO redirect
function handleGoogle() {
const callbackUrl = 'https://app.privacyai.com/auth/callback'
const params = new URLSearchParams({
redirect: callbackUrl,
product: 'privacyai'
})
window.location.href = 'https://api.sprintmode.ai/auth/sso/google?' + params
}Or use the authBase prop on the Login component — it automatically constructs the redirect with the current origin as callback.
GET /auth/sso/redirect
For programmatic cross-domain redirect (e.g. from a marketing site CTA):
https://api.sprintmode.ai/auth/sso/redirect?redirect=https://app.privacyai.com/dashboard&product=privacyaiSM API will:
- Check if user has an active
sm_clientsession - If yes → generate exchange token → redirect to
app.privacyai.com/auth/callback?token=... - If no → redirect to Google SSO with the callback flow
This lets users who are already logged in on another SM portal access a new portal without re-authenticating.
