Bbizbasics/ developers

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.

Browseryour customerbizbasicsauth.bizbasics.aiYour product/auth/ssosign in at app.bizbasics.ai1redirect → /auth/sso?token=… (one-time)2verify-app-token · X-Internal-Key3{ user · org · role · plan · apps }4set own session cookie · render5

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_at is short-lived (~20 min). Verify it, then mint your own session cookie (below) for continuity, and re-check bb_at periodically to pick up entitlement changes and honour platform logout.
  • Verify bb_at, never bb_session. bb_session is the platform's internal, instantly-revocable session — not yours to read.
Entitlements and quota arrive in the token (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 platform bb_session ~8h. Re-verify bb_at on 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.
There is no platform-to-product logout push today (no 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

All four are served at the bare path on your product domain — /auth/sso, /health, /ready,/bootstrapnot 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.
You are never given the platform's signing secret or a shared internal key. Verifying the user is done by JWKS (public keys); your platform API calls use your own scoped 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.

Do not stash the token in 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_at with 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 sessionbb_session stays 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.
© bizbasics — developer platform All systems operational