Skip to main content
Documentation

Passport SSO

How the developer portal authenticates you through Realsee Passport.

Realsee Passport is the SSO that gates the developer portal. This page documents the sign-in handshake, the cookies involved, the CSRF protection, and how to detect login state from your own code.

When this matters

Passport is involved only when you (the developer) sign in to /dashboard. End-users of your application do not sign in through Passport. Browser-safe Five SDK flows receive only the scene data and public credentials supported by the integration, while OpenAPI credentials remain on your server. If you are building a public-facing integration, Passport never appears in your code.

The sign-in flow

Browser            developer.realsee.ai            login.realsee.ai           app-gateway
   β”‚                       β”‚                              β”‚                        β”‚
   β”‚ GET /auth/sign-in?gotoURL=/dashboard/apps            β”‚                        β”‚
   β”‚ ──────────────────────▢                              β”‚                        β”‚
   β”‚                       β”‚ get source_verify ticket ────────────────────────────▢│
   β”‚                       β”‚ ◀────────────────────────────────────────────────────-β”‚
   β”‚  302 β†’ login.realsee.ai/?source=...&source_verify=...&redirect=...            β”‚
   β”‚ ◀─────────────────────                               β”‚                        β”‚
   β”‚                                                                               β”‚
   β”‚ GET login.realsee.ai/?source=...&source_verify=...&redirect=...               β”‚
   β”‚ ─────────────────────────────────────────────────────▢                        β”‚
   β”‚                       (user signs in)                β”‚                        β”‚
   β”‚  302 β†’ /auth/sign-in-callback?source=...&ticket=...&login_type=...            β”‚
   β”‚ ◀─────────────────────────────────────────────────────                        β”‚
   β”‚                       β”‚                              β”‚                        β”‚
   β”‚ GET /auth/sign-in-callback?source=...&ticket=...     β”‚                        β”‚
   β”‚ ──────────────────────▢                              β”‚                        β”‚
   β”‚                       β”‚ exchange ticket for token ───────────────────────────▢│
   β”‚                       β”‚ ◀────────────────────────────────────────────────────-β”‚
   β”‚                       β”‚ fetch user profile ──────────────────────────────────▢│
   β”‚                       β”‚ ◀────────────────────────────────────────────────────-β”‚
   β”‚  302 β†’ /dashboard/apps + Set-Cookie: realsee_token (httpOnly)                 β”‚
   β”‚ ◀──────────────────────                              β”‚                        β”‚

The five steps:

  1. Initiate β€” Sign-in starts at /auth/sign-in (the dashboard's sign-in button links there; unauthenticated /api/* calls return HTTP 401 AUTH_REQUIRED rather than redirecting). The handler asks the internal app-gateway for a fresh source_verify ticket (proof the developer portal is allowed to ask for sign-in), then 302s to login.realsee.ai/ with source, source_verify, and redirect parameters. The gotoURL query string captures where the user came from, so they land back there after sign-in.
  2. Authenticate β€” The user signs in on Passport. Passport handles passwords, MFA, OAuth providers, and recovery flows β€” the developer portal sees none of it. On success, Passport issues a short-lived ticket and redirects back.
  3. Exchange β€” /auth/sign-in-callback?source=...&ticket=...&login_type=... receives the ticket. The handler calls the app-gateway to exchange the ticket for a long-lived token, then calls the gateway again to fetch the user profile. Both calls happen server-side; the ticket never touches a long-lived store.
  4. Set cookies β€” The handler issues two cookies on the response (outside production builds both names carry an environment suffix, e.g. realsee_token_development):
    • realsee_token (httpOnly, 30 days) β€” the session token. Sent on every subsequent request.
    • realsee_account (httpOnly, 30 days) β€” the currently-selected Passport account, base64url-encoded and HMAC-SHA256-signed with the cookie secret. Remembers which account a multi-account user picked; the profile itself is still fetched from the gateway on each request.
  5. Redirect β€” The handler 302s the user to the original gotoURL (or /dashboard/apps if missing). The cookie travels with them and /dashboard now sees an authenticated session.

Cross-domain cookies

In production, the session cookies default to Domain=.realsee.ai, Path=/, SameSite=Lax, and Secure. Browsers can therefore attach them to matching HTTPS subdomains. Outside production they are host-only by default. Deployments can override either behavior with REALSEE_AUTH_COOKIE_DOMAIN.

The cookies are httpOnly, so browser JavaScript cannot read their values. That protection does not make a domain cookie host-only: the browser can still send it automatically to matching Realsee subdomains.

CSRF protection

Every mutating API endpoint on the developer portal (all are POST) is protected by a double-submit CSRF token plus an Origin/Referer check. The pattern:

  1. On GET /api/me (the session probe the portal front-end calls on load), the server mints a random token (18 bytes, base64url-encoded) and sets it as the csrf cookie (24-hour lifetime). This cookie is not httpOnly β€” by design, so client JavaScript can read it.
  2. On any mutating request, the client must:
    • Send the csrf cookie (automatic).
    • Echo the same value in the x-csrf-token request header (manual β€” your fetch wrapper sets this).
    • If an Origin or Referer header is sent (browsers send one automatically), it must point at the developer portal host.

The server compares the cookie to the header verbatim and rejects with HTTP 403 CSRF_TOKEN_MISSING when either value is absent or the two differ. Cross-origin attackers cannot read the cookie and cannot mint the matching header, so the check fails for them.

For implementation details see assertCsrf() and createCsrfCookieHeader() in src/server/http/request.ts; the client helper that echoes the header is src/client/csrf.ts. Failure codes you may encounter:

CodeMeaning
CSRF_TOKEN_MISSINGCookie or header absent, or the two values differ. Call GET /api/me first to mint the cookie.
CSRF_ORIGIN_MISMATCHAn Origin or Referer header was sent but does not match the developer portal host.

Detecting login state

Server-side, protected API handlers call requireDeveloperContext() from src/server/http/request.ts. It resolves the Passport session and distinguishes Passport unavailability, an anonymous visitor, and a signed-in user without an active Team. Client-side, you can probe login state with a simple fetch:

async function isSignedIn(): Promise<boolean> {
  const res = await fetch('/auth/get-user', { credentials: 'include' })
  if (!res.ok) return false
  const body = await res.json()
  return body?.data?.isAuthenticated === true
}

/auth/get-user never mutates state. It responds HTTP 200 with a { data: ... } envelope for both anonymous and authenticated sessions, and HTTP 503 when Passport is unavailable. When signed in, data is the developer account (userCode, isAuthenticated: true, email, name, avatarUrl, currentSource, teams); the raw Passport UserID is not exposed. When signed out, it is { isAuthenticated: false, teams: [] }. GET /api/me returns the same session shape and additionally mints the CSRF cookie.

To sign out: send the user to /auth/sign-out (GET or POST) β€” the server clears both session cookies and 302s to the gotoURL query parameter (default /docs). It does not round-trip through Passport, so only the developer portal session is ended.

Compatibility routes

The portal preserves several legacy URLs as 302 redirects for backwards compatibility:

Legacy pathCanonical target
/login/auth/sign-in
/logout/auth/sign-out
/signup/auth/sign-up
/register/auth/sign-up
/login/callback/auth/sign-in-callback
/logout/callback/auth/sign-out-callback
/signup/callback/auth/sign-up-callback

Query strings are preserved, so existing deep links with ?gotoURL=... continue to work.

Open redirect protection

The gotoURL parameter (the auth routes also accept redirect_to and next as aliases) is validated server-side before any redirect: it must start with / and must not start with // (protocol-relative). A crafted ?gotoURL=https://evil.example/phish falls back to the safe default /dashboard/apps. The check lives in safeGotoPath() in src/server/auth/cookies.ts.

Next steps