SAML vs OAuth2 vs OIDC
If you have ever logged into an application using “Sign in with Google” or accessed a corporate dashboard without typing your password twice, you have already used one of these protocols. SAML, OAuth2, and OpenID Connect (OIDC) solve related but different problems. This post breaks down what each protocol does, how it works, and when to pick one over another.
The Core Problem
Modern applications rarely manage usernames and passwords on their own. Instead, they delegate identity and access decisions to a trusted third party. This pattern is called federated identity. The three protocols covered here are the most common ways to implement it.
Before diving in, two terms worth knowing:
- Authentication (AuthN): proving who you are (e.g., entering a password).
- Authorization (AuthZ): proving what you are allowed to do (e.g., reading a file).
SAML (Security Assertion Markup Language)
SAML is an XML-based protocol designed in the early 2000s. It is primarily an authentication protocol used in enterprise environments. When your company’s intranet redirects you to a single sign-on (SSO) page, SAML is likely running behind the scenes.
How SAML Works
SAML involves three parties:
- Principal: the user trying to log in
- Service Provider (SP): the application the user wants to access
- Identity Provider (IdP): the system that authenticates the user (e.g., Okta, Azure AD)
sequenceDiagram
participant User
participant SP as Service Provider
participant IdP as Identity Provider
User->>SP: 1. Access application
SP->>User: 2. Redirect to IdP with SAML Request
User->>IdP: 3. Follow redirect
IdP->>User: 4. Show login page
User->>IdP: 5. Submit credentials
IdP->>User: 6. Redirect back with SAML Response (XML assertion)
User->>SP: 7. Forward SAML Response
SP->>SP: 8. Validate assertion and create session
SP->>User: 9. Grant access
The SAML Response contains an assertion, an XML document signed by the IdP. The assertion carries information about the user (name, email, roles) and a digital signature the SP can verify.
SAML Characteristics
- Format: XML
- Transport: HTTP redirects and POST requests via the browser
- Primary use case: enterprise SSO, one login for many internal applications
- Token type: SAML Assertion (XML document)
- Strengths: mature, widely adopted in enterprise, strong security model with XML signatures
- Weaknesses: verbose XML payloads, complex to implement, not mobile-friendly
SSO Initiation: SP vs IdP
SAML supports two ways to start the authentication process depending on where the user begins.
SP-Initiated SSO is the most common flow. The user navigates to the application (Service Provider) first. The SP detects the user is not authenticated and redirects them to the IdP for login. After successful authentication, the IdP sends the SAML assertion back to the SP.
sequenceDiagram
participant User
participant SP as Service Provider
participant IdP as Identity Provider
User->>SP: 1. Access application
SP->>User: 2. Not authenticated, redirect to IdP
User->>IdP: 3. Follow redirect (with AuthnRequest)
IdP->>IdP: 4. Authenticate user
IdP->>User: 5. Redirect back with SAML Response
User->>SP: 6. Forward SAML Response
SP->>User: 7. Grant access
IdP-Initiated SSO starts at the Identity Provider. The user logs into the IdP portal (e.g., an Okta dashboard) and clicks on an application tile. The IdP generates a SAML assertion and sends it directly to the SP without the SP requesting it first.
sequenceDiagram
participant User
participant IdP as Identity Provider
participant SP as Service Provider
User->>IdP: 1. Log into IdP portal
User->>IdP: 2. Click application tile
IdP->>IdP: 3. Generate SAML assertion
IdP->>User: 4. Redirect to SP with SAML Response
User->>SP: 5. Forward SAML Response
SP->>User: 6. Grant access
| SP-Initiated | IdP-Initiated | |
|---|---|---|
| Starting point | Application (SP) | Identity Provider portal |
| AuthnRequest | Yes, SP sends request to IdP | No, IdP sends assertion unsolicited |
| Security | More secure (SP validates request context) | Less secure (no request to correlate with response) |
| Common use | Direct application URLs, bookmarks | Enterprise dashboards, app portals |
SP-Initiated is generally preferred because the SP can include a unique request ID in the
AuthnRequest and verify it matches the InResponseTo field in the SAML Response.
This prevents replay attacks. IdP-Initiated SSO skips this step, making it more vulnerable
to unsolicited response attacks if not carefully implemented.
OAuth2
OAuth2 is an authorization framework published in 2012. It was designed to let a user grant a third-party application limited access to their resources without sharing their password.
A classic example: you allow a photo-printing service to read your Google Photos. The printing service never sees your Google password. It only receives a short-lived access token that grants read access to your photos.
How OAuth2 Works
OAuth2 defines four roles:
- Resource Owner: the user who owns the data
- Client: the application requesting access
- Authorization Server: issues tokens after the user consents
- Resource Server: hosts the protected data (e.g., Google Photos API)
sequenceDiagram
participant User as Resource Owner
participant Client
participant AuthServer as Authorization Server
participant API as Resource Server
User->>Client: 1. Use application
Client->>AuthServer: 2. Redirect to authorization endpoint
AuthServer->>User: 3. Show consent screen
User->>AuthServer: 4. Approve access
AuthServer->>Client: 5. Return authorization code
Client->>AuthServer: 6. Exchange code for access token
AuthServer->>Client: 7. Return access token
Client->>API: 8. Request data with access token
API->>Client: 9. Return protected resource
This is the Authorization Code flow, the most common and secure grant type. OAuth2 also defines other flows (Implicit, Client Credentials, Device Code), but the Authorization Code flow covers the majority of use cases.
PKCE (Proof Key for Code Exchange)
The Authorization Code flow has a vulnerability: if an attacker intercepts the authorization code during the redirect (e.g., on a mobile device via a malicious app registered for the same custom URI scheme), they can exchange it for an access token. PKCE (pronounced “pixy”, defined in RFC 7636) solves this.
Before starting the flow, the client generates a random code verifier and derives a code challenge from it using SHA-256. The client sends the code challenge with the initial authorization request. When exchanging the authorization code for a token, the client sends the original code verifier. The authorization server hashes it and compares with the stored challenge. An attacker who intercepted the code cannot complete the exchange without the verifier.
sequenceDiagram
participant Client
participant AuthServer as Authorization Server
Client->>Client: 1. Generate code_verifier (random string)
Client->>Client: 2. Derive code_challenge = SHA256(code_verifier)
Client->>AuthServer: 3. Authorization request + code_challenge
AuthServer->>Client: 4. Return authorization code
Client->>AuthServer: 5. Token request + code_verifier
AuthServer->>AuthServer: 6. Verify SHA256(code_verifier) == code_challenge
AuthServer->>Client: 7. Return access token
PKCE was originally designed for mobile and single-page applications that cannot securely store a client secret. Since 2021, the OAuth 2.1 draft recommends PKCE for all clients, including confidential (server-side) ones, as a defense-in-depth measure.
OAuth2 Characteristics
- Format: JSON
- Transport: HTTP redirects, JSON REST calls
- Primary use case: delegated authorization, granting third-party apps limited access to user data
- Token type: Access Token (opaque string or JWT)
- Strengths: simple, widely supported, great for APIs and mobile apps
- Weaknesses: OAuth2 by itself does not define authentication. It tells you the user allowed something, not who the user is
The Identity Gap in OAuth2
OAuth2 has no standard way to retrieve user identity. Different providers invented their own endpoints (e.g., /userinfo), leading to incompatible implementations. This gap is exactly what OIDC was created to fill.
OpenID Connect (OIDC)
OpenID Connect is a thin authentication layer built on top of OAuth2. Published in 2014, it adds a standardized way to verify the user’s identity using an ID Token.
Think of it this way: OAuth2 gives you a key to a room. OIDC also hands you an ID card that says who you are.
How OIDC Works
OIDC reuses the OAuth2 Authorization Code flow and adds an ID Token (a JWT) to the response.
sequenceDiagram
participant User
participant Client as Application
participant Provider as OIDC Provider
User->>Client: 1. Click "Sign in with Google"
Client->>Provider: 2. Redirect with scope=openid
Provider->>User: 3. Show login / consent screen
User->>Provider: 4. Authenticate and approve
Provider->>Client: 5. Return authorization code
Client->>Provider: 6. Exchange code for tokens
Provider->>Client: 7. Return access token + ID token (JWT)
Client->>Client: 8. Validate ID token (verify signature, claims)
Client->>User: 9. User is logged in
The ID Token is a JSON Web Token containing standard claims:
| Claim | Meaning |
|---|---|
sub | Unique identifier for the user |
iss | Who issued the token (the provider) |
aud | Who the token is intended for (your app) |
exp | When the token expires |
iat | When the token was issued |
email | User’s email address (optional) |
name | User’s display name (optional) |
The client validates the token by checking the signature against the provider’s public keys
(published at a well-known endpoint), verifying the aud matches its own client ID,
and ensuring the token has not expired.
OIDC Characteristics
- Format: JSON / JWT
- Transport: HTTP redirects, JSON REST calls (same as OAuth2)
- Primary use case: user authentication, “Sign in with X” buttons
- Token type: ID Token (JWT) + Access Token
- Strengths: standardized identity on top of OAuth2, simple for developers, great for web and mobile
- Weaknesses: less mature in legacy enterprise environments where SAML dominates
Discovery: openid-configuration
OIDC defines a standard discovery mechanism. Every OIDC provider publishes a JSON document at a well-known URL:
https://{issuer}/.well-known/openid-configuration
The client fetches this document once and learns everything it needs to interact with the provider: where to send authorization requests, where to exchange codes for tokens, where to find public keys for signature verification, and what features the provider supports. No manual configuration required.
A typical response looks like this:
{
"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"],
"response_types_supported": ["code", "token", "id_token"],
"grant_types_supported": [
"authorization_code",
"refresh_token",
"client_credentials"
],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"code_challenge_methods_supported": ["S256"]
}
Key fields:
| Field | Purpose |
|---|---|
issuer | Base URL of the provider, must match iss in tokens |
authorization_endpoint | Where to redirect the user for login |
token_endpoint | Where to exchange the authorization code for tokens |
userinfo_endpoint | Where to fetch additional user profile claims |
jwks_uri | Where to download public keys for token signature verification |
scopes_supported | What scopes the provider accepts |
response_types_supported | What OAuth2/OIDC flows are available |
code_challenge_methods_supported | What PKCE methods are supported |
This is one of the key advantages OIDC has over both SAML and plain OAuth2. SAML uses XML-based metadata documents that require manual exchange between SP and IdP. OAuth2 has no discovery mechanism at all, so endpoints must be configured manually per provider. OIDC discovery makes integration straightforward: point your client library at the issuer URL, and it configures itself automatically.
Side-by-Side Comparison
| SAML | OAuth2 | OIDC | |
|---|---|---|---|
| Purpose | Authentication (SSO) | Authorization | Authentication + Authorization |
| Year | 2005 | 2012 | 2014 |
| Specification | OASIS Standard | RFC 6749 | OpenID Foundation |
| Data format | XML | JSON | JSON / JWT |
| Token type | SAML Assertion | Access Token | ID Token + Access Token |
| Token size | Large (kilobytes) | Small (opaque) or medium (JWT) | Medium (JWT) |
| Signature | XML Digital Signature | Not defined (TLS) | JWS (JSON Web Signature) |
| Transport | HTTP Redirect, POST | HTTP Redirect, REST | HTTP Redirect, REST |
| Encryption | XML Encryption (optional) | TLS only | JWE (optional) |
| Discovery | Metadata XML | Not defined | .well-known/openid-configuration |
| Built for | Enterprise / web browsers | APIs / third-party access | Web, mobile, APIs |
| Mobile support | Poor | Good | Good |
| SPA support | Not practical | Yes (with PKCE) | Yes (with PKCE) |
| Machine-to-machine | Not designed for it | Yes (Client Credentials) | Yes (Client Credentials) |
| User identity | Yes (assertion attributes) | No (not standardized) | Yes (/userinfo + ID Token) |
| Session management | Single Logout (SLO) | Not defined | Front/Back-Channel Logout |
| Implementation complexity | High | Medium | Medium |
| Library ecosystem | Smaller, enterprise-focused | Large | Large (extends OAuth2 libraries) |
Example Payloads
SAML Assertion
A SAML assertion is an XML document. Even a minimal one is verbose compared to JSON-based tokens:
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_a]b1c2d3e4f5" Version="2.0"
IssueInstant="2026-02-11T12:00:00Z">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<!-- XML Digital Signature (truncated) -->
</ds:Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
user@example.com
</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
InResponseTo="_request123"
Recipient="https://app.example.com/saml/acs"
NotOnOrAfter="2026-02-11T12:05:00Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2026-02-11T12:00:00Z"
NotOnOrAfter="2026-02-11T12:05:00Z">
<saml:AudienceRestriction>
<saml:Audience>https://app.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-02-11T12: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>user@example.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="displayName">
<saml:AttributeValue>John Doe</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute Name="role">
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
OAuth2 Access Token
An OAuth2 access token can be an opaque string or a JWT. The authorization server response looks like this:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...JWT_PAYLOAD...signature",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4"
}
When the access token is a JWT, its decoded payload contains authorization scopes but no standardized identity claims:
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "https://api.example.com",
"scope": "read write",
"exp": 1739282800,
"iat": 1739279200
}
The client uses the token in API requests via the Authorization header:
GET /api/v1/photos HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
OIDC ID Token
An OIDC token response includes both an access token and an ID token. The ID token is always a JWT:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIs...JWT_PAYLOAD...signature",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4"
}
Decoded ID token payload:
{
"iss": "https://auth.example.com",
"sub": "user-123",
"aud": "my-app-client-id",
"exp": 1739282800,
"iat": 1739279200,
"email": "john@example.com",
"name": "John Doe"
}
Key differences from the OAuth2 access token:
audis the client ID (not the API URL), because the ID token is meant for the application, not the resource server- Identity claims (
email,name) are standardized by the OIDC specification
Can They Work Together?
Yes. In practice, many organizations use SAML for internal enterprise SSO and OIDC/OAuth2 for customer-facing applications and API access. An Identity Provider like Okta or Azure AD can speak both protocols simultaneously, acting as a SAML IdP for legacy enterprise apps and an OIDC Provider for modern web and mobile clients.
Summary
- SAML is the enterprise veteran: XML-based, battle-tested, and ideal for browser-based SSO in corporate environments
- OAuth2 is the authorization framework: it controls what apps can access, not who the user is
- OIDC is OAuth2 plus identity: it adds a standardized authentication layer, making it the go-to choice for modern login flows
If you are starting a new project and need user login, start with OIDC. If you are integrating with an existing enterprise IdP, support SAML. If you only need API authorization without user identity, OAuth2 alone is sufficient.