Authentication and Authorization: A Comprehensive Guide

OAuth 2.0, JWT, PKCE, OIDC, SAML, SSO: the auth ecosystem is big enough to misremember. This compendium builds a clear mental model for each concept and explains how they fit together.

Authentication and Authorization: A Comprehensive Guide

If you have worked in software long enough, you have almost certainly used the words "authentication" and "authorization" interchangeably in conversation and got away with it, because everyone knew what you meant. But the moment you start designing a system that handles identity, you cannot afford the ambiguity anymore. The concepts are distinct, they compose in non-obvious ways, and the ecosystem of standards built on top of them (OAuth 2.0, JWT, OpenID Connect, PKCE, SAML, SSO) has grown to a size where it is genuinely easy to misremember which piece does what.

This article is a compendium. It will not teach you to integrate a specific identity provider from scratch. What it will do is give you a solid mental model for each concept, explain how they relate to each other, and serve as a reference you can return to whenever you need to refresh your memory.

Authentication vs Authorization

These two words share a prefix and get used in the same breath, but they describe fundamentally different things.

Authentication answers the question: who are you? It is the process of verifying that a user, service, or system is who it claims to be. Checking a username and password is authentication. So is verifying a biometric, a hardware token, or a cryptographic signature.

Authorization answers the question: what are you allowed to do? Given that we know who you are, which resources can you access and which operations can you perform? A user might be successfully authenticated but still not authorized to delete records they do not own.

The distinction matters because you can have one without the other. An API key grants authorization to call an endpoint without telling you anything about who the human behind it is. Conversely, you can authenticate a user perfectly and then have the authorization layer deny every action because their account lacks the required permissions.

In practice, most secure systems require both, in that order: authenticate first, then authorize based on the verified identity.

The Problem with Sessions

Traditional web applications solved authentication by maintaining server-side sessions. A user logs in, the server creates a session object, stores it (in memory, in a database, wherever), and hands back a session ID as a cookie. Every subsequent request sends that cookie, the server looks up the session, and the user's identity is restored.

This works, but it has a fundamental scalability problem: the server is stateful. Every machine in your fleet needs access to the same session store. You end up needing sticky sessions, a shared Redis cluster, or some other coordination mechanism. It also makes it hard to share identity across different domains.

The industry largely moved toward a different model for APIs: stateless tokens. Instead of storing state on the server, you encode it in a token and hand it to the client. The server trusts the token if it can verify it was issued by a trusted party. There is nothing to look up.

Bearer Tokens

A bearer token is a type of access credential where possession is sufficient for access. The name comes from the idea that whoever bears the token can use it. There is no additional proof of identity required beyond presenting the token.

When an API says it uses Bearer authentication, it means the client sends the token in the Authorization HTTP header:

Authorization: Bearer <token>

The server receives this header and validates the token. If it is valid, the request is authorized. If not, the server returns a 401.

Bearer tokens are simple and portable, which is why they are ubiquitous. The downside is that theft is easy: anyone who intercepts the token can use it. This is why bearer tokens should always be transmitted over TLS, kept short-lived, and stored securely.

The question of what a bearer token contains, or how the server validates it, is where things get more interesting. That is where JWTs come in.

JWT: JSON Web Tokens

A JSON Web Token (JWT) is a compact, self-contained format for transmitting claims between parties. The key property is that the token itself carries the information a server needs to validate it, with no database lookup required.

A JWT consists of three Base64URL-encoded parts separated by dots:

header.payload.signature

The header identifies the token type and the signing algorithm:

{
  "alg": "RS256",
  "typ": "JWT"
}

Common algorithms are HS256 (HMAC with SHA-256, symmetric) and RS256 (RSA with SHA-256, asymmetric). Asymmetric algorithms are preferred in distributed systems because the private key signs the token and the public key verifies it, so verification servers do not need access to the secret.

Payload

The payload contains the claims: statements about the subject and additional metadata.

{
  "sub": "user_123",
  "name": "Alberto De Bortoli",
  "email": "alberto@example.com",
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "iat": 1726394400,
  "exp": 1726398000
}

Standard registered claims include:

  • sub: subject, the entity the token is about
  • iss: issuer, who created the token
  • aud: audience, who the token is intended for
  • iat: issued at (Unix timestamp)
  • exp: expiration time (Unix timestamp)

Anything beyond these is a custom claim. You can put roles, scopes, tenant IDs, or any other identity-related data in the payload.

Signature

The signature is computed by taking the Base64URL-encoded header, a dot, the Base64URL-encoded payload, and signing the result with the specified algorithm and the issuer's secret or private key.

signature = RS256(base64(header) + "." + base64(payload), privateKey)

To validate a JWT, a server:

  1. Decodes the header and payload
  2. Re-computes the signature using the public key
  3. Compares the computed signature with the one in the token
  4. Checks the exp claim has not passed
  5. Checks the iss and aud claims match expectations

If all checks pass, the token is valid and the payload can be trusted.

One critical misconception to address: JWT payloads are not encrypted by default. The Base64URL encoding is trivially reversible. Anyone who obtains a JWT can read its contents. Never put sensitive secrets in a JWT payload unless you are using JWE (JSON Web Encryption). What the signature guarantees is integrity: that the payload has not been tampered with since it was issued.

Access Tokens vs ID Tokens

These two token types are often confused because they look the same (both are often JWTs) and arrive together in the same response. But they serve completely different purposes.

Access Token

An access token is a credential that grants access to a protected resource. It is what you include in the Authorization: Bearer header when calling an API. The API validates the token and uses its claims to decide what the caller is allowed to do.

An access token answers the question: what can this client do? It typically contains scopes (the set of permitted operations), the subject identifier, and an expiry. It does not need to contain rich user profile information, because APIs do not generally need to display user profiles. They need to know whether an operation is permitted.

ID Token

An ID token represents the result of an authentication event. It answers the question: who is the user? It contains profile claims like name, email address, and profile picture, which a client application uses to display to the end user.

ID tokens are issued by an identity provider and consumed by the client application. They are not meant to be sent to APIs as credentials. Sending an ID token to an API in the Authorization header is a mistake that is more common than it should be. The API is not the intended audience and should reject it.

The distinction to hold onto: access tokens are for APIs, ID tokens are for clients.

OAuth 2.0

OAuth 2.0 is an authorization framework. It defines a set of flows for how a client application can obtain authorization to access resources on behalf of a user, without the user having to share their credentials with the client.

The key insight of OAuth is the introduction of a trusted third party, the authorization server, that sits between the client and the resource owner. The user authenticates with the authorization server and the authorization server issues tokens that the client uses to access resources.

Core Roles

  • Resource Owner: the user who owns the data
  • Client: the application requesting access
  • Authorization Server: issues tokens after authenticating the user and obtaining consent
  • Resource Server: the API that holds the protected resources, validates tokens

Authorization Code Flow

The most common and most secure OAuth 2.0 flow for applications with a server-side component is the Authorization Code flow:

  1. The client redirects the user to the authorization server with a request that includes:
    • client_id
    • redirect_uri
    • response_type=code
    • scope (the permissions being requested)
    • state (a random value to prevent CSRF attacks)
  2. The authorization server authenticates the user (via login screen) and asks them to consent to the requested scopes.
  3. Upon consent, the authorization server redirects back to the client's redirect_uri with a short-lived authorization code in the query string.
  4. The client exchanges the authorization code for tokens by making a back-channel request to the authorization server's token endpoint, including:
    • The authorization code
    • The redirect_uri
    • The client_id and client_secret
  5. The authorization server validates the code and issues an access token (and optionally a refresh token and an ID token).

The separation into two steps is deliberate: getting a code first, then exchanging it for tokens. The code is short-lived, single-use, and transmitted through the browser (front channel). The actual tokens are exchanged server-to-server over a direct HTTPS connection (back channel), where the client authenticates with its client_secret. This means the tokens never touch the browser.

Client Credentials Flow

For machine-to-machine communication where there is no user involved, the Client Credentials flow is the right choice. A service authenticates directly with the authorization server using its client_id and client_secret to obtain an access token:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=service_a&client_secret=secret&scope=read:orders

This is what microservices use to authenticate against other internal services.

Scopes

Scopes are strings that represent a specific permission. They are declared by the resource server and requested by the client. Common examples:

openid profile email read:orders write:orders admin

The authorization server may grant all requested scopes, a subset, or none. The issued access token carries the granted scopes, and the resource server checks them before processing a request.

PKCE: Proof Key for Code Exchange

The Authorization Code flow relies on the client's ability to keep a client_secret private. This assumption holds for server-side applications. It breaks down completely for public clients: native mobile apps, single-page applications, or desktop apps, because there is no secure way to embed a secret in code that runs on an end user's device. The secret can always be extracted.

PKCE (RFC 7636, pronounced "pixie") was designed to solve this problem for public clients. It makes the Authorization Code flow secure without requiring a client_secret.

The Mechanism

Before initiating the flow, the client generates a cryptographically random string called the code verifier. It then computes a hash of it called the code challenge, typically using SHA-256:

code_challenge = BASE64URL(SHA256(code_verifier))

The code challenge is included in the initial authorization request:

?response_type=code
&client_id=app
&redirect_uri=...
&code_challenge=<hash>
&code_challenge_method=S256

The authorization server stores the code challenge alongside the issued authorization code. When the client exchanges the authorization code for tokens, it sends the original code_verifier (not the hash):

POST /token
grant_type=authorization_code&code=...&code_verifier=<original_string>&redirect_uri=...

The authorization server hashes the received code_verifier, compares it to the stored code_challenge, and proceeds only if they match.

Why This Helps

Without PKCE, an attacker who intercepts the authorization code (for example, through a malicious app registered with the same URL scheme on a mobile device) can immediately exchange it for tokens, because no client_secret is required for a public client.

With PKCE, the authorization code alone is worthless. The attacker does not know the code_verifier, which was never transmitted and exists only in memory on the legitimate client. When the legitimate client makes the token exchange, the binding is proven. The intercepted code cannot be used by anyone else.

PKCE was initially designed as an enhancement for mobile apps, but it is now recommended for all authorization code flows, including server-side applications. Many authorization servers require it unconditionally.

OpenID Connect (OIDC)

OAuth 2.0 is an authorization framework. It defines how to grant access to resources, but it says nothing about authentication or about establishing who the user is. The sub claim in an access token tells you the user's identifier, but the protocol does not standardize what authentication happened, when, or how strong it was.

OpenID Connect is a thin identity layer built on top of OAuth 2.0 that adds standardized authentication semantics. It is not a replacement for OAuth; it is an extension of it.

What OIDC Adds

OIDC defines:

  1. The openid scope: Including this scope in an OAuth 2.0 authorization request signals that you want an ID token in addition to an access token.
  2. The ID Token: A JWT containing standardized claims about the authentication event and the authenticated user.
  3. Standard claims: sub, name, email, picture, phone_number, birthdate, and others are defined in the specification, giving clients a consistent way to retrieve user profile information regardless of which provider issued the tokens.
  4. The UserInfo endpoint: An API endpoint that the client can call with an access token to retrieve additional profile claims not included in the ID token.
  5. Authentication context: The auth_time claim records when authentication occurred; amr (Authentication Methods References) records what methods were used (password, OTP, etc.); acr (Authentication Context Class Reference) encodes the assurance level.
  6. Discovery document: Identity providers expose a .well-known/openid-configuration endpoint that returns a JSON document describing all the provider's endpoints, supported algorithms, and capabilities. This allows clients to configure themselves automatically.

OIDC in Practice

When you click "Sign in with Google" or "Sign in with GitHub" on a website, you are going through an OIDC flow:

  1. The relying party (the website) redirects you to Google's authorization endpoint with scope=openid profile email.
  2. You authenticate with Google.
  3. Google issues an ID token and an access token.
  4. The relying party validates the ID token's signature using Google's public keys (fetched from the discovery document) and extracts your identity from the claims.
  5. The user is now authenticated and the session is established.

The access token from this flow is used by the relying party to call Google APIs on your behalf (e.g., fetching calendar events). The ID token is what the relying party uses to establish your identity locally.

Single Sign-On (SSO)

Single Sign-On is a user experience pattern, not a specific protocol. It describes a system where a user authenticates once and gains access to multiple applications without being asked to log in again.

The SSO experience is powered by a central identity provider (IdP) that maintains the user's authenticated session. When the user accesses an application (a service provider or relying party), the application checks whether the user has an active session at the IdP. If they do, the IdP issues tokens or assertions silently, and the user is logged in without re-entering credentials. If they do not, the user is redirected to the IdP's login page.

The practical benefits are significant: users maintain one set of credentials, IT teams manage identities centrally, and revoking access to all applications at once is as simple as disabling an account at the IdP.

SSO can be implemented using different protocols. The two dominant ones are OIDC (and OAuth 2.0) for modern web and mobile applications, and SAML for enterprise contexts.

SAML: Security Assertion Markup Language

SAML (Security Assertion Markup Language) is a mature XML-based standard for exchanging authentication and authorization data between an identity provider and a service provider. The current version, SAML 2.0, was published in 2005. It predates OAuth and OIDC by several years and remains deeply embedded in enterprise SSO deployments.

The SAML Flow

The most common SAML flow is the SP-initiated Web Browser SSO Profile:

  1. A user tries to access a resource at the service provider (SP).
  2. The SP generates a SAML authentication request (an XML document) and redirects the user to the identity provider (IdP), either via HTTP Redirect (URL-encoded) or HTTP POST.
  3. The IdP authenticates the user (via whatever mechanism it supports: password, MFA, smart card).
  4. The IdP generates a SAML assertion, an XML document containing authentication statements and attribute statements about the user, signs it with its private key, and sends it back to the SP via an HTTP POST to the SP's Assertion Consumer Service (ACS) URL.
  5. The SP validates the assertion signature using the IdP's public certificate, checks the assertions, and establishes a local session.

SAML Assertions

A SAML assertion is the SAML equivalent of a JWT ID token. It carries:

  • Authentication statements: when the user authenticated, and with what method
  • Attribute statements: user attributes like email, name, group membership, department
  • Authorization decision statements: whether the subject is permitted to access the resource (less commonly used)
<saml:Assertion>
  <saml:Issuer>https://idp.example.com</saml:Issuer>
  <saml:Subject>
    <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
      user@example.com
    </saml:NameID>
  </saml:Subject>
  <saml:Conditions NotBefore="..." NotOnOrAfter="...">
    <saml:AudienceRestriction>
      <saml:Audience>https://sp.example.com</saml:Audience>
    </saml:AudienceRestriction>
  </saml:Conditions>
  <saml:AttributeStatement>
    <saml:Attribute Name="email">
      <saml:AttributeValue>user@example.com</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="groups">
      <saml:AttributeValue>engineering</saml:AttributeValue>
    </saml:Attribute>
  </saml:AttributeStatement>
</saml:Assertion>

SAML vs OIDC

The question of which to use tends to answer itself based on context:

SAML 2.0

OpenID Connect

Format

XML

JSON / JWT

Transport

HTTP Redirect / POST

HTTP Redirect / POST / API

Age

2005

2014

Primary use case

Enterprise SSO (web)

Modern web and mobile

Mobile support

Poor (browser-based)

First-class

API access

Not designed for it

OAuth 2.0 underneath

Complexity

Higher

Lower

Enterprise adoption

Very high

Growing

SAML is entrenched in enterprise environments because it was the standard when SaaS applications started integrating with corporate identity stores (Active Directory, LDAP). Platforms like Salesforce, Workday, and countless others implemented SAML integrations years ago and have no reason to replace them.

OIDC is the right choice for greenfield work: new applications, mobile apps, developer-facing APIs. It is simpler, more flexible, and built on the same OAuth 2.0 foundation that handles API authorization.

Tokens, Lifetimes, and Refresh Tokens

Access tokens are intentionally short-lived. Typical lifetimes range from a few minutes to an hour. This limits the window of exposure if a token is compromised: once it expires, it is useless.

But forcing users to re-authenticate every hour is a terrible experience. Refresh tokens solve this. A refresh token is a long-lived credential that can be exchanged for a new access token without user interaction:

POST /token
grant_type=refresh_token&refresh_token=<token>&client_id=...&client_secret=...

The authorization server validates the refresh token, issues a new access token (and sometimes a new refresh token), and the cycle continues invisibly.

Refresh tokens are considerably more sensitive than access tokens. They last longer, and compromising one can give an attacker persistent access. They should be stored securely, in a server-side session or in secure storage on mobile, and never in localStorage on the web.

Some authorization servers implement refresh token rotation: every time a refresh token is used, it is invalidated and a new one is issued. If the authorization server detects an attempt to use an already-invalidated refresh token, it can assume the refresh token was stolen and revoke the entire token family, forcing re-authentication.

Putting It All Together

It helps to understand how these pieces connect in a concrete scenario. Consider an enterprise employee using a single sign-on portal to access a cloud application:

  1. The employee navigates to the cloud application (sp.example.com). They are not authenticated.
  2. The application checks its configuration. If it uses SAML, it sends a SAML AuthnRequest to the corporate IdP. If it uses OIDC, it initiates an OAuth 2.0 Authorization Code flow (with PKCE) against the IdP.
  3. The IdP presents a login screen. The employee enters their corporate credentials (and perhaps completes MFA). The IdP authenticates them.
  4. With SAML: the IdP sends a signed SAML assertion back to the application's Assertion Consumer Service (ACS) URL. With OIDC: the IdP redirects back with an authorization code, which the application exchanges for an access token and an ID token.
  5. The application establishes a local session. The employee is logged in.
  6. When the application needs to call a backend API, it sends the access token in the Authorization: Bearer header. The API validates the JWT signature against the IdP's public keys, checks the exp claim, verifies the aud claim matches the API, and confirms the token has the required scopes.
  7. When the access token expires, the application uses the refresh token to get a new one silently.
  8. The employee closes their browser. Later, an IT administrator disables their account in the corporate directory. The IdP marks their sessions as invalid. When the application eventually attempts a token refresh, it fails, and the employee is required to log in again.

The layering becomes clear: SAML or OIDC handles the authentication and SSO experience. OAuth 2.0 handles the authorization framework and token issuance. JWT provides the token format. Bearer tokens carry the credential in HTTP. PKCE ensures the Authorization Code flow is secure on public clients.

Common Mistakes Worth Knowing

Sending ID tokens to APIs. An ID token is issued for the client application, not for the resource server. Its aud claim will typically be the client's ID, not the API's identifier. A well-configured API will reject it, and rightly so.

Not validating token claims. Checking the signature is necessary but not sufficient. The exp, iss, and aud claims must be validated too. A token signed correctly by a different issuer for a different audience is not a valid credential for your API.

Using the Implicit flow. OAuth 2.0 originally included an Implicit flow that returned tokens directly in the URL fragment, bypassing the back-channel exchange. It was designed for SPAs before CORS was widely supported. It is now deprecated. Use the Authorization Code flow with PKCE instead.

Storing tokens in localStorage. Access tokens in localStorage are accessible to any JavaScript on the page, making them vulnerable to XSS attacks. For web applications, the recommended approach is to keep tokens in memory and use httpOnly cookies for refresh tokens.

Not using PKCE for public clients. Any native app or SPA using the Authorization Code flow without PKCE is vulnerable to authorization code interception attacks. Most modern authorization servers require PKCE; if yours does not, enable it.

Conflating OAuth 2.0 with authentication. OAuth 2.0 grants authorization to access resources. If you use an OAuth 2.0 access token to identify a user without an ID token and OIDC, you are making assumptions the spec does not guarantee. Use OIDC if you need authentication.

Conclusion

Authentication and authorization are not interchangeable terms, and the ecosystem of standards that implements them is not a single monolithic system. It is a layered stack where each piece has a specific job.

OAuth 2.0 defines how authorization flows work. OpenID Connect adds the authentication layer on top. JWT gives tokens a structured, self-verifiable format. PKCE hardens the code flow for clients that cannot keep secrets. Bearer tokens carry the credential in HTTP. SAML does much of the same job in enterprise contexts using a different, older technology. SSO is the experience that these protocols collectively enable.

Understanding where each concept lives in that stack, and what problem it was introduced to solve, makes the whole thing considerably less overwhelming. The next time you find yourself staring at an authorization server configuration or debugging a 401, you have the map.