Authentication, Authorization, OIDC, JWT, Refresh Tokens & SSO — Complete Reference
"Authentication always comes first. You can't authorize someone whose identity you haven't verified. For example, a user authenticates via SSO + MFA, then authorization determines whether they can view Project A's data or only Project B's — that's RBAC combined with data-level access control."
This is the most secure and common flow for web applications:
Authorization framework. Grants third-party apps limited access to user resources WITHOUT sharing passwords.
Identity layer on OAuth 2.0. Returns an ID Token (JWT) with user identity. Modern standard for web & mobile.
XML-based, enterprise-focused SSO protocol. Older but still dominant in corporate environments. Heavy payload.
Directory protocol for looking up user/group info. Often the backend that SAML/OIDC authenticate against.
openid (required for OIDC), profile, email, custom scopessub (user ID), name, email, rolesAlgorithm (RS256, HS256) and token type (JWT). Tells the server how to verify the signature.
User data: sub (user ID), name, email, roles, exp (expiry), iss (issuer). NOT encrypted — only Base64 encoded!
HMAC or RSA signature of Header + Payload. Proves the token hasn't been tampered with. Server verifies using secret/public key.
"JWTs are ideal for microservices architectures because they're stateless — each service can independently verify the token without calling a central session store. But the trade-off is revocation: if a user's access should be revoked immediately (like a terminated employee), you need either very short token lifetimes or a token blocklist. As a PM, I'd push for short-lived access tokens (5-15 min) with refresh token rotation for any security-sensitive application."
| Purpose | Authorize API requests |
| Lifetime | 5–15 minutes (short!) |
| Storage | In-memory (browser) or HTTP-only cookie |
| Format | Usually JWT (self-contained) |
| Sent via | Authorization: Bearer <token> header |
| If stolen | Limited damage due to short expiry |
| Purpose | Get new access tokens silently |
| Lifetime | Hours to days (7–30 days common) |
| Storage | HTTP-only secure cookie or server-side |
| Format | Opaque string (not JWT typically) |
| Sent to | Token endpoint only — never to APIs |
| If stolen | Major risk — use rotation + detection |
With rotation, every time a refresh token is used, it's invalidated and a new one is issued. If an attacker steals and uses the old refresh token, the server detects the reuse (since it was already consumed) and invalidates the entire token family — forcing re-authentication.
This is mandatory in security-sensitive applications (finance, healthcare, enterprise SaaS) because it provides stolen token detection — a critical security control for compliance.
Trusts the IdP
Trusts the IdP
Auth patterns vary by architecture. Here's how these concepts apply in practice:
Bearer header.aud claim matches your API.Authentication (AuthN) verifies who you are — it's the process of proving your identity. Examples: entering a password, scanning a fingerprint, or completing MFA.
Authorization (AuthZ) determines what you can do — it's the process of checking permissions after identity is confirmed. Examples: RBAC (role-based access control), OAuth scopes, API permissions.
Key point: Authentication always happens first. You must establish identity before the system can check permissions. A common analogy: authentication is showing your ID at the door; authorization is the access badge that lets you into specific rooms.
OAuth 2.0 is an authorization framework. It allows a third-party app to access a user's resources without knowing their password. Example: "Allow this app to access your Google Drive files." OAuth issues an Access Token but does NOT tell the app who the user is.
OIDC (OpenID Connect) is an identity layer built on top of OAuth 2.0. It adds an ID Token (a JWT) that contains user identity information (name, email, roles). So you get both: who the user is (AuthN) AND scoped access (AuthZ).
Common mistake in interviews: Saying "OAuth handles login." OAuth is NOT an authentication protocol — it's authorization only. OIDC is what handles login by extending OAuth with identity.
A JWT has three Base64-encoded parts separated by dots: Header.Payload.Signature
Header: Contains the algorithm (RS256, HS256) and token type. Tells the verifier how to check the signature.
Payload: Contains claims — key-value data like sub (user ID), exp (expiry), iss (issuer), aud (audience), roles. This is NOT encrypted — anyone can decode it. Never put secrets here.
Signature: Cryptographic hash of Header + Payload using a secret key (HS256) or private key (RS256). Proves the token hasn't been tampered with.
Validation steps: (1) Decode the header to find the algorithm. (2) Verify the signature using the IdP's public key (for RS256). (3) Check claims: exp not expired? iss from trusted issuer? aud matches our API? If any check fails → reject with 401.
Important: JWTs are signed, not encrypted. The signature proves integrity and authenticity, not confidentiality. If you need encryption, use JWE (JSON Web Encryption).
PKCE (Proof Key for Code Exchange) is a security extension for the OAuth Authorization Code flow. It prevents authorization code interception attacks.
The problem: In public clients (SPAs, mobile apps), there's no client secret — the code lives in the browser or on the device. If an attacker intercepts the authorization code (via a malicious app or redirect), they could exchange it for tokens.
How PKCE solves this: (1) The app generates a random code_verifier (kept secret, in memory). (2) It computes code_challenge = SHA256(code_verifier) and sends the challenge with the auth request. (3) When exchanging the code for tokens, the app sends the original code_verifier. (4) The IdP computes SHA256 of the verifier and checks it matches the original challenge. If it doesn't match → the exchange is rejected.
Key insight: Even if an attacker steals the authorization code, they can't exchange it because they don't have the code_verifier that was never transmitted over the network — only its hash was. PKCE is now recommended for ALL OAuth clients, not just public ones.
ID Token: Issued by OIDC. Contains user identity claims (who you are): sub, name, email, roles. It's always a JWT. The client app consumes it to know who logged in. Never send it to an API — it's meant for the client only.
Access Token: Issued by OAuth/OIDC. Contains authorization claims (what you can do): scope, aud, exp. Used to call APIs via Authorization: Bearer <token>. Can be JWT or opaque. Sent to APIs, never consumed by the client for identity.
A common interview trap: "Can you use an Access Token to get user info?" Technically the token might contain user claims, but the correct answer is: the ID Token is for identity, the Access Token is for API access. If you need user info at the API, the API reads claims from the Access Token or calls the UserInfo endpoint — it should never receive the ID Token.
localStorage is accessible to any JavaScript running on the page. If your app has an XSS vulnerability (Cross-Site Scripting) — even in a third-party library — an attacker's script can read the JWT from localStorage and send it to their server. Game over: they now have your access token.
Better alternatives:
(1) In-memory storage: Store tokens in a JavaScript variable. They're cleared on page refresh, but that's actually a security feature — combine with silent refresh or refresh tokens for persistence.
(2) HTTP-only, Secure, SameSite cookies: The browser automatically sends them with requests, and JavaScript cannot access them — immune to XSS. Set Secure (HTTPS only), HttpOnly (no JS access), and SameSite=Strict (CSRF protection).
Note: sessionStorage has the same XSS vulnerability as localStorage — it's not a safe alternative.
With refresh token rotation, every time a refresh token is used, the server invalidates it and issues a new refresh token along with the new access token. The old refresh token can never be used again.
Why it matters — theft detection: Suppose an attacker steals a refresh token. Two scenarios:
(1) Attacker uses it first: They get new tokens. When the legitimate user tries to use the (now-invalidated) original refresh token, the server detects reuse of a consumed token → revokes the entire token family (all tokens in that chain). Attacker is also kicked out.
(2) Legitimate user uses it first: The token is rotated. When the attacker tries to use the stolen (old) token → reuse detected → entire family revoked.
Either way, compromise is detected. Without rotation, a stolen refresh token is a permanent backdoor — the attacker can silently generate new access tokens indefinitely.
This is why modern security best practices (and the OAuth 2.0 Security Best Current Practice RFC) recommend rotation for all refresh tokens, especially for public clients.
HS256 (HMAC-SHA256): Symmetric algorithm. A single shared secret is used to both sign and verify the token. Fast, simple, but the secret must be shared with every service that needs to verify tokens.
RS256 (RSA-SHA256): Asymmetric algorithm. A private key signs the token (only the IdP has this). A public key verifies it (anyone can have this). The public key is published at the IdP's /.well-known/jwks.json endpoint.
When to use HS256: Single-service architectures where the same server issues and verifies tokens. Simple setup, no key distribution needed.
When to use RS256: Distributed systems, microservices, any architecture where multiple services need to verify tokens. Since only the IdP holds the private key, no other service can forge tokens — even if they have the public key. This is the standard for production systems.
Security risk of HS256 in distributed systems: If you share the HS256 secret with 10 microservices, any one of them can forge tokens for any user. With RS256, only the IdP can sign — the services can only verify.
This is one of JWT's fundamental trade-offs. Since JWTs are stateless — the server doesn't track issued tokens — there's no built-in revocation mechanism. A valid, unexpired JWT will be accepted until exp passes.
Strategies for revocation:
(1) Short-lived access tokens (recommended): Set expiry to 5-15 minutes. Even if stolen, the window of exploitation is small. Pair with refresh tokens for session longevity.
(2) Token blocklist/denylist: Maintain a server-side list of revoked token IDs (jti claim). Check every incoming token against this list. Downside: you've reintroduced server-side state, which defeats the stateless advantage. Use a fast store like Redis to minimize latency.
(3) Token versioning: Store a "token version" per user in your database. Embed the version in the JWT. On revocation, increment the version. Tokens with the old version are rejected. Only requires one DB lookup per user, not per token.
(4) Revoke the refresh token: You can't recall an access token in the wild, but you can revoke its refresh token so no new access tokens are issued. Combined with short access token lifetimes, the old token expires naturally within minutes.
The practical answer: Most production systems combine strategy 1 + 4: short access tokens + refresh token revocation. The blocklist approach (strategy 2) is used only for critical scenarios like immediate employee termination.
SSO allows users to authenticate once with a central Identity Provider and then access multiple applications without logging in again. Under the hood, it uses protocols like SAML 2.0 or OIDC.
Benefits: Fewer passwords to remember (reduces password fatigue and reuse), centralized access control (disable one account → revoke access everywhere), single place to enforce MFA, comprehensive audit trail of who accessed what, and reduced help desk tickets for password resets.
Trade-offs:
(1) Single point of failure: If the IdP goes down, no one can log into any app. Mitigation: high-availability IdP deployment.
(2) Increased blast radius: One compromised account gives access to everything. Mitigation: mandatory MFA, anomaly detection, step-up auth for sensitive operations.
(3) Session management complexity: How long should the SSO session last? Too long = security risk. Too short = users re-authenticate constantly. Most systems use sliding sessions with idle timeout.
CSRF (Cross-Site Request Forgery) tricks a user's browser into making an unintended request to a site where they're already authenticated. Example: a malicious site includes an image tag that triggers a bank transfer on a site where you're logged in.
In the OAuth/OIDC context, a CSRF attack could trick the app into exchanging a malicious authorization code — the attacker initiates an OAuth flow and somehow gets the victim's app to complete it, linking the attacker's account.
Protection mechanisms:
(1) State parameter: The app generates a random state value, includes it in the auth request, and verifies it matches when the callback returns. An attacker can't predict or forge this value.
(2) PKCE: Also prevents this — the code_verifier is bound to the session that started the flow.
(3) SameSite cookies: Set SameSite=Strict or Lax on auth cookies so they're not sent with cross-origin requests.
SAML 2.0: XML-based, enterprise SSO protocol from 2005. Uses XML assertions signed with X.509 certificates. Communicated via browser redirects and POST bindings. Heavyweight payload. Mature, battle-tested in enterprise environments.
OIDC: JSON-based, modern identity protocol built on OAuth 2.0 (2014). Uses JWTs. Lightweight, designed for web, mobile, and APIs. Easier to implement and debug.
Choose SAML when: Integrating with legacy enterprise systems that only support SAML (many corporate IdPs like legacy Active Directory). Federated identity across organizations (B2B). Compliance requirements that mandate SAML.
Choose OIDC when: Building modern web/mobile apps. Need API authentication (SAML doesn't work well with APIs). Want simpler implementation. Working with cloud-native services. Need a lighter payload.
In practice: Most modern IdPs (Azure AD, Okta, Auth0) support both. New applications almost always use OIDC. SAML remains dominant in enterprise SSO federations where switching would be costly. Some organizations use both — SAML for legacy apps, OIDC for new ones — with the same IdP.
roles: ["admin"] but the user was demoted 5 minutes ago. What happens and how do you handle it?The user retains admin access until the JWT expires. This is the fundamental trade-off of stateless tokens — the JWT was signed with roles: ["admin"] at issuance time, and no one can change it after the fact without invalidating the signature.
Mitigation strategies (from least to most complex):
(1) Short token lifetimes: If access tokens expire in 5 minutes, the stale role persists for at most 5 minutes. This is usually acceptable.
(2) Force refresh: When a role changes, invalidate the user's refresh token. On the next refresh cycle (within minutes), the new token will have the updated role. The old access token expires naturally.
(3) Hybrid check: For sensitive operations (delete, admin actions), the API does a real-time permission check against the database, ignoring the JWT's role claim. Regular read operations still use the JWT's cached role for performance.
(4) Token blocklist: Add the old token's jti to a Redis blocklist. All services check this on every request. Most invasive but provides immediate revocation.
The "right" answer: It depends on your security requirements. For most systems, short TTL + forced refresh (1+2) is the sweet spot. For admin/delete operations, add the hybrid check (3) as a safety net.
Zero Trust is a security model based on the principle: "never trust, always verify." Unlike traditional security (trust everything inside the network perimeter), Zero Trust assumes every request could be malicious — regardless of whether it comes from inside or outside the network.
How auth supports Zero Trust:
(1) Every request is authenticated: No implicit trust based on network location. Even internal microservice-to-microservice calls must carry valid tokens (mTLS or OAuth client credentials).
(2) Every request is authorized: Fine-grained, context-aware access control. Not just "is this a valid user?" but "should this user access this resource, from this device, at this time, from this location?"
(3) Least privilege: Tokens carry only the minimum scopes needed. A service that only needs to read data should never receive a token with write permissions.
(4) Continuous verification: Don't just authenticate once — re-verify throughout the session. Short-lived tokens force periodic re-validation. Step-up auth for sensitive actions.
Practical examples: Google's BeyondCorp is a Zero Trust implementation. Azure AD Conditional Access evaluates device compliance, location, and risk level on every sign-in — even for users inside the corporate network.
When there's no user involved (e.g., Service A calls Service B on a schedule), you need machine-to-machine authentication. The main approaches:
(1) OAuth 2.0 Client Credentials flow: Each service has its own client_id and client_secret (or certificate). It authenticates directly with the IdP and receives an access token scoped to what that service needs. The receiving service validates the JWT as normal. This is the most common approach in cloud environments.
(2) Mutual TLS (mTLS): Both sides present TLS certificates. The connection itself is authenticated — you know which service is calling at the transport layer. Often used in service meshes (Istio, Linkerd). Can be combined with JWT for authorization claims.
(3) API Keys: Simplest but weakest. A shared secret sent as a header. No expiry, no scoping, difficult to rotate. Acceptable only for low-risk internal services or as a supplementary check alongside other mechanisms.
Best practices: Use client credentials flow with short-lived JWTs. Each service gets its own identity (not a shared account). Apply least-privilege scoping — Service A's token only grants access to the specific endpoints it needs on Service B. Rotate credentials regularly. Log all service-to-service calls for audit trails.