TS-55: Authentication and authorization

This technical standard covers the architecture of authentication and authorization: the choice between session- and token-based authentication, OAuth 2.0 and OpenID Connect, single sign-on and federation, multi-factor authentication, authorization models (RBAC, ABAC, ReBAC), and service-to-service authentication.

It does not cover the credential and secrets-handling rules that apply regardless of architecture — password strength, hashing, re-authentication, and least-privilege access verification — which are set out in TS-52: Security and secrets management. Nor does it cover JWT schema design, which is TS-56: JSON Web Tokens (JWTs). For how an HTTP API transmits credentials on the wire, see TS-21: HTTP APIs.

Authentication models

Authentication answers "who is this?". Two models dominate, and the choice between them shapes the rest of a system’s security architecture.

Session-based authentication

In a session-based model, a server verifies credentials once, then creates a session record — stored server-side, in memory or a shared store such as Redis — and hands the client an opaque session identifier, typically in a cookie. Every subsequent request carries the identifier; the server looks up the session to establish identity.

Session-based authentication SHOULD be preferred for traditional server-rendered web applications, where the browser and server share an origin and cookies are the natural transport.

Its defining trade-off is statefulness. Revocation is immediate — deleting the session record ends the session everywhere — but the server MUST maintain session state, which complicates horizontal scaling: every server instance needs access to the shared session store, or requests from a given client MUST be routed consistently to the instance holding their session.

Token-based authentication

In a token-based model, a server verifies credentials once, then issues a signed token — typically a JWT (see TS-56: JSON Web Tokens (JWTs) for token design) — that itself carries the claims needed to establish identity and permissions. The server does not need to persist anything: validation is a signature check, not a lookup.

Token-based authentication SHOULD be preferred for HTTP APIs, single-page applications, mobile clients, and service-to-service calls, where clients are decoupled from the server and may not share an origin or support cookies.

Its defining trade-off is the inverse of the session model. Statelessness removes the shared-store bottleneck and simplifies horizontal scaling, but revocation is hard: a signed token remains valid until it expires, whatever happens server-side, unless the system also maintains a denylist or a short-enough expiry that compromise has a bounded blast radius. See Token lifetime and revocation.

Choosing between them

Session-based

Token-based

State

Server-side (session store)

Client-side (self-contained token)

Revocation

Immediate

Delayed, bounded by expiry (or denylist)

Scaling

Requires a shared session store

Stateless; scales horizontally without coordination

Cross-origin clients

Awkward (cookie scoping, CORS credentials)

Natural fit

Typical clients

Server-rendered web apps

HTTP APIs, SPAs, mobile apps, services

A system MAY combine both: a session cookie for the primary web application, and issued tokens for its API. Do not mix the two for the same client and purpose — pick one model per client-server relationship, and be deliberate about the trade-off.

Token lifetime and revocation

Where a token-based model is used, access tokens SHOULD be short-lived — typically minutes, not hours — to bound the damage of a leaked token. A longer-lived refresh token, exchanged for new access tokens without requiring re-authentication, offsets the resulting UX cost. See TS-56: JSON Web Tokens (JWTs) for refresh-token design.

Where immediate revocation is a hard requirement — for example, disabling a compromised account — a token-based system MUST maintain a denylist (or an equivalent short-lived allowlist check) for the affected token or subject, rather than relying on expiry alone.

OAuth 2.0 and OpenID Connect

OAuth 2.0 is an authorization framework: it lets a client obtain limited access to a resource on a user’s behalf, without the user sharing their credentials with the client. OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0: it adds a standardized identity token (the id_token, a JWT) so a client can also learn who the user is.

The two solve different problems and are commonly deployed together. A system that only needs to know "is this user who they say they are" needs OIDC. A system that needs to act on a user’s behalf against a third-party API — read their calendar, post to their account — needs OAuth 2.0’s authorization grant, whether or not OIDC is also in play.

Roles

  • Resource owner. The user who owns the data or account being accessed.
  • Client. The application requesting access, eg. a web or mobile app.
  • Authorization server. Issues tokens after authenticating the resource owner and obtaining their consent.
  • Resource server. The API that holds the protected data and validates tokens presented to it.

A system MAY combine the authorization server and resource server roles, but SHOULD keep them logically separate, so that token issuance and validation can evolve independently of the APIs that rely on them.

Grant types

Grant

Use when

Notes

Authorization code (+ PKCE)

A user is present in a browser or mobile app

The RECOMMENDED default for any client that can redirect a user through a login screen. PKCE (Proof Key for Code Exchange) MUST be used for public clients (SPAs, mobile apps, CLIs) that cannot hold a client secret, and SHOULD be used even for confidential clients as defense in depth.

Client credentials

Service-to-service, no user involved

The client authenticates as itself, using its own credentials, and receives a token scoped to its own identity. See [Service-to-service authentication].

Refresh token

Extending a session without re-prompting the user

Exchanges a previously-issued refresh token for a new access token. See Token lifetime and revocation in Authentication models.

The implicit grant and the resource owner password credentials grant are both deprecated by the OAuth 2.0 Security Best Current Practice (BCP) and MUST NOT be used in new systems. The implicit grant returns tokens directly in a URL fragment, exposing them to referrer leakage and browser history; the password grant requires the client to handle the user’s raw credentials directly, defeating the purpose of delegated authorization. Authorization code with PKCE covers every use case either was intended for.

Scopes

A scope is a named unit of access a client can request, eg. read:profile or write:orders. An authorization server MUST issue a token scoped only to what the client requested and the user consented to — never a superset "just in case".

A client SHOULD request the narrowest set of scopes that its current operation requires, and request additional scopes incrementally as needed, rather than requesting broad access up front. This follows the same least-privilege principle set out in TS-52: Security and secrets management.

OpenID Connect

OIDC adds three things on top of an OAuth 2.0 authorization code flow:

  • An id_token — a signed JWT asserting the authenticated user’s identity, with standard claims (sub, iss, aud, exp — see TS-56: JSON Web Tokens (JWTs)).
  • A /userinfo endpoint, for retrieving additional profile claims about the authenticated user.
  • Standardized discovery (/.well-known/openid-configuration), so a client can locate an identity provider’s endpoints and public keys without out-of-band configuration.

A system that needs to authenticate a user — establish who they are, as opposed to what they can access — SHOULD use OIDC rather than inventing a bespoke identity layer on raw OAuth 2.0.

Third-party identity providers

Delegating authentication to a managed identity provider (eg. Auth0, Okta, AWS Cognito, or a social provider such as Google or GitHub) is RECOMMENDED over operating a first-party identity system, unless identity is core to the product’s value proposition. A managed provider removes the burden of securely storing credentials, keeping up with evolving MFA and passkey support, and maintaining OIDC/SAML conformance — see Single sign-on and federation.

Single sign-on and federation

Single sign-on (SSO) lets a user authenticate once with an identity provider (IdP) and gain access to multiple, independently-operated applications without re-entering credentials for each. Federation is the underlying mechanism: a trust relationship between an IdP and a service provider (SP), established so that the SP accepts assertions from the IdP instead of authenticating users itself.

When to support SSO

Business and enterprise software SHOULD support SSO against the customer’s own identity provider (eg. Okta, Azure AD, Google Workspace). Enterprise buyers commonly require it, both for user convenience and because it lets their own IT department control onboarding, offboarding, and MFA policy centrally, rather than per application.

Consumer-facing applications SHOULD offer "social login" against a small number of major providers (eg. Google, Apple) as a convenience, in addition to — not instead of — a first-party or delegated identity system. A user MUST always retain a way to recover access to their account that does not depend on a third party’s continued cooperation.

Protocols

Two protocols account for the great majority of SSO deployments:

  • SAML 2.0. XML-based. Still the dominant protocol in large enterprises and legacy identity infrastructure. An IdP issues a signed XML assertion, typically delivered via an HTTP POST binding through the user’s browser.
  • OpenID Connect. JSON/JWT-based, built on OAuth 2.0 — see OAuth 2.0 and OpenID Connect. The RECOMMENDED default for new integrations: simpler to implement correctly, smaller payloads, and native fit for mobile and single-page applications, which SAML was not designed for.

A system SHOULD support OIDC as its primary federation protocol and add SAML support only when a specific enterprise customer requires it. Supporting both means the application’s identity layer MUST treat "which protocol" as a per-connection configuration detail, not a fork in the codebase.

Just-in-time provisioning

An application integrating SSO SHOULD provision user accounts just-in-time, on a user’s first successful federated login, rather than requiring accounts to be pre-created out of band. The federated assertion (SAML) or ID token (OIDC) carries enough identity information — typically an email address and display name — to create the local account record automatically.

Role and permission assignment on first login SHOULD be driven by group or role claims asserted by the IdP where available, falling back to a configurable default role otherwise. See Authorization models.

Deprovisioning

Removing a user’s access at the IdP MUST take effect promptly for every downstream application, either by:

  • validating the federated session or token against the IdP frequently enough that a revoked account is locked out within an acceptable window, or
  • the IdP actively pushing deprovisioning events to downstream applications, eg. via SCIM (System for Cross-domain Identity Management).

An application MUST NOT rely solely on a long-lived local session that outlives the user’s standing with the IdP — that defeats the point of centralizing access control.

Multi-factor authentication

Multi-factor authentication (MFA) requires a user to prove their identity with two or more independent factors, drawn from separate categories:

  • Something you know — a password or PIN.
  • Something you have — a phone, hardware security key, or authenticator app.
  • Something you are — a biometric, eg. fingerprint or face recognition.

Two factors from the same category (eg. a password and a security question, both "something you know") do not constitute MFA — they are both vulnerable to the same class of attack (guessing, phishing, data breach).

When MFA is required

MFA MUST be available for every user account, and MUST be required — not merely offered — for accounts with elevated privileges, such as administrators and anyone with production access.

MFA SHOULD be required for all user accounts, opt-out rather than opt-in, except where a specific product context makes that friction unacceptable at signup. Where MFA is optional at signup, the application SHOULD prompt users to enable it, and MAY require it retroactively for accounts that hold sensitive data or elevated permissions.

Factor methods, in order of preference

  1. Passkeys (WebAuthn/FIDO2). Public-key credentials bound to a device or platform authenticator (eg. a phone’s biometric sensor, a hardware security key). The RECOMMENDED default: phishing-resistant, because the credential is cryptographically bound to the origin it was registered against, and requires no shared secret that can be intercepted or database that can be breached for its value.
  2. TOTP (Time-based One-Time Password). A six-to-eight digit code generated by an authenticator app from a shared secret, refreshed every 30 seconds (RFC 6238). Widely supported and does not depend on network connectivity at verification time. RECOMMENDED as the default second factor where passkeys are not yet supported by a client.
  3. SMS or voice one-time codes. MAY be offered for accessibility or as a fallback recovery method, but MUST NOT be the only MFA method available, and SHOULD NOT be the default. SMS is vulnerable to SIM-swapping and number-porting attacks, and the OAuth 2.0 Security BCP explicitly discourages relying on it as a sole factor.

Push notifications to a trusted device (approve/deny prompts) MAY be offered as a convenience layer on top of TOTP or passkeys, but MUST require the user to confirm a matching code or context (a technique known as "number matching"), to resist MFA-fatigue attacks where an attacker spams approval requests hoping for an accidental tap.

Recovery

An account MUST have a documented recovery path for a user who loses access to their primary MFA factor — eg. single-use backup codes, generated at enrollment and shown to the user exactly once, or a manual identity verification process for support-assisted recovery.

Recovery codes MUST be treated as a credential: stored hashed, not in plain text, and each code MUST be invalidated after a single use. A recovery process MUST NOT weaken the security the primary factor was providing — for example, a "call support" fallback that only verifies information available in a data breach re-opens the exact attack MFA was added to close.

Re-authentication for sensitive operations

Consistent with TS-52: Security and secrets management's requirement that destructive or sensitive operations require re-authentication, an application SHOULD prompt for a fresh MFA challenge — not merely a valid session — before allowing changes to account security settings, such as disabling MFA itself or changing the registered recovery methods.

Authorization models

Authorization answers "what is this identity allowed to do?" — a distinct question from authentication, and one that MUST be addressed with its own explicit model rather than left implicit in application code.

Role-based access control (RBAC)

RBAC grants permissions to roles (eg. admin, editor, viewer), and assigns roles to identities. It is the RECOMMENDED default model: it maps naturally onto how most organizations already think about access ("she’s an admin"), and keeps the permission set small and auditable, since permissions are defined once per role rather than once per user.

RBAC’s weakness is granularity: it answers "can a viewer read any document" more naturally than "can this user read this specific document". Systems that need per-resource or contextual decisions SHOULD layer a finer-grained model on top of RBAC, rather than abandoning RBAC entirely — most authorization decisions in most systems really are role-level, and only a minority need finer control.

Attribute-based access control (ABAC)

ABAC evaluates a policy against attributes of the subject (who), the resource (what), the action (verb), and the environment (eg. time of day, network location) to reach an allow/deny decision. It is the RECOMMENDED model where an access decision genuinely depends on more than role — for example, "a user MAY approve an expense report only if they are not its submitter and the amount is within their approval limit."

ABAC’s expressiveness comes at a cost: policies are harder to audit at a glance than a role list, and a poorly-organized policy set can become as opaque as the ad-hoc if statements it was meant to replace. Policies SHOULD be centralized in a dedicated policy engine (eg. Open Policy Agent) rather than scattered across application code, so they remain a single reviewable artifact.

Relationship-based access control (ReBAC)

ReBAC derives access from relationships in a graph — eg. "a user can edit a document if they are a member of the team that owns the folder containing it." It is the natural fit for systems with nested ownership, sharing, or collaboration structures (project management tools, file storage, social graphs), where RBAC’s flat role list and ABAC’s attribute rules both struggle to express "access flows through this chain of relationships."

Google’s Zanzibar system, and its open-source descendants (eg. OpenFGA, SpiceDB), are the reference implementations of this model at scale.

Choosing a model

Model

Fits when

RBAC

Permissions map cleanly to a small set of organizational roles; the default starting point for most systems.

ABAC

A decision depends on contextual attributes beyond role — ownership, amount, time, location.

ReBAC

Access is inherited through a graph of relationships — team membership, folder hierarchy, sharing.

These models are not mutually exclusive. A system commonly uses RBAC for coarse-grained, application-wide permissions (who can access the admin panel) and ABAC or ReBAC for fine-grained, per-resource decisions (who can edit this specific record) within it.

Enforcement

Whichever model is used, the requirements set out in TS-52: Security and secrets management apply without exception: authorization decisions MUST be enforced server-side, MUST be checked before every operation, and MUST NOT be inferred from the absence of a client-side affordance.

An authorization check SHOULD be implemented as a single, centralized decision point — a middleware, policy engine, or shared library — that every code path invokes, rather than re-implemented ad hoc at each call site. This is what makes the permission model auditable, and prevents the common defect class where one forgotten check leaves a single endpoint unprotected while the rest of the system enforces the rule correctly.

Service-to-service authentication

Authenticating a machine client — a backend service, a scheduled job, a CI pipeline — differs from authenticating a human: there is no user to redirect through a login screen, and the credential’s lifecycle needs to be managed without a human present to type a password or approve an MFA prompt.

API keys

A static, long-lived API key is the simplest mechanism, and MAY be used for low-sensitivity integrations or where the calling system cannot support anything more sophisticated (eg. a legacy third-party integration). See TS-21: HTTP APIs for how an API key is transmitted on an HTTP request, and TS-52: Security and secrets management for the storage, scoping, and rotation requirements that apply to any secret, including an API key.

API keys' weakness is exactly their simplicity: a static credential that does not expire is a standing liability if it leaks, and there is no cryptographic proof that the caller is who it claims to be beyond possession of the string.

OAuth 2.0 client credentials grant

Where the calling system supports it, the OAuth 2.0 client credentials grant (see OAuth 2.0 and OpenID Connect) is RECOMMENDED over a static API key. The client authenticates with its own credential to obtain a short-lived access token, which is what is actually presented on each call. This bounds the exposure of a leaked token to its expiry window, and centralizes issuance and revocation in the authorization server rather than requiring each integration point to check a key against its own store.

Mutual TLS (mTLS)

Mutual TLS has each side of a connection present a certificate, so that both the client and the server authenticate each other as part of the TLS handshake itself, before any application-layer credential is exchanged.

mTLS is RECOMMENDED for service-to-service traffic within a trusted network boundary — for example, between microservices in the same cluster — where a service mesh (eg. Istio, Linkerd) can issue and rotate short-lived certificates automatically. It is a poor fit for public-facing APIs, where managing client certificate distribution for external, unmanaged callers is impractical.

Workload identity

Where services run on a cloud platform, workload identity (eg. AWS IAM roles for service accounts, GCP Workload Identity, Azure Managed Identity) SHOULD be preferred over any manually-distributed credential. The platform itself attests to a workload’s identity — based on what is running, not a secret it holds — and issues short-lived, automatically-rotated credentials scoped to that identity’s permissions. This removes an entire class of risk: there is no long-lived secret to leak, because none is ever created. See TS-52: Security and secrets management for the general secrets-management requirements this satisfies by construction.


References