Skip to content

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 /dashboard

Worker Callback Handler

js
// 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:

jsx
// 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=privacyai

SM API will:

  1. Check if user has an active sm_client session
  2. If yes → generate exchange token → redirect to app.privacyai.com/auth/callback?token=...
  3. 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.

Sprint Mode LLC — Internal Platform Documentation