Steven's Knowledge

Patterns

SSO, SAML, SCIM, RBAC, multi-tenant, passkeys, refresh-token rotation, impersonation

Patterns

The patterns that come up once you've moved past "users can log in." Each is small in isolation; together they're the difference between a hobby app and a real platform.

SSO: SAML and OIDC

Enterprise customers buy SSO. They use Okta / Microsoft Entra ID / Google Workspace / Azure AD as their identity provider; they want their employees to log into your app with company credentials.

Two protocols:

ProtocolWhenNotes
SAML 2.0Most enterprises (especially Okta)XML; horrible to implement; IdP does the work
OIDCNewer / Google-native customersJSON; lighter; modern

Both result in the same user experience: redirect to customer's IdP → user authenticates there → redirect back with assertion → you create/match the local user.

SSO setup per customer typically involves:

  1. Customer creates an "application" in their IdP.
  2. They give you their metadata URL or IdP entity ID + certificate.
  3. You configure that on your side (or your IdP partner does — WorkOS / Auth0 / Clerk).
  4. Test with one user.
  5. Roll out to their org.

The right architecture: your IdP partner handles this, not you. WorkOS specifically exists to ship "we support SSO from any enterprise IdP" in days, not months. Auth0 has SAML / OIDC Connections; Keycloak has Identity Brokering.

SCIM: User Provisioning

SSO solves "log in with corporate credentials." SCIM solves "automatically create/update/delete users when HR does it."

Customer's HR system          Your app
─────────────────────         ───────────────────
1. New hire joins      ─────► SCIM POST /Users      → create user
2. Promotion           ─────► SCIM PATCH /Users/123 → update attributes
3. Leaves              ─────► SCIM DELETE /Users/123→ deactivate

SCIM is a REST protocol most enterprise IdPs speak. Implementing SCIM endpoints is fiddly; provider-managed (WorkOS, Auth0, Clerk SCIM) is the standard answer.

For enterprise sales, SCIM is on the same checklist as SSO. If you support one without the other, customers ask why.

RBAC and ABAC

Role-Based Access Control (RBAC)

The default model:

Users → Roles → Permissions
alice → admin → [read:*, write:*, delete:*]
bob   → editor → [read:*, write:articles]

Most IdPs ship RBAC. Auth0, Clerk, Keycloak all support roles and permissions natively. The JWT carries them as claims.

Attribute-Based Access Control (ABAC)

When roles aren't enough:

Can alice edit document X?
  → alice.team == X.owning_team
  AND alice.tier in ["pro", "enterprise"]
  AND X.status != "archived"
  AND now() < X.lock_expiry

ABAC encodes per-resource, attribute-driven rules. Tools:

  • OPA / Rego — declarative policies; CNCF standard.
  • Cedar — AWS's policy language; powering AWS Verified Permissions.
  • Casbin — multi-language library.
  • Auth0 FGA / Permify / Warrant — Zanzibar-inspired hosted services.

For most apps, RBAC is enough. Add ABAC when the rules involve resource state.

Multi-Tenant Patterns

For B2B SaaS, users belong to organizations. The model:

User → Organization → Role within organization → Permissions
alice → AcmeCorp → admin → [team:manage, billing:read]
alice → BetaCorp → member → [team:read]   ← same user, different org

Approach 1: Org-Scoped Tokens

When alice switches orgs, she gets a new JWT scoped to that org. The JWT has org_id as a claim. Every API request is implicitly scoped.

function requireSameOrg(req, res, next) {
  if (req.params.orgId !== req.auth.org_id) {
    return res.status(403).json({ error: 'wrong org' });
  }
  next();
}

app.get('/orgs/:orgId/teams', checkJwt, requireSameOrg, getTeams);

Auth0 Organizations, Clerk Organizations, WorkOS — all support this.

Approach 2: Multi-Org in One Token

The JWT lists all orgs the user belongs to with their roles:

{
  "sub": "user_abc",
  "organizations": [
    { "org_id": "acme", "role": "admin" },
    { "org_id": "beta", "role": "member" }
  ]
}

Your API picks the right org from request context (path, header). More flexible; harder to keep tokens small.

Passkeys and WebAuthn

Passwords are slowly dying. Passkeys are device-bound credentials (synced via iCloud / Google Password Manager) that replace passwords:

  • No password to phish.
  • No password to forget.
  • No password to leak in a breach.
  • Native biometric / device PIN on login.

Most IdPs now support passkeys as a primary method:

  • Auth0 — Passkey enrollment and login built in.
  • Clerk — First-class.
  • Stytch — Their pitch.
  • Hanko / Corbado — Passkey-first IdPs.

The migration is incremental: support passwords + passkeys; nudge users to add a passkey; eventually deprecate passwords.

Refresh Token Rotation

Long-lived refresh tokens are a liability — a leaked one grants persistent access. Rotation mitigates:

1. App has refresh_token_A
2. App exchanges A for new access_token + refresh_token_B
3. A is invalidated
4. Next time, app uses B → gets new access_token + refresh_token_C
5. If anyone tries to use A again, the IdP detects "token reuse" and revokes the whole chain

This means a stolen old token quickly stops working and triggers an alert. Auth0, Clerk, Keycloak all support refresh token rotation. Always turn it on for public clients (SPAs, mobile).

Impersonation

Support needs to "act as the user" sometimes. Don't let them ask for the password. Build impersonation properly:

  • Admin opens a customer support tool.
  • Clicks "Impersonate user X."
  • Backend mints a token for X with an act (actor) claim identifying the admin.
  • Admin sees the user's UI; every action is logged as "X (acting as: admin alice)".
  • Auto-expire short; force re-login.

Most IdPs have impersonation features. Audit every impersonation session.

Social Login

The classic "Sign in with Google / Apple / GitHub" — every IdP handles this via configuration. The flow:

  1. User clicks "Sign in with Google".
  2. Redirect to Google with OAuth params.
  3. Google authenticates the user; redirects back with code.
  4. Your IdP exchanges code for Google profile.
  5. Your IdP creates / matches a local user account based on email.
  6. Returns your IdP's tokens to your app.

The IdP normalizes the result — you see a consistent req.user regardless of source.

Apple login is mandatory if you have iOS — it's an App Store rule for any app using social login.

Passwordless alternatives:

MethodUXSecurityWhere
Magic link (email)Click link → logged inOK; vulnerable if email is compromisedNotion-style
OTP via emailCode → enterSlightly better UX; same vulnerabilityCommon
OTP via SMSCode → enterSIM swap attackAvoid for high-value accounts
Authenticator app TOTPCode from appStrongCommon for MFA
PasskeysBiometricStrongestNew default

Use magic links / OTP for low-security flows (reading content) and require passkeys / MFA for sensitive actions (billing, settings, data export).

MFA Enforcement

Two policies that work:

PolicyEffect
MFA on every loginStrict; user friction
MFA on sensitive actions"step-up authentication"; better UX

Step-up: log in with password / passkey; before deleting an account or changing billing, require MFA again. The IdP records "user authenticated with MFA at time T"; your app checks auth_time claim.

Most B2B IdPs let admins enforce MFA org-wide.

Session Management

JWTs are easy and stateless but have a downside: you can't revoke them. Until expiry, a stolen JWT works.

Mitigations:

StrategyNotes
Short access token expiry (5-15 min)Stolen token has narrow window
Refresh token rotationDetect theft via reuse
Sliding session in your appTrack "last activity"; reject if too old
Token revocation list (Redis)Hybrid stateless + revocable
Opaque tokens with introspectionReal-time revocable but slow

The standard combination: short-lived access tokens + rotating refresh tokens + a Redis revocation list for the "log everywhere out" button.

What's Next

You've seen the patterns real identity systems use. Best Practices covers token security, session strategy, migration paths, vendor lock-in, observability, and compliance.

On this page