SSO contract
Users sign in once at app.bizbasics.ai. The platform is an OIDC-style identity provider: it issues a short-lived, asymmetrically-signed access token your product verifies locally — no login to implement, no callback on every request, and no shared secret.
Your product also exposes four endpoints. The contract tests in tests/contract/ validate them, and the scaffold implements all four for you.
The fast path — verify the bb_at cookie via JWKS
On login the platform sets an access-token cookie named bb_at on .bizbasics.ai. Because your product runs on a subdomain (your-product.bizbasics.ai), the browser sends it to you automatically. Verify it RS256 against the platform JWKS — no call back to the platform:
# Public keys (cache them; rotate by kid):
GET https://auth.bizbasics.ai/.well-known/jwks.json
# -> { "keys": [ { "kty":"RSA", "use":"sig", "alg":"RS256", "kid":"...", "n":"...", "e":"AQAB" } ] }
# bb_at is a standard JWT. Match the header "kid" to a JWKS key, verify RS256, read claims:
{
"user_id": "...", "email": "...", "org_id": "...", "role": "org_owner",
"plan": "pro", "apps": ["relay", "monk", "your-product"], "quotas": { ... },
"iss": "https://auth.bizbasics.ai", "exp": 1781809999
}- Authorize by checking your product slug is in
apps(e.g."your-product" ∈ apps). Entitlement is in the token — don't call back for it. bb_atis short-lived (~20 min). Verify it, then mint your own session cookie (below) for continuity, and re-checkbb_atperiodically to pick up entitlement changes and honour platform logout.- Verify
bb_at, neverbb_session.bb_sessionis the platform's internal, instantly-revocable session — not yours to read.
apps, quotas). The quota snapshot is bounded by the token TTL — for hard, real-time limits, call the quota API (see the API reference). Never read live limits from a JWT.The launcher handoff — one-time token
When a user clicks your product in the launcher (or arrives without a valid bb_at), the platform mints a one-time app token and redirects the browser to your product:
GET https://your-product.bizbasics.ai/auth/sso?token=<token>
Path /auth/sso, single query param token (no redirect param is sent today — land the user at your own home). Your handler exchanges it server-to-server, authenticating with your product's SSO credential (bbas_…) as X-Internal-Key:
GET https://auth.bizbasics.ai/api/v1/internal/verify-app-token?token=<token>
-H "X-Internal-Key: bbas_..."
# 200 ->
{ "user_id": "...", "email": "...", "full_name": "...",
"org_id": "...", "role": "org_member", "app": "your-product",
"is_platform_admin": false }Tokens are single-use and expire quickly — exchange immediately, never store them. Note this response carries identity only; it omits apps/plan/quotas — get those from bb_at or the catalog API. On success, set your own session cookie and 302 to redirect.
Your own session cookie
Both paths converge here: once you trust the user, mint your own session JWT signed with your own APP_SESSION_SECRET (≥32 random bytes you generate — never the platform's secret) and set it httpOnly on your domain. Cookies only, never localStorage.
Re-establishing the session
When bb_at is absent or expired, refresh it server-side, no visible redirect — you still hold the platform bb_session cookie (same domain). Call silent SSO, then exchange the returned token like the launcher handoff:
POST https://auth.bizbasics.ai/api/v1/internal/silent-sso?app=<slug> -H "Authorization: Bearer <bb_session cookie value>" -H "X-Internal-Key: bbas_..." # -> a one-time token; exchange it at verify-app-token
If bb_session is also gone (fully logged out), redirect the browser to https://app.bizbasics.ai to sign in; the user re-enters via the launcher.
Where your service runs
Provisioning puts you at https://<slug>.bizbasics.ai (API at /api), on cookie domain bizbasics.ai. So your API receives the bb_at cookie directly — no need to forward it from the frontend.
Session lifetime & logout
bb_at~20 min; the platformbb_session~8h. Re-verifybb_aton every request — it's a local RS256 check against cached JWKS, effectively free.- Cap your own product session to a modest TTL (≈1–2h) and re-check via silent SSO on navigation, so entitlement changes and logout reach you promptly.
session.revoked webhook). Logout propagation to your product is bounded by your session TTL + how often you re-check bb_at/silent-SSO — keep both short if you need it fast.Your four endpoints
/auth/sso, /health, /ready,/bootstrap — not under /api. /api is your own business API. And /auth/sso is a GET (the launcher redirects the browser to it).1. GET /auth/sso
Handles the launcher handoff above (one-time token → your session).
2. GET /bootstrap
Called by your frontend after sign-in, authed by your own session cookie. Must return all four keys (the contract test checks them):
{
"user": { "id": "...", "email": "...", "full_name": "..." },
"org": { "id": "...", "name": "...", "slug": "..." },
"capabilities": ["create_channel", "invite_member"],
"quotas": { "seats": 25, "channels_remaining": 47 }
}capabilities/quotas are yours to define — derive them from the role/plan/apps in bb_at and your own usage counters.
3. GET /health
{ "status": "ok", "service": "<service-name>", "version": "<git-sha-or-semver>" }service must match your canonical service name — the contract test asserts it, to catch an ingress routing to the wrong backend.
4. GET /ready
Readiness probe — 200 once dependencies (e.g. your DB) are reachable.
Credentials
bbas_…(SSO credential, scoped to your product) — authenticates your calls to the platform's internal endpoints (verify-app-token, quota, webhooks). Issue it at Settings → SSO credentials.bbk_…(API key) — for the data API (workspace records). Settings → API keys.APP_SESSION_SECRET— you generate it, to sign your own session.
bbas_/bbk_. A credential that could sign or impersonate the platform would be a security hole — by design, none is issued.Establishing your session
After you verify the handoff token, mint your own session and set it as an httpOnly cookie, then redirect into your app (build the destination from APP_URL). Your frontend establishes auth by calling your API with credentials: "include" — the browser sends the cookie, your API validates it.
localStorage and have the frontend read it from there: JavaScript can't read an httpOnly cookie, so the frontend sees "no session" and loops straight back to login. Derive auth from the cookie via an API call. The Runtime contract has the full checklist and a symptom→fix table.Why it's built this way
- Local JWKS verification — verify
bb_atwith public keys; no per-request callback, no shared signing secret to leak or rotate by hand (rotation is JWKS/kid-native). - Short-lived access token + separate platform session —
bb_sessionstays the platform's instantly-revocable session;bb_at's short TTL bounds how long a stale entitlement or a logout takes to reach you. - Derived, scoped credentials — your platform API identity comes from your
bbas_/bbk_, so you can only ever act as your own product. - Cookies only, never localStorage — XSS in your product can't exfiltrate a session.