Auth
Authentication and authorization protocols, identity federation, and access control
Overview
Authentication (AuthN) answers "who are you?" — verifying a user's or service's identity. Authorization (AuthZ) answers "what are you allowed to do?" — determining what resources and actions the authenticated identity can access. These two concerns are distinct but tightly coupled in every production system.
Concept Identity Providers (IdP)
An Identity Provider is the authoritative source for user identities. It authenticates users and issues tokens or assertions that other systems trust. Examples: Okta, Microsoft Entra ID (formerly Azure AD), Google Workspace, Keycloak, Auth0. The IdP maintains the user directory, credentials, and MFA policies.
Concept Service Providers (SP)
A Service Provider is any application that relies on an IdP for authentication. Instead of managing its own user database and login flow, the SP redirects users to the IdP and trusts the tokens or assertions it receives back. Also called relying parties (RP) in OIDC terminology.
Pattern Federation
Identity federation allows users to authenticate once with their IdP and access multiple SPs without re-entering credentials. This is the foundation of Single Sign-On (SSO). Federation works across organizational boundaries — a partner company's IdP can grant access to your SP if trust is established.
Pattern Centralized Auth
Centralizing authentication in an IdP eliminates password sprawl, enables consistent MFA enforcement, provides a single audit trail for all access, and simplifies offboarding (disable one account, revoke access everywhere). Every production system should use centralized auth.
Protocols covered
This guide covers the major authentication and authorization protocols you will encounter in production systems:
- OAuth 2.0 — the authorization framework for delegated access (scopes, tokens, grants)
- OpenID Connect (OIDC) — the identity layer on top of OAuth 2.0 (ID tokens, user info, discovery)
- SAML 2.0 — XML-based SSO protocol widely used in enterprise environments
- LDAP — directory protocol for user lookups and bind-based authentication
- Kerberos — ticket-based authentication protocol, the backbone of Active Directory auth
OAuth 2.0 is not an authentication protocol. It is an authorization framework. OIDC adds the authentication layer on top of OAuth 2.0. Many security vulnerabilities arise from treating OAuth 2.0 access tokens as proof of identity — they are not. Use OIDC ID tokens for authentication, OAuth 2.0 access tokens for authorization.
OAuth 2.0
OAuth 2.0 (RFC 6749) is an authorization framework that enables applications to obtain limited access to user accounts on third-party services. Instead of sharing passwords, users grant applications specific scopes of access. The application receives an access token that represents these permissions.
Roles
- Resource Owner — the user who owns the data and grants access
- Client — the application requesting access (web app, mobile app, CLI tool)
- Authorization Server — issues tokens after authenticating the resource owner (e.g., Okta, Auth0, Microsoft Entra ID, Keycloak)
- Resource Server — hosts the protected resources; validates access tokens (e.g., your API)
Grant types
| Grant Type | Use Case | Security |
|---|---|---|
| Authorization Code | Server-side web apps, SPAs (with PKCE), mobile apps | Recommended |
| Client Credentials | Machine-to-machine / service-to-service communication | Recommended |
| Device Code | Input-constrained devices (smart TVs, CLI tools) | Standard |
| Refresh Token | Obtain new access tokens without re-authentication | Standard |
| Resource Owner Password | Legacy direct username/password exchange (no redirect) | Deprecated |
| Implicit | Legacy SPAs (no backend) | Deprecated |
Access tokens vs refresh tokens
Token Access Token
Short-lived (minutes to hours). Sent with every API request in the Authorization: Bearer header. Can be opaque strings or JWTs. If compromised, the damage window is limited by the short expiry.
Token Refresh Token
Long-lived (days to months). Used only to obtain new access tokens from the authorization server. Must be stored securely (never in localStorage). Should be rotated on use (one-time use refresh tokens) and bound to the client.
Scopes
Scopes limit the access granted to a token. The client requests specific scopes, the user consents, and the issued token only grants those permissions.
# Authorization request with scopes
GET /authorize?
response_type=code&
client_id=myapp&
redirect_uri=https://myapp.com/callback&
scope=read:users write:posts&
state=xyzabc123
Token introspection & revocation
# Token introspection (RFC 7662) - check if token is active
curl -X POST https://auth.example.com/oauth/introspect \
-d "token=eyJhbGciOiJSUzI1NiIs..." \
-d "client_id=myapp" \
-d "client_secret=secret123"
# Response
# { "active": true, "scope": "read:users", "sub": "user123", "exp": 1711000000 }
# Token revocation (RFC 7009)
curl -X POST https://auth.example.com/oauth/revoke \
-d "token=eyJhbGciOiJSUzI1NiIs..." \
-d "token_type_hint=access_token" \
-d "client_id=myapp" \
-d "client_secret=secret123"
Authorization code flow
Always use the Authorization Code flow with PKCE (RFC 7636) for public clients (SPAs, mobile apps, CLIs). PKCE prevents authorization code interception attacks without requiring a client secret. The implicit grant is deprecated — do not use it for new applications.
OpenID Connect
OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. While OAuth 2.0 only handles authorization (what can this token access?), OIDC adds authentication (who is this user?). OIDC introduces the ID token — a JWT that contains claims about the authenticated user.
ID tokens
The ID token is a signed JWT issued by the authorization server after successful authentication. It is intended for the client application, not for API access.
// Decoded ID token payload
{
"iss": "https://auth.example.com",
"sub": "user-12345",
"aud": "myapp-client-id",
"exp": 1711003600,
"iat": 1711000000,
"nonce": "n-0S6_WzA2Mj",
"auth_time": 1710999900,
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true,
"picture": "https://cdn.example.com/jane.jpg"
}
UserInfo endpoint
The UserInfo endpoint returns claims about the authenticated user. It requires an access token (not the ID token) and provides additional profile information beyond what's in the ID token.
# Fetch user profile from the UserInfo endpoint
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
https://auth.example.com/userinfo
# Response
# {
# "sub": "user-12345",
# "name": "Jane Doe",
# "email": "jane@example.com",
# "email_verified": true,
# "groups": ["engineering", "platform-team"]
# }
Discovery
OIDC providers publish their configuration at a well-known URL. Clients use this to auto-configure endpoints, supported scopes, signing algorithms, and more.
# Fetch OIDC discovery document
curl https://auth.example.com/.well-known/openid-configuration
# Key fields in the response:
# {
# "issuer": "https://auth.example.com",
# "authorization_endpoint": "https://auth.example.com/authorize",
# "token_endpoint": "https://auth.example.com/oauth/token",
# "userinfo_endpoint": "https://auth.example.com/userinfo",
# "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
# "scopes_supported": ["openid", "profile", "email", "groups"],
# "response_types_supported": ["code", "id_token", "token id_token"],
# "id_token_signing_alg_values_supported": ["RS256"]
# }
Standard scopes
| Scope | Claims Returned |
|---|---|
openid | Required. Indicates an OIDC request. Returns sub (subject identifier). |
profile | name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at |
email | email, email_verified |
address | address (structured JSON object) |
phone | phone_number, phone_number_verified |
How OIDC adds authentication to OAuth
OAuth only No identity
A plain OAuth 2.0 access token tells the resource server what the bearer can do, not who the bearer is. You cannot reliably extract user identity from an access token — it may be opaque, it may be a JWT with implementation-specific claims, and it is meant for the resource server, not the client.
OIDC Identity layer
OIDC adds the openid scope, the ID token (a standardized JWT with user claims), the UserInfo endpoint, and the discovery document. The client now has a standardized, verifiable way to know who the user is. The ID token is signed by the IdP and validated by the client using the JWKS endpoint.
The ID token is for the client application (to know who logged in). The access token is for the resource server (to authorize API calls). Never send the ID token as a bearer token to APIs. Never use the access token to determine user identity in the client.
SAML
Security Assertion Markup Language 2.0 (SAML) is an XML-based framework for exchanging authentication and authorization data between an Identity Provider (IdP) and a Service Provider (SP). SAML is the dominant SSO protocol in enterprise environments, particularly for web-based applications.
SP-initiated vs IdP-initiated flows
Recommended SP-Initiated SSO
The user starts at the SP (e.g., visits app.example.com). The SP generates a SAML AuthnRequest and redirects the user to the IdP. After authentication, the IdP posts a SAML Response back to the SP's ACS (Assertion Consumer Service) URL. This is the recommended flow because the SP controls the request and can include anti-forgery protections.
Caution IdP-Initiated SSO
The user starts at the IdP portal (e.g., Okta dashboard) and clicks on an app. The IdP generates a SAML Response without a corresponding AuthnRequest and posts it to the SP. This flow is more vulnerable to replay attacks because there is no request to correlate with the response. Some SPs don't support it.
SAML assertions
A SAML assertion is an XML document containing statements about a subject (user). There are three types of statements:
- Authentication statement — confirms the user was authenticated by the IdP at a specific time using a specific method
- Attribute statement — provides user attributes (email, name, groups, roles) to the SP
- Authorization decision statement — indicates whether the user is authorized to access a resource (rarely used in practice)
<!-- Simplified SAML Assertion -->
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_abc123" IssueInstant="2026-03-20T10:00:00Z" Version="2.0">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature>...</ds:Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
jane@example.com
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
Recipient="https://app.example.com/saml/acs"
NotOnOrAfter="2026-03-20T10:05:00Z"
InResponseTo="_request456" />
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2026-03-20T09:59:00Z" NotOnOrAfter="2026-03-20T10:05:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://app.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-03-20T10:00:00Z">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="email">
<saml:AttributeValue>jane@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="groups">
<saml:AttributeValue>engineering</saml:AttributeValue>
<saml:AttributeValue>platform-team</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
XML signing & metadata
SAML responses and assertions are signed using XML Digital Signatures (XMLDSig). The SP validates the signature using the IdP's public certificate, which is exchanged via SAML metadata. Both the IdP and SP publish metadata XML files containing endpoints, certificates, and supported bindings.
# Download IdP metadata
curl -o idp-metadata.xml https://idp.example.com/saml/metadata
# Download SP metadata (your app)
curl -o sp-metadata.xml https://app.example.com/saml/metadata
Bindings
| Binding | How it works | Use case |
|---|---|---|
| HTTP-Redirect | SAML message is encoded (base64 + deflate) in the URL query string | AuthnRequest from SP to IdP (small messages) |
| HTTP-POST | SAML message is base64-encoded in a hidden HTML form field, submitted via auto-POST | SAML Response from IdP to SP (large messages with signatures) |
| Artifact | A reference (artifact) is sent; the receiver fetches the actual message via a back-channel SOAP call | When messages are too large or must not pass through the browser |
SP-initiated SSO flow
Always validate the entire SAML response: check the XML signature (on both the response and the assertion), verify the Destination, InResponseTo, Recipient, Audience, and time conditions (NotBefore/NotOnOrAfter). SAML signature wrapping attacks exploit SPs that validate the signature but extract attributes from unsigned portions of the XML.
OIDC vs SAML
Both OIDC and SAML achieve single sign-on, but they differ significantly in design philosophy, transport, and ideal use cases. Understanding when to use each is critical for making the right architectural decision.
Comparison
| Aspect | OIDC | SAML 2.0 |
|---|---|---|
| Token format | JWT (compact JSON) | XML assertion |
| Transport | REST / JSON over HTTPS | XML / SOAP over HTTP-POST/Redirect |
| Discovery | .well-known/openid-configuration | SAML metadata XML |
| Mobile-friendly | Yes — JSON is native to mobile/SPA | No — XML parsing is heavy, browser redirects required |
| API access | Built-in (access tokens for APIs) | Not designed for APIs (assertions are for SSO) |
| Complexity | Lower — simpler spec, easier libraries | Higher — XML signing, canonicalization, bindings |
| Enterprise adoption | Growing rapidly, dominant for new apps | Deeply entrenched in enterprise SSO |
| Spec maturity | 2014 (younger) | 2005 (mature, stable) |
| Session management | Front-channel/back-channel logout specs | Single Logout (SLO) with SOAP/Redirect binding |
When to use which
Use OIDC Modern applications
- Single-page applications (SPAs)
- Mobile and native applications
- Microservices and API-first architectures
- New greenfield projects
- Consumer-facing applications
- When you need both SSO and API authorization
Use SAML Enterprise/legacy
- Enterprise SSO with existing SAML infrastructure
- Legacy applications that only support SAML
- Government and regulated industries requiring SAML
- B2B federation with partners using SAML IdPs
- When the IdP only supports SAML (note: ADFS 3.0+ supports both SAML and OIDC)
- Desktop web applications without API needs
Most modern IdPs (Okta, Microsoft Entra ID, Auth0, Keycloak) support both OIDC and SAML. If you control both the IdP and SP, choose OIDC. If you're integrating with a third-party enterprise IdP, support both and let the customer choose. Many production systems run OIDC and SAML simultaneously behind a single auth gateway.
LDAP & Directory Services
LDAP (Lightweight Directory Access Protocol) is a protocol for accessing and maintaining distributed directory information services. It organizes data in a hierarchical tree structure and is the foundation of Active Directory, OpenLDAP, and other directory services used for user authentication and group-based authorization.
Directory structure
LDAP directories use a tree structure called the Directory Information Tree (DIT). Each entry is identified by a Distinguished Name (DN).
dc=example,dc=com (domain root)
|
+-- ou=People (organizational unit)
| |
| +-- cn=Jane Doe (common name)
| | uid=jdoe
| | mail=jane@example.com
| | memberOf=cn=engineering,ou=Groups,dc=example,dc=com
| |
| +-- cn=John Smith
| uid=jsmith
| mail=john@example.com
|
+-- ou=Groups
| |
| +-- cn=engineering
| | member=cn=Jane Doe,ou=People,dc=example,dc=com
| |
| +-- cn=admins
| member=cn=John Smith,ou=People,dc=example,dc=com
|
+-- ou=ServiceAccounts
|
+-- cn=app-reader
Key terminology
| Term | Description | Example |
|---|---|---|
DN | Distinguished Name — unique path to an entry | cn=Jane Doe,ou=People,dc=example,dc=com |
OU | Organizational Unit — container for grouping entries | ou=People, ou=Groups |
CN | Common Name — typically the entry's display name | cn=Jane Doe, cn=engineering |
DC | Domain Component — parts of the domain name | dc=example,dc=com |
uid | User ID — unique login identifier | uid=jdoe |
Bind operations (authentication)
LDAP authentication works via bind operations. The client sends a DN and password, and the LDAP server verifies the credentials.
# Simple bind (authenticate as a user)
ldapsearch -x -H ldap://ldap.example.com \
-D "cn=Jane Doe,ou=People,dc=example,dc=com" \
-W \
-b "dc=example,dc=com" \
"(uid=jdoe)"
# Search for a user by email
ldapsearch -x -H ldap://ldap.example.com \
-D "cn=app-reader,ou=ServiceAccounts,dc=example,dc=com" \
-w 'service-password' \
-b "ou=People,dc=example,dc=com" \
"(mail=jane@example.com)" cn uid mail memberOf
# Search for all members of a group
ldapsearch -x -H ldap://ldap.example.com \
-D "cn=app-reader,ou=ServiceAccounts,dc=example,dc=com" \
-w 'service-password' \
-b "ou=Groups,dc=example,dc=com" \
"(cn=engineering)" member
Search filters
# Exact match
(uid=jdoe)
# Wildcard
(cn=Jane*)
# AND - users in engineering who are active
(&(memberOf=cn=engineering,ou=Groups,dc=example,dc=com)(accountStatus=active))
# OR - match by email or uid
(|(uid=jdoe)(mail=jane@example.com))
# NOT - all users except service accounts
(!(ou=ServiceAccounts))
# Combined - active users in engineering or admins group
(&(objectClass=person)(|(memberOf=cn=engineering,ou=Groups,dc=example,dc=com)(memberOf=cn=admins,ou=Groups,dc=example,dc=com))(accountStatus=active))
LDAPS and StartTLS
Encryption LDAPS (port 636)
LDAP over TLS. The entire connection is encrypted from the start, similar to HTTPS. Use ldaps:// scheme. This is the recommended approach — simple, always encrypted.
Encryption StartTLS (port 389)
Upgrades a plain LDAP connection to TLS mid-session. The connection starts unencrypted on port 389, then the client sends a StartTLS extended operation. Vulnerable to downgrade attacks if not enforced. Prefer LDAPS.
Never bind over plain LDAP (port 389 without StartTLS). Credentials are sent in cleartext. In production, always use LDAPS (port 636) or enforce StartTLS. Active Directory's default LDAP port is unencrypted — configure it for LDAPS or enforce channel binding.
Kerberos
Kerberos is a ticket-based network authentication protocol that uses symmetric-key cryptography and a trusted third party (the Key Distribution Center, or KDC) to authenticate users and services. It is the default authentication protocol in Active Directory (on-premises) and Microsoft Entra ID (cloud, for Kerberos-based Windows sign-in) environments and is widely used in enterprise networks.
Key components
KDC Key Distribution Center
The trusted third party that issues tickets. In Active Directory, the domain controller is the KDC. It consists of two services: the Authentication Service (AS) and the Ticket Granting Service (TGS).
TGT Ticket Granting Ticket
Issued by the AS after initial authentication. The TGT proves the user has already authenticated and is used to request service tickets without re-entering credentials. TGTs have a configurable lifetime (default 10 hours in AD).
SPN Service Principal Name
A unique identifier for a service instance. Format: service/hostname@REALM (e.g., HTTP/web.example.com@EXAMPLE.COM). SPNs are registered in the KDC and used to locate the correct encryption key for service tickets.
Keytab Keytab Files
A file containing pairs of Kerberos principals and their encryption keys. Services use keytab files to authenticate without human interaction. Treat keytab files like private keys — restrict file permissions to the service account only.
The authentication flow
Common Kerberos commands
# Request a TGT (kinit)
kinit jdoe@EXAMPLE.COM
# List cached tickets
klist
# Request a service ticket manually
kvno HTTP/web.example.com@EXAMPLE.COM
# Destroy all cached tickets (logout)
kdestroy
# Create a keytab file for a service
ktutil
addent -password -p HTTP/web.example.com@EXAMPLE.COM -k 1 -e aes256-cts
wkt /etc/krb5.keytab
quit
# Verify a keytab works
kinit -kt /etc/krb5.keytab HTTP/web.example.com@EXAMPLE.COM
Kerberos configuration
# /etc/krb5.conf
[libdefaults]
default_realm = EXAMPLE.COM
dns_lookup_realm = false
dns_lookup_kdc = true
ticket_lifetime = 10h
renew_lifetime = 7d
forwardable = true
[realms]
EXAMPLE.COM = {
kdc = dc1.example.com
kdc = dc2.example.com
admin_server = dc1.example.com
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
In AD environments, Kerberos is used for domain authentication, while NTLM serves as a fallback. Modern AD security best practices recommend disabling NTLM where possible and relying solely on Kerberos. Kerberos requires DNS to be working correctly — SPN resolution depends on DNS. Clock skew between clients and the KDC must be under 5 minutes (default tolerance).
JWT
JSON Web Tokens (RFC 7519) are compact, URL-safe tokens used to represent claims between two parties. JWTs are the token format used by OIDC ID tokens, and are commonly used for OAuth 2.0 access tokens as well. A JWT has three base64url-encoded parts separated by dots: header.payload.signature.
Structure
# JWT structure
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9. <-- Header (base64url)
eyJzdWIiOiJ1c2VyLTEyMzQ1IiwiaXNzIjoiaHR0. <-- Payload (base64url)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c <-- Signature
// Header
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-2024-03"
}
// Payload (claims)
{
"iss": "https://auth.example.com",
"sub": "user-12345",
"aud": "https://api.example.com",
"exp": 1711003600,
"iat": 1711000000,
"nbf": 1711000000,
"jti": "unique-token-id-abc",
"scope": "read:users write:posts",
"roles": ["admin", "editor"]
}
Signing algorithms
| Algorithm | Type | Use case |
|---|---|---|
RS256 | Asymmetric (RSA + SHA-256) | Recommended. IdP signs with private key, anyone verifies with public key (via JWKS). Standard for OIDC. |
ES256 | Asymmetric (ECDSA + SHA-256) | Recommended. Smaller keys and signatures than RSA. Increasingly preferred. |
HS256 | Symmetric (HMAC + SHA-256) | Both parties share the same secret. Only use for internal services where the signer and verifier are the same entity. |
none | No signature | Never use. Unsigned tokens. Libraries must reject alg: none. |
Standard claims
| Claim | Name | Description |
|---|---|---|
iss | Issuer | Who issued the token (URL of the auth server) |
sub | Subject | Who the token is about (user ID) |
aud | Audience | Who the token is intended for (API URL or client ID) |
exp | Expiration | Unix timestamp after which the token is invalid |
iat | Issued At | Unix timestamp when the token was created |
nbf | Not Before | Unix timestamp before which the token is not valid |
jti | JWT ID | Unique identifier for the token (for revocation/replay prevention) |
Token validation steps
- Parse the JWT and extract the header
- Verify
algis an expected algorithm (rejectnone, reject unexpected algorithms) - Fetch the signing key from the JWKS endpoint using the
kidfrom the header - Verify the cryptographic signature
- Check
exp— reject expired tokens - Check
nbf— reject tokens not yet valid - Check
iss— must match expected issuer - Check
aud— must include your API/client ID - Extract claims and apply authorization logic
JWKS endpoint
# Fetch the JSON Web Key Set
curl https://auth.example.com/.well-known/jwks.json
// JWKS response
{
"keys": [
{
"kty": "RSA",
"kid": "key-id-2024-03",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps...",
"e": "AQAB"
}
]
}
Common pitfalls
Pitfall No revocation
JWTs are self-contained — once issued, they are valid until they expire. There is no built-in revocation mechanism. Mitigate by keeping access tokens short-lived (5-15 minutes) and using refresh token rotation. For immediate revocation, maintain a token blocklist or use token introspection.
Pitfall Algorithm confusion
An attacker changes the alg header from RS256 to HS256 and signs with the public key (which the server uses for RS256 verification). If the server naively switches to HS256 verification, the public key becomes the symmetric secret. Always validate that alg matches your expected algorithm.
Pitfall Token size
JWTs can grow large with many claims. They are sent with every HTTP request in the Authorization header. Large JWTs impact network performance and may exceed header size limits (many proxies cap at 8 KB). Keep custom claims minimal.
Pitfall Sensitive data in payload
JWT payloads are base64url-encoded, not encrypted. Anyone can decode the payload. Never include secrets, passwords, or PII that shouldn't be visible to the client. Use JWE (JSON Web Encryption) if the payload must be confidential.
MFA & Passwordless
Multi-factor authentication (MFA) requires users to provide two or more verification factors to gain access. This dramatically reduces the risk of account compromise — even if a password is stolen, the attacker still needs the second factor. Passwordless authentication eliminates passwords entirely, using cryptographic credentials instead.
Authentication factors
| Factor | Category | Examples |
|---|---|---|
| Something you know | Knowledge | Password, PIN, security questions |
| Something you have | Possession | Phone (SMS/push), hardware key (YubiKey), TOTP app |
| Something you are | Inherence | Fingerprint, face recognition, retina scan |
TOTP (Time-based One-Time Password)
TOTP (RFC 6238) generates a 6-8 digit code that changes every 30 seconds. Both the server and the authenticator app share a secret key. The code is derived from the secret + current time using HMAC-SHA1 by default, though the RFC also supports HMAC-SHA-256 and HMAC-SHA-512.
# Generate a TOTP secret (base32-encoded)
# Server stores this and shares it with the user via QR code
SECRET="JBSWY3DPEHPK3PXP"
# The otpauth:// URI for QR code generation
# otpauth://totp/Example:jane@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=6&period=30
# Python example of TOTP verification
python3 -c "
import hmac, hashlib, struct, time, base64
secret = base64.b32decode('JBSWY3DPEHPK3PXP')
counter = int(time.time()) // 30
msg = struct.pack('>Q', counter)
h = hmac.new(secret, msg, hashlib.sha1).digest()
offset = h[-1] & 0x0F
code = (struct.unpack('>I', h[offset:offset+4])[0] & 0x7FFFFFFF) % 1000000
print(f'Current TOTP: {code:06d}')
"
WebAuthn / FIDO2
WebAuthn (Web Authentication API) is a W3C standard that enables passwordless authentication using public-key cryptography. The user's device generates a key pair — the private key never leaves the device, and the public key is registered with the server.
Type Platform Authenticators
Built into the device: Touch ID, Face ID, Windows Hello, Android biometrics. Convenient for users but tied to a specific device. Lost device means lost credential (unless synced via passkeys).
Type Roaming Authenticators
External hardware: YubiKey, Titan Security Key. Work across multiple devices via USB, NFC, or Bluetooth. More secure (phishing-resistant), but users must carry the device. Recommended for high-security accounts.
Passkeys
Passkeys are synced WebAuthn credentials stored in the platform's credential manager (iCloud Keychain, Google Password Manager, Microsoft Password Manager, 1Password). They combine the security of public-key cryptography with the convenience of cross-device availability. When a user creates a passkey on their iPhone, it syncs to their Mac, iPad, and other Apple devices automatically. Microsoft Edge and Windows also support passkey syncing via Microsoft accounts.
Recovery codes
Recovery codes are single-use backup codes generated when MFA is set up. Store them securely (password manager, printed in a safe). Best practices:
- Generate 8-10 codes, each 8+ characters
- Store hashed on the server (like passwords)
- Invalidate each code after use
- Allow regeneration (which invalidates all previous codes)
- Require identity verification before displaying recovery codes
Risk-based authentication
Rather than requiring MFA for every login, risk-based (adaptive) authentication evaluates contextual signals and only steps up when risk is elevated:
- Known device + known location — low risk, skip MFA
- New device + known location — medium risk, require MFA
- New device + new location — high risk, require stronger MFA
- Impossible travel — login from two distant locations within minutes, block and notify
Prioritize phishing-resistant MFA (WebAuthn/FIDO2, passkeys) over SMS or TOTP. SMS is vulnerable to SIM swapping. TOTP is vulnerable to real-time phishing proxies. WebAuthn cryptographically binds the authentication to the origin, making phishing impossible. For the transition period, TOTP is still far better than no MFA.
Token Management
The token lifecycle — issuance, validation, refresh, and revocation — is critical to the security of any auth system. Poor token management is one of the most common sources of authentication vulnerabilities.
Token lifecycle
Short-lived access tokens + long-lived refresh tokens
The standard pattern: access tokens expire quickly (5-15 minutes) so that if stolen, the attack window is small. Refresh tokens last longer (hours to days) but are stored securely and used only with the authorization server, never sent to resource servers.
// Token response from authorization server
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "v1.MRjE4ZDI1NzQtMDRh...",
"scope": "openid profile read:data"
}
# Refresh an access token
curl -X POST https://auth.example.com/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=v1.MRjE4ZDI1NzQtMDRh..." \
-d "client_id=myapp" \
-d "client_secret=secret123"
Token storage
| Storage | Security | Use Case |
|---|---|---|
httpOnly cookie | Recommended. Not accessible via JavaScript (XSS-safe). Add Secure, SameSite=Lax. | Server-rendered web apps, BFF pattern |
| In-memory (JS variable) | Good. Lost on page refresh. Not accessible to other tabs. XSS can still read it. | SPAs during a session (refresh via silent auth) |
localStorage | Avoid. Accessible to any JS on the page. XSS = full token theft. | Never for sensitive tokens |
sessionStorage | Caution. Per-tab only, but still XSS-vulnerable. | Short-lived data, not for auth tokens |
PKCE (Proof Key for Code Exchange)
PKCE (RFC 7636) protects the authorization code flow for public clients (SPAs, mobile apps) that cannot securely store a client secret. It prevents authorization code interception by binding the code to the client that requested it.
// PKCE flow implementation
// 1. Generate code_verifier (random string, 43-128 chars)
const codeVerifier = generateRandomString(64);
// 2. Generate code_challenge (SHA-256 hash of verifier, base64url-encoded)
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = base64urlEncode(digest);
// 3. Authorization request includes challenge
// GET /authorize?
// response_type=code&
// client_id=myapp&
// redirect_uri=https://myapp.com/callback&
// code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
// code_challenge_method=S256&
// scope=openid profile
// 4. Token request includes verifier
// POST /token
// grant_type=authorization_code&
// code=SplxlOBeZQQYbYS6WxSbIA&
// redirect_uri=https://myapp.com/callback&
// client_id=myapp&
// code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Token binding
Token binding ties tokens to the TLS connection or client certificate, preventing stolen tokens from being used on different connections. Standards include DPoP (Demonstration of Proof-of-Possession, RFC 9449) and mTLS-bound tokens (RFC 8705).
# DPoP (Demonstration of Proof-of-Possession)
# Client generates a key pair and includes a DPoP proof JWT with each request
curl -H "Authorization: DPoP eyJhbGciOiJSUzI1NiIs..." \
-H "DPoP: eyJhbGciOiJSUzI1NiIsInR5cCI6ImRwb3Arand..." \
https://api.example.com/resource
Implement refresh token rotation: each time a refresh token is used, the server issues a new refresh token and invalidates the old one. If an attacker replays a used refresh token, the server detects the reuse, invalidates the entire token family, and forces re-authentication. This is critical for detecting token theft.
Security Best Practices
Authentication and authorization systems are high-value targets. A single vulnerability can compromise every user account. These practices address the most common attack vectors.
OAuth 2.0 / OIDC protections
CSRF State parameter
Always include a cryptographically random state parameter in authorization requests. Verify it matches on the callback. This prevents CSRF attacks where an attacker initiates an OAuth flow and tricks the victim into completing it, linking the attacker's account.
# Generate state
STATE=$(openssl rand -base64 32)
# Store in session, include in /authorize
# Verify on callback before exchanging code
Replay Nonce
Include a nonce in OIDC authentication requests. The IdP includes this nonce in the ID token. The client verifies the nonce matches what it sent, preventing token replay attacks where an attacker reuses a stolen ID token.
Intercept PKCE
Use PKCE for all authorization code flows, not just public clients. PKCE prevents authorization code interception attacks even for confidential clients. Many IdPs now require PKCE.
Open redirect Redirect URI validation
The authorization server must exactly match the redirect URI against the registered list. No wildcards, no partial matches, no open redirects. An attacker who can manipulate the redirect URI can steal authorization codes.
Session management
// Secure cookie configuration
app.use(session({
name: '__Host-session', // __Host- prefix: requires Secure, no Domain
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'lax', // CSRF protection
maxAge: 3600000, // 1 hour
path: '/'
}
}));
Secure cookie attributes
| Attribute | Purpose | Recommendation |
|---|---|---|
Secure | Only sent over HTTPS | Always set |
HttpOnly | Not accessible via JavaScript | Always set for auth cookies |
SameSite=Lax | Not sent on cross-site POST requests | Default — prevents CSRF for most cases |
SameSite=Strict | Never sent on cross-site requests | Strictest, but breaks inbound links from other sites |
__Host- prefix | Requires Secure, no Domain, path=/ | Recommended for session cookies |
Brute force & account protection
- Rate limiting — limit login attempts per IP and per account (e.g., 5 attempts per minute)
- Account lockout — temporarily lock accounts after repeated failures (e.g., 15-minute lockout after 10 failures). Use exponential backoff.
- CAPTCHA — require CAPTCHA after failed attempts (not on first attempt — it degrades UX)
- Credential stuffing protection — check passwords against breach databases (HaveIBeenPwned API)
- IP blocking — block IPs with excessive failed attempts across multiple accounts
# Rate limiting configuration example (nginx)
http {
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
server {
location /auth/login {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
proxy_pass http://auth-backend;
}
}
}
Never reveal whether a username exists in error messages. Use generic messages like "Invalid credentials" instead of "User not found" or "Wrong password." This prevents username enumeration attacks. Apply the same principle to password reset flows — always say "If an account exists, we sent a reset email."
Production Checklist
- Use a centralized IdP — Okta, Microsoft Entra ID, Keycloak, Auth0. Never build your own auth system from scratch. Centralize all authentication through a single IdP with SSO.
- Enforce MFA for all users — at minimum TOTP, preferably WebAuthn/FIDO2/passkeys. Require phishing-resistant MFA for admin and privileged accounts.
- Use OIDC for authentication, OAuth 2.0 for authorization — do not use OAuth access tokens as proof of identity. Use OIDC ID tokens for authentication, access tokens for API authorization.
- Authorization Code flow + PKCE everywhere — never use the implicit grant. Use PKCE for all clients (public and confidential). Client credentials grant for machine-to-machine only.
- Short-lived access tokens (5-15 min) — pair with refresh token rotation. Detect refresh token reuse and invalidate the entire token family.
- Validate JWTs completely — check signature,
alg,iss,aud,exp,nbf. Rejectalg: none. Fetch signing keys from the JWKS endpoint. Cache keys but handle rotation. - Store tokens securely —
httpOnly+Secure+SameSitecookies for web apps. Never inlocalStorage. In-memory for SPAs with silent refresh. - Include state and nonce — random
statefor CSRF protection in every OAuth flow. Randomnoncein every OIDC request. Verify both on callback. - Exact redirect URI matching — register all redirect URIs explicitly. No wildcards, no pattern matching. The authorization server must reject unrecognized redirect URIs.
- Encrypt all auth traffic — TLS 1.2+ everywhere (prefer TLS 1.3 for new deployments). LDAPS instead of plain LDAP. HSTS headers.
Secureflag on all cookies. - Implement rate limiting and account lockout — rate-limit login endpoints, use exponential backoff for lockouts, never reveal if a username exists.
- SAML: validate everything — check XML signatures on both response and assertion. Verify Destination, Audience, Recipient, InResponseTo, and time conditions. Defend against signature wrapping attacks.
- Audit all auth events — log every login, logout, MFA challenge, token issuance, and failed attempt. Include IP, user agent, and timestamp. Alert on anomalies (impossible travel, brute force).
- Plan for token revocation — implement a token blocklist or introspection endpoint for immediate access revocation. Short token lifetimes are your primary defense.
- Rotate secrets and certificates — rotate OIDC signing keys, SAML certificates, and client secrets on a schedule. Support key rollover (publish new key before retiring old one via JWKS).