Single Sign-On and Federation
Signing in once for many applications: identity providers and service providers, SAML assertions, OAuth 2.0 authorization flows, OpenID Connect and ID tokens, and what a JSON Web Token contains.
Single sign-on and federation
Single sign-on (SSO) and identity federation are introduced in Identity and Access Management: users sign in once at a central identity provider and then open many applications without signing in again, and the applications trust that identity provider instead of keeping their own passwords. This page looks at how that trust actually works on the wire.
Every federated sign-in has two parties besides the user. The identity provider (IdP; see What Is Identity Management?) authenticates the user — password, MFA, device checks — and vouches for them. A service provider (SP), also called the relying party, is the application that relies on that statement: it has no password of its own for the user and lets them in because a trusted IdP says who they are. At Kestrel Energy, Microsoft Entra ID is the IdP; the payroll SaaS application, the customer portal and the internal wiki are service providers.
Federation means the trust crosses an organizational boundary: the payroll vendor trusts Kestrel's IdP without knowing anything about Kestrel's directory. That trust is set up once, by exchanging metadata: the IdP's sign-in URL and its signing certificate go to the SP, and the SP's identifier and reply URL go to the IdP.
The benefits and risks follow directly:
- One place to enforce MFA and conditional access for every application, and one switch to disable a leaver everywhere.
- Fewer passwords for users, and no password databases at the application vendors to leak.
- But one account and one IdP now unlock everything: the IdP's administrators and its signing keys need the strongest protection you have.
Security tokens and claims
What the IdP hands over is a security token: a data structure, protected against tampering by its issuer (usually with a digital signature) and valid for a limited time, that states facts about a subject as proof of authentication or authorization. A SAML assertion, an OpenID Connect ID token, an OAuth access token and a Kerberos ticket (see Kerberos and NTLM) are all security tokens; a Kerberos ticket is protected mainly by encryption with a key the issuer shares with the service rather than by a signature. Note that this is a different meaning from the token of a language model.
Each fact in a token is a claim: a name–value pair such as "email is lucia.romero@example.com", "department is Billing" or "authenticated with MFA at 14:13". The application reads claims to decide what to show — but only after checking that the token is genuine, meant for it, and not expired. Because a token is signed, the user carrying it through the browser cannot change a claim without breaking the signature (see digital signatures).
SAML 2.0
Security Assertion Markup Language (SAML) is an XML-based standard for exchanging authentication and attribute information between an identity provider and a service provider. Version 2.0 became an OASIS standard in March 2005 and is still the most widely supported way to add enterprise SSO to a SaaS application: almost every business application has a "Configure SAML" page.
An SP-initiated sign-in
In the most common case the user starts at the application. SAML never needs a direct connection between SP and IdP; all messages travel through the user's browser using two bindings (ways of carrying a message over HTTP): the HTTP-Redirect binding puts a compressed, encoded request in the URL of a redirect, and the HTTP-POST binding puts the (larger) response in an HTML form that the browser submits automatically.
The address the response is posted to is the SP's Assertion Consumer Service (ACS) URL, also called the reply URL. The IdP only sends responses to ACS URLs registered for that application, so a phishing site cannot simply ask for an assertion to be delivered to itself. After step 7 the SP creates its own session; SAML is not involved again until that session ends.
In an IdP-initiated sign-in the user starts at the IdP's portal (for example the My Apps page) and the IdP sends an unsolicited response. It is convenient, but the SP cannot match the response to a request it made, which makes replay and injection of stolen responses easier; prefer SP-initiated where the application supports it.
The SAML assertion
A SAML assertion is the signed XML statement inside the IdP's response: who the subject is, how and when they authenticated, which attributes they have, and under which conditions — for which audience and until when — the statement is valid. A shortened example:
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_a7c3e91f" Version="2.0" IssueInstant="2026-09-21T14:13:20Z">
<saml:Issuer>https://login.example.com/</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">…</ds:Signature>
<saml:Subject>
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">lucia.romero@example.com</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData InResponseTo="_req4821"
Recipient="https://payroll.example.net/saml/acs"
NotOnOrAfter="2026-09-21T14:18:20Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="2026-09-21T14:08:20Z" NotOnOrAfter="2026-09-21T15:13:20Z">
<saml:AudienceRestriction>
<saml:Audience>https://payroll.example.net/</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="2026-09-21T14:13:18Z" SessionIndex="_sess77"/>
<saml:AttributeStatement>
<saml:Attribute Name="department">
<saml:AttributeValue>Billing</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
-
Issuernames the IdP; the SP accepts assertions only from issuers it was configured to trust. -
NameIDidentifies the user. A mismatch between what the IdP sends (an email address) and what the application expects (an employee number) is the most common reason a new SAML integration "signs in" but finds no user. -
InResponseTolinks the assertion to the SP's request;RecipientandAudiencesay which application it is for. An SP must reject assertions meant for another SP. -
NotBeforeandNotOnOrAfterlimit the validity to a short window (minutes for delivery, typically up to an hour for the assertion). A clock that is badly off on either side makes valid assertions fail — the same class of problem as Kerberos clock skew.
Certificates and signing
The IdP signs the response or assertion with its private key; the SP verifies the signature with the IdP's certificate, which it received in the metadata (see PKI and Certificates). The certificate is usually self-signed: trust comes from the metadata exchange, not from a certificate authority. Optionally, the IdP also encrypts the assertion with the SP's public key so the browser cannot read the attributes.
Signing certificates expire, typically after a few years. When the IdP's certificate is renewed, every SP that still holds the old one rejects all sign-ins at the moment of the switch. Track expiry dates, add the new certificate to the SPs before activating it, and prefer SPs that read the IdP's metadata URL automatically.
OAuth 2.0
OAuth 2.0 (RFC 6749, October 2012) is a framework for delegated authorization: it lets an application obtain limited access to an API on behalf of a user, or for itself, without ever seeing the user's password. The classic example is a photo-printing site that asks "allow access to your photos?" — you approve at your photo service, and the printing site receives a token that can read photos, nothing else. OAuth is not an authentication protocol: a token that says "may read photos" does not tell the client who the user is.
OAuth defines four roles:
| Role | Meaning | At Kestrel Energy |
|---|---|---|
| Resource owner | Whoever can grant access to the data, usually the user | A billing clerk |
| Client | The application that wants to call the API | The billing portal web application |
| Authorization server | Authenticates the user, asks consent, issues tokens | Microsoft Entra ID |
| Resource server | The API that accepts the access token | The invoice API |
Permissions in OAuth are expressed as scopes, such
as invoices.read. The client asks for scopes, the user
(or an administrator) consents, and the token carries the granted
scopes. Ask for as few scopes as the application needs.
The authorization code flow with PKCE
In the authorization code flow, the client sends the user's browser to the authorization server, which signs the user in and redirects back with a short-lived, single-use authorization code. The client then exchanges that code for tokens in a direct call to the token endpoint. The tokens never pass through the browser's address bar, and a confidential client (one that runs on a server and can keep a secret) also authenticates itself at that exchange.
PKCE (Proof Key for Code Exchange, RFC
7636, September 2015, pronounced "pixy") protects the code: before
redirecting, the client creates a random secret, the
code_verifier, and sends only its SHA-256 hash, the
code_challenge. At the token endpoint it must present
the verifier itself. An attacker who intercepts the code — through a
malicious app on a phone, or a leaked log — cannot redeem it without
the verifier. PKCE was designed for mobile and single-page apps that
cannot keep a secret; the OAuth security best current practice (RFC
9700, January 2025) requires it for such public clients and
recommends it for all others.
Other flows
- Implicit flow: returned tokens directly in the browser's redirect URL, where history, logs and scripts can leak them. RFC 9700 says clients should not use it; single-page apps use the authorization code flow with PKCE instead.
- Resource owner password credentials: the app collects the user's password and sends it to the token endpoint. It defeats the purpose of OAuth, cannot do MFA, and RFC 9700 says it must not be used.
- Client credentials: no user at all; a service authenticates with its own secret or certificate and gets a token for itself. This is how background jobs and daemons call APIs (see Identity for Applications and Machines).
- Device authorization grant (device code flow, RFC 8628): for devices without a usable browser, such as a TV, a conference-room system or a command-line tool. The device shows a short code; the user enters it at a sign-in page on another device.
OAuth 2.1
OAuth 2.1 consolidates OAuth 2.0 and a decade of security guidance into one document: PKCE for every authorization code flow, no implicit or password grants, and exact matching of redirect URIs. As of October 2026 it is still an Internet-Draft (draft-ietf-oauth-v2-1-16, September 2026), not an RFC. In practice most identity providers already behave as it describes; cite RFC 6749 and RFC 9700 as the current standards.
OpenID Connect and the ID token
OpenID Connect (OIDC; also used for GitHub Actions to cloud sign-in,
see
GitHub Actions) is an
identity layer on top of OAuth 2.0. It reuses the same flows — the
client adds the scope openid — and adds what OAuth
lacks: a standard way to tell the client who signed in. OpenID
Connect Core 1.0 was finalized in 2014 and has also been published
as an International Organization for Standardization (ISO) standard.
That answer is the
ID token: a JSON Web Token issued to
the client that describes the authentication event — who the user is
(sub), which IdP says so (iss), which
client it is for (aud, the client ID), when the user
authenticated and until when the token is valid. A decoded ID token
payload:
{
"iss": "https://login.example.com/",
"sub": "4f3a9d1e-77c2-4b0e-9a51-2d6c8e0b13f7",
"aud": "6a1f2c3d-5b4e-4f70-8a19-0c2d3e4f5a6b",
"iat": 1790000000,
"exp": 1790003600,
"auth_time": 1789999998,
"nonce": "n-8Hq2xLd0",
"name": "Lucía Romero",
"preferred_username": "lucia.romero@example.com",
"amr": ["pwd", "mfa"]
}
The nonce is a random value the client sent in its
request; finding it back in the ID token proves the token belongs to
this sign-in and is not replayed from another one. The
sub is the stable identifier: use it, not the email
address or display name, as the key for a user in your application,
because names and addresses change.
Access, ID and refresh tokens
An access token is the token a
client presents to an API — usually in the header
Authorization: Bearer <token> (RFC 6750) — to
show it may perform certain operations. It is meant for the API, not
for the client: the client should treat it as opaque, even when it
happens to be a readable JWT.
A refresh token is a longer-lived credential that the client sends only to the authorization server, to get new access tokens without asking the user to sign in again. This lets access tokens stay short-lived (often an hour or less) while the user's session lasts much longer. Because it is long-lived, a stolen refresh token is valuable; identity providers rotate them, bind them to the client or device, and revoke them when a password is reset or a user is disabled.
| Token | Meant for | Answers | Typical lifetime |
|---|---|---|---|
| ID token | The client application | Who signed in, how and when? | Minutes to an hour; used once at sign-in |
| Access token | The API (resource server) | What may this caller do? | Minutes to an hour |
| Refresh token | The authorization server | May this client get a new access token? | Hours to months, revocable |
JSON Web Tokens
A JSON Web Token (JWT,
pronounced "jot"; RFC 7519, May 2015) is a compact token format: a
signed JWT has three base64url-encoded parts joined by dots —
header.payload.signature. The header says how the token
is signed (alg, for example RS256) and with which key
(kid); the payload is a JSON object of claims; the
signature covers the first two parts. The registered claims you will
meet most:
-
iss(issuer): who created and signed the token; sub(subject): whom the token is about;aud(audience): who the token is meant for;-
exp(expiration time) andiat(issued at), in seconds since 1970 (Unix time).
Base64url is an encoding, not encryption. The example below decodes a sample access token with nothing but the standard library, then makes the claim checks an API performs. It deliberately does not verify the signature — read the paragraph after it before you reuse any of this.
import base64
import json
# A sample token, as an API receives it in the header "Authorization: Bearer <token>".
TOKEN = (
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlbW8ta2V5LTEifQ."
"eyJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tLyIsInN1YiI6IjRmM2E5"
"ZDFlLTc3YzItNGIwZS05YTUxLTJkNmM4ZTBiMTNmNyIsImF1ZCI6ImFwaTovL2Jp"
"bGxpbmctcG9ydGFsIiwiaWF0IjoxNzkwMDAwMDAwLCJleHAiOjE3OTAwMDM2MDAs"
"Im5hbWUiOiJMdWPDrWEgUm9tZXJvIiwicm9sZXMiOlsiQmlsbGluZy5SZWFkIl19."
"c2lnbmF0dXJlIGJ5dGVzIGZyb20gdGhlIElkUA"
)
def b64url_decode(part):
"""Decode base64url; JWTs leave out the '=' padding, so add it back."""
return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
header_b64, payload_b64, signature_b64 = TOKEN.split(".")
header = json.loads(b64url_decode(header_b64))
payload = json.loads(b64url_decode(payload_b64))
print("header:", header)
for claim, value in payload.items():
print(f" {claim:<6}{value}")
print("signature:", len(b64url_decode(signature_b64)), "bytes - NOT verified here")
def check_claims(payload, audience, now):
"""Claim checks an API makes AFTER a library has verified the signature."""
if payload["aud"] != audience:
return "reject: token was issued for another API"
if now >= payload["exp"]:
return "reject: token has expired"
return f"accept: valid for {(payload['exp'] - now) // 60} more minutes"
print()
print(check_claims(payload, "api://billing-portal", now=1790001800))
print(check_claims(payload, "api://billing-portal", now=1790004000))
print(check_claims(payload, "api://hr-portal", now=1790001800))
header: {'alg': 'RS256', 'typ': 'JWT', 'kid': 'demo-key-1'}
iss https://login.example.com/
sub 4f3a9d1e-77c2-4b0e-9a51-2d6c8e0b13f7
aud api://billing-portal
iat 1790000000
exp 1790003600
name Lucía Romero
roles ['Billing.Read']
signature: 28 bytes - NOT verified here
accept: valid for 30 more minutes
reject: token has expired
reject: token was issued for another API
Decoding is not verifying. Anyone can build a token with any claims
— "roles: Global Administrator" included — and base64url-encode it;
the code above would print it just as happily. Real code verifies
the signature with a maintained JWT or OIDC library and the issuer's
public keys, which the IdP publishes as a JSON Web Key Set (JWKS) at
the jwks_uri listed in its discovery document
(/.well-known/openid-configuration). The library picks
the key by kid, accepts only the algorithms you
configure (never none, and never a symmetric algorithm
when you expect RS256), and then checks iss,
aud, exp and nbf with a small
allowance for clock skew. Only then are the claims worth reading.
Choosing a protocol, and common mistakes
| Situation | Usual choice |
|---|---|
| Existing enterprise SaaS application with a "SAML" settings page | SAML 2.0 |
| New web, mobile or single-page application that signs users in | OpenID Connect, authorization code flow with PKCE |
| Application calls an API on behalf of the user | OAuth 2.0 access token (from the same OIDC sign-in) |
| Background service calls an API | OAuth 2.0 client credentials, ideally without a stored secret |
| Windows desktop and file shares inside the domain | Kerberos (see Kerberos and NTLM) |
- Skipping audience checks: an API that accepts any valid token from the IdP also accepts tokens issued for other applications.
-
Wildcard redirect URIs: an authorization server
that sends codes to any URL matching
https://*.example.comcan be tricked into delivering them to an attacker's subdomain. Register exact redirect URIs. - Forgetting the signing certificate's expiry date for SAML applications, so SSO breaks without warning.