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.

SP-initiated SAML sign-in. 1: the browser requests the payroll application. 2: the service provider redirects the browser to the identity provider with an AuthnRequest. 3: the browser delivers the AuthnRequest to the identity provider. 4: the identity provider signs the user in with MFA. 5: it returns an HTML form containing the signed SAML response. 6: the browser posts it to the service provider's assertion consumer service. 7: the service provider verifies signature, audience and time. 8: it returns the application with its own session cookie. Browser Service provider Identity provider 1. GET /payroll 2. redirect + AuthnRequest 3. AuthnRequest 4. sign-in + MFA 5. form with signed Response 6. POST to the ACS URL 7. verify signature, audience, time 8. app + session cookie
The SP and the IdP never talk directly during sign-in; the browser carries the request (Redirect binding) and the response (POST binding).

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>
  • Issuer names the IdP; the SP accepts assertions only from issuers it was configured to trust.
  • NameID identifies 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.
  • InResponseTo links the assertion to the SP's request; Recipient and Audience say which application it is for. An SP must reject assertions meant for another SP.
  • NotBefore and NotOnOrAfter limit 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.

Authorization code flow with PKCE. 1: the client sends the user through the browser to the authorization server with a code challenge. 2: the authorization server signs the user in and asks consent. 3: it redirects back to the client with an authorization code. 4: the client calls the token endpoint with the code and the code verifier. 5: it receives an access token, and with OpenID Connect an ID token and possibly a refresh token. 6: the client calls the API with the access token as a bearer token. 7: the API checks the token and returns data. Client app Authorization server Resource server (API) 1. /authorize + code_challenge 2. sign-in, consent 3. redirect with code 4. /token: code + verifier 5. access (+ ID, refresh) token 6. Bearer access token 7. check token, return data
Steps 1 and 3 go through the user's browser; steps 4 to 7 are direct calls. Only the client that created the verifier can redeem the code.

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) and iat (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.com can 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.