Auth

Authentication and authorization protocols, identity federation, and access control

01

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
Key distinction

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.

02

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 TypeUse CaseSecurity
Authorization CodeServer-side web apps, SPAs (with PKCE), mobile appsRecommended
Client CredentialsMachine-to-machine / service-to-service communicationRecommended
Device CodeInput-constrained devices (smart TVs, CLI tools)Standard
Refresh TokenObtain new access tokens without re-authenticationStandard
Resource Owner PasswordLegacy direct username/password exchange (no redirect)Deprecated
ImplicitLegacy 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

User Client (App) Auth Server Resource Server | | | | |--1. Login---->| | | | |--2. /authorize-->| | |<--3. Login page-----------------| | |--4. Credentials---------------->| | | |<-5. code+state--| | | |--6. POST /token-| | | | (code+secret) | | | |<-7. access_token| | | | + refresh_token | | |--8. GET /api (Bearer token)--------->| | |<-9. Protected resource---------------| |<--10. Data----| | |
Recommendation

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.

03

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

ScopeClaims Returned
openidRequired. Indicates an OIDC request. Returns sub (subject identifier).
profilename, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
emailemail, email_verified
addressaddress (structured JSON object)
phonephone_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.

Key concept

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.

04

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

BindingHow it worksUse case
HTTP-RedirectSAML message is encoded (base64 + deflate) in the URL query stringAuthnRequest from SP to IdP (small messages)
HTTP-POSTSAML message is base64-encoded in a hidden HTML form field, submitted via auto-POSTSAML Response from IdP to SP (large messages with signatures)
ArtifactA reference (artifact) is sent; the receiver fetches the actual message via a back-channel SOAP callWhen messages are too large or must not pass through the browser

SP-initiated SSO flow

User Service Provider (SP) Identity Provider (IdP) | | | |--1. Visit app---->| | | |--2. Generate AuthnRequest-->| |<--3. Redirect to IdP (HTTP-Redirect binding)----| | | |--4. Enter credentials------------------------->| | | |<--5. SAML Response (HTTP-POST binding)----------| | (auto-submit form to SP ACS URL) | | | | | |<--6. Validate signature, | | | check conditions, | | | extract attributes | | | | | |--7. Create session---------->| |<--8. Logged in----| |
Critical

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.

05

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

AspectOIDCSAML 2.0
Token formatJWT (compact JSON)XML assertion
TransportREST / JSON over HTTPSXML / SOAP over HTTP-POST/Redirect
Discovery.well-known/openid-configurationSAML metadata XML
Mobile-friendlyYes — JSON is native to mobile/SPANo — XML parsing is heavy, browser redirects required
API accessBuilt-in (access tokens for APIs)Not designed for APIs (assertions are for SSO)
ComplexityLower — simpler spec, easier librariesHigher — XML signing, canonicalization, bindings
Enterprise adoptionGrowing rapidly, dominant for new appsDeeply entrenched in enterprise SSO
Spec maturity2014 (younger)2005 (mature, stable)
Session managementFront-channel/back-channel logout specsSingle 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
Practical advice

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.

06

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

TermDescriptionExample
DNDistinguished Name — unique path to an entrycn=Jane Doe,ou=People,dc=example,dc=com
OUOrganizational Unit — container for grouping entriesou=People, ou=Groups
CNCommon Name — typically the entry's display namecn=Jane Doe, cn=engineering
DCDomain Component — parts of the domain namedc=example,dc=com
uidUser ID — unique login identifieruid=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.

Warning

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 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

User KDC (AS) KDC (TGS) Service | | | | |--1. AS-REQ------->| | | | (username, | | | | timestamp | | | | encrypted w/ | | | | user's key) | | | | | | | |<--2. AS-REP-------| | | | (TGT encrypted | | | | w/ KDC key, | | | | session key | | | | encrypted w/ | | | | user's key) | | | | | | | |--3. TGS-REQ------------------------------>| | | (TGT + SPN + | | | | authenticator) | | | | | | | |<--4. TGS-REP------------------------------| | | (service ticket | | | | encrypted w/ | | | | service key) | | | | | | | |--5. AP-REQ------------------------------------------------------->| | (service ticket | | | | + authenticator)| | | | | | | |<--6. AP-REP-------------------------------------------------------| | (mutual auth | | | | optional) | | |

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
Active Directory

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).

08

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

AlgorithmTypeUse case
RS256Asymmetric (RSA + SHA-256)Recommended. IdP signs with private key, anyone verifies with public key (via JWKS). Standard for OIDC.
ES256Asymmetric (ECDSA + SHA-256)Recommended. Smaller keys and signatures than RSA. Increasingly preferred.
HS256Symmetric (HMAC + SHA-256)Both parties share the same secret. Only use for internal services where the signer and verifier are the same entity.
noneNo signatureNever use. Unsigned tokens. Libraries must reject alg: none.

Standard claims

ClaimNameDescription
issIssuerWho issued the token (URL of the auth server)
subSubjectWho the token is about (user ID)
audAudienceWho the token is intended for (API URL or client ID)
expExpirationUnix timestamp after which the token is invalid
iatIssued AtUnix timestamp when the token was created
nbfNot BeforeUnix timestamp before which the token is not valid
jtiJWT IDUnique identifier for the token (for revocation/replay prevention)

Token validation steps

  1. Parse the JWT and extract the header
  2. Verify alg is an expected algorithm (reject none, reject unexpected algorithms)
  3. Fetch the signing key from the JWKS endpoint using the kid from the header
  4. Verify the cryptographic signature
  5. Check exp — reject expired tokens
  6. Check nbf — reject tokens not yet valid
  7. Check iss — must match expected issuer
  8. Check aud — must include your API/client ID
  9. 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.

09

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

FactorCategoryExamples
Something you knowKnowledgePassword, PIN, security questions
Something you havePossessionPhone (SMS/push), hardware key (YubiKey), TOTP app
Something you areInherenceFingerprint, 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
Recommendation

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.

10

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

Issuance Validation Refresh Revocation | | | | v v v v Auth Server Resource Server Auth Server Auth Server issues tokens validates on issues new invalidates after authn every request access token tokens on using refresh logout/ token compromise Timeline: |--access token (5-15 min)--| |--new access token--| |--------refresh token (hours to days)------------| |--revoked--|

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

StorageSecurityUse Case
httpOnly cookieRecommended. 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)
localStorageAvoid. Accessible to any JS on the page. XSS = full token theft.Never for sensitive tokens
sessionStorageCaution. 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
Warning

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.

11

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

AttributePurposeRecommendation
SecureOnly sent over HTTPSAlways set
HttpOnlyNot accessible via JavaScriptAlways set for auth cookies
SameSite=LaxNot sent on cross-site POST requestsDefault — prevents CSRF for most cases
SameSite=StrictNever sent on cross-site requestsStrictest, but breaks inbound links from other sites
__Host- prefixRequires 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;
    }
  }
}
Critical

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."

12

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. Reject alg: none. Fetch signing keys from the JWKS endpoint. Cache keys but handle rotation.
  • Store tokens securelyhttpOnly + Secure + SameSite cookies for web apps. Never in localStorage. In-memory for SPAs with silent refresh.
  • Include state and nonce — random state for CSRF protection in every OAuth flow. Random nonce in 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. Secure flag 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).