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:
| Protocol | When | Notes |
|---|---|---|
| SAML 2.0 | Most enterprises (especially Okta) | XML; horrible to implement; IdP does the work |
| OIDC | Newer / Google-native customers | JSON; 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:
- Customer creates an "application" in their IdP.
- They give you their metadata URL or IdP entity ID + certificate.
- You configure that on your side (or your IdP partner does — WorkOS / Auth0 / Clerk).
- Test with one user.
- 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→ deactivateSCIM 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_expiryABAC 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 orgApproach 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 chainThis 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:
- User clicks "Sign in with Google".
- Redirect to Google with OAuth params.
- Google authenticates the user; redirects back with code.
- Your IdP exchanges code for Google profile.
- Your IdP creates / matches a local user account based on email.
- 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.
Magic Links and OTP
Passwordless alternatives:
| Method | UX | Security | Where |
|---|---|---|---|
| Magic link (email) | Click link → logged in | OK; vulnerable if email is compromised | Notion-style |
| OTP via email | Code → enter | Slightly better UX; same vulnerability | Common |
| OTP via SMS | Code → enter | SIM swap attack | Avoid for high-value accounts |
| Authenticator app TOTP | Code from app | Strong | Common for MFA |
| Passkeys | Biometric | Strongest | New 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:
| Policy | Effect |
|---|---|
| MFA on every login | Strict; 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:
| Strategy | Notes |
|---|---|
| Short access token expiry (5-15 min) | Stolen token has narrow window |
| Refresh token rotation | Detect theft via reuse |
| Sliding session in your app | Track "last activity"; reject if too old |
| Token revocation list (Redis) | Hybrid stateless + revocable |
| Opaque tokens with introspection | Real-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.