SASE, SAML & Modern Identity: Part 3 — OIDC & OAuth 2.0

Share
SASE, SAML & Modern Identity: Part 3 — OIDC & OAuth 2.0

Part 3 of 3. Parts 1 and 2 covered SASE architecture and SAML. This one is OIDC and OAuth 2.0: the token-based identity framework that powers most modern authentication. There's a lot of ground to cover — stick with it through the token anatomy section, because everything else builds on it.


OAuth 2.0 and OIDC: The Distinction

Two separate things that are almost always used together and frequently confused.

OAuth 2.0 is an authorization framework. It answers one question: what is this application allowed to do on behalf of this user? It handles delegated permissions. When you connect a third-party app to your Google account and it asks whether it can read your calendar, that consent mechanism is OAuth 2.0.

OIDC (OpenID Connect) is an identity layer built on top of OAuth 2.0. It adds the authentication piece OAuth 2.0 deliberately left out. Where OAuth 2.0 answers "what can this app do," OIDC answers "who is this user." It extends the OAuth 2.0 flow to return a cryptographically verifiable identity credential alongside the authorization tokens.

OIDC is often treated as the modern alternative for SAML. The core difference at the protocol level is the data format: SAML uses XML, with all the document tree structure, DOM parsing, and namespace complexity that creates. OIDC uses JSON — a flat key/value dictionary. There's no document object model, no canonicalization, no ability to define external entities. The entire class of XML-based attacks covered in Part 2 simply doesn't exist in a JSON-based protocol (though it still has its own risks).

Session-Based vs Stateless Tokens

SAML and OIDC handle session continuity differently, and understanding the tradeoff matters for appreciating why each design decision exists.

Session-based (SAML): After the initial assertion is processed, the application usually creates its own local session for the browser. The SAML assertion is not sent on every click, and the application usually is not checking with the IDP on every request. It checks its own session cookie or server-side session state until that local session expires or the application forces a new authentication flow, sending it back to the IDP.

The problem with this at scale: if you have a hundred microservices, each calling back to a central authority or session store before making an access decision, that constant validation traffic becomes a bottleneck. That is one reason token-based systems became attractive: services can often validate signed tokens locally instead of making a network call for every request.

Token-based validation (OIDC/JWT): In many OIDC deployments, a signed JWT contains enough claims for the receiving application or API to validate it locally: issuer, audience, subject, expiration, and signature. That makes authorization decisions faster and reduces dependency on a central session lookup. This is not universal, though — some access tokens are opaque and require introspection, and many web applications still create their own local session after the OIDC login completes.

The tradeoff: if a JWT is stolen, the attacker has a valid credential until the token expires. The application isn't checking with the IDP on each request, so there's no immediate mechanism to revoke access mid-session. The token is a golden ticket for its lifespan. Short expiration times are the primary mitigation, and Continuous Access Evaluation — covered at the end of this post — is a modern solution to the revocation gap.


The Four Actors

Every OAuth 2.0 and OIDC flow involves four parties:

Resource Owner: The user. The entity capable of granting access to protected data — their profile, their files, their contacts. When a consent screen asks "Do you allow this app to view your email address?", it's asking the resource owner to authorize access to data they own.

Client: The application requesting access on behalf of the resource owner. Also called the Relying Party (RP) because it relies on identity information from the authorization server. This is roughly equivalent to the Service Provider in SAML. It's the software actively making the request — the HR portal, the mobile app, the dashboard.

Authorization Server: The central identity authority — Okta, Microsoft Entra, Auth0. It knows the user, handles authentication, presents the consent screen, and mints and issues tokens. This is the IDP. When you hear "auth server" in the context of OIDC, this is what it means.

Resource Server: The API that sits in front of the protected data. It doesn't authenticate users. It accepts tokens, validates them mathematically, and responds to requests. If the client has a valid access token, the resource server serves the data. If not, it returns a 401.

Note that the resource server and the client are often described as being on the "service provider" side, but they have different jobs. The client is the application interacting with the user. The resource server is the backend API holding the data the client is trying to access.


Why the ID Token Exists: Token Substitution

Before getting into the token types themselves, it's worth understanding the specific attack that OIDC's ID token was designed to prevent.

The access token should be treated as opaque by the client — just a credential the client passes to the resource server. In some systems it really is an opaque random string. In others, it is a JWT that can technically be decoded. Either way, the client should not use the access token as proof of user identity. It is meant to be presented to the resource server, which validates it and decides what API access to allow.

A vulnerability arises if an application uses the access token alone as proof of identity. An attacker who obtains a valid access token — for example, by building a malicious third-party app that convinces a user to authorize it — can take that token and inject it into a different, unrelated application.

The mechanics: Okta issues a valid access token to the attacker's app after a user authenticates. The attacker takes that token and presents it to a target application. The target application sees a valid token signed by Okta, trusts Okta as an issuer, and grants access. It never checks whether the token was intended for it specifically. A valid key for any door in the building opens every door.

Real-world vectors for this are more common than they sound. Employees regularly connect third-party utilities to their corporate accounts — PDF converters, calendar tools, project management apps. Some of these are attacker-controlled. Attackers have enrolled in legitimate platform partner programs to create applications that appear official. A vendor your company authorized could be compromised, and the tokens from that vendor's app could be used to pivot into other systems.

OIDC fixes this with the ID token and specifically with the audience claim — a field inside the token that names exactly which client application the token was issued for. An application that validates the audience claim will reject any token that wasn't specifically issued to it. A token that works for App A cannot be replayed against App B.


The Three Tokens

Access Token

The access token is OAuth 2.0's artifact — purely for authorization. The client receives it, treats it as an opaque string, and attaches it to the Authorization header of API requests to the resource server as a bearer token:

Authorization: Bearer <access_token>

The client cannot decode the access token and doesn't try to. It's not meant for the client to read — it's a credential to be presented to the resource server. The resource server validates it and decides what to return.

Access tokens are intentionally short-lived, typically measured in minutes or around an hour, depending on the provider and policy. If an attacker intercepts one, they have a narrow window before it becomes mathematically invalid.

ID Token

The ID token is the centerpiece of OIDC. It's specifically for the client application — not for the resource server — and it carries the cryptographically verified identity of the authenticated user. It's formatted as a JWT (JSON Web Token), which has three parts separated by periods. Each part is a separate JSON object that's been base64-URL encoded, which replaces characters like / with URL-safe alternatives so they're not misinterpreted as path separators in transit.

Part 1: The Header

Metadata about the token itself.

alg: The signing algorithm. Almost always RS256 — RSA signature with SHA-256. Asymmetric algorithms are required here. If you see alg: none or a symmetric algorithm like HS256 in a token that crosses a trust boundary, that should be treated as a serious validation red flag. An asymmetric algorithm means only the auth server — which holds the private key — can sign tokens, while any application can verify them using the public key.

kid (Key ID): A pointer indicating which of the auth server's keys was used to sign this token. A large enterprise auth server might have several active keys simultaneously (during rotation periods, for instance). The KID tells the client which specific public key to fetch for verification.

Part 2: The Payload (Claims)

The actual content — statements about the user and the authentication event.

iss (Issuer): The URL of the authorization server that created the token. The client confirms this matches the expected issuer before trusting anything else in the payload.

aud (Audience): The client application the token was issued for, identified by its client ID. This is what prevents the token substitution attack. The client validates that its own client ID appears in this claim. If it doesn't, the token is rejected.

sub (Subject): A unique, immutable identifier for the authenticated user — typically a database UUID, an internal employee number, or a generated ID. Using an email address as the long-term application identifier is a bad idea. People get married, change their names, update their email addresses. If the sub claim is an email address and a user changes theirs, the primary key of every database relationship in every application that stored that email as the user identifier breaks. The sub needs to be a permanent anchor that will never change, regardless of what happens to the user's profile information.

iat (Issued At): Unix timestamp of when the token was minted.

exp (Expiration): Unix timestamp after which the token must be rejected.

nbf (Not Before): Unix timestamp before which the token is not yet valid. Prevents a token issued for a future window from being used immediately.

Part 3: The Signature

TLS secures data in transit. Once the token arrives at its destination and exits the TLS tunnel, TLS is done. In a browser-based flow, the token has traveled through a browser — an environment with extensions, developer tools, local storage that records URL parameters, and potential malware. An attacker with access to the browser context could modify the payload. They could change the sub claim to an admin's user ID. Without a signature, the application would have no way to detect the tampering and would believe the modified claims.

The signature provides message-level security using JWS (JSON Web Signature):

The auth server holds a private key in secure storage. When minting the token, it takes the header and payload, runs them through SHA-256, and encrypts the resulting hash with its private key. That encrypted hash becomes the signature in the third part of the JWT.

When the client receives the ID token:

  1. It reads the alg and kid from the header.
  2. It fetches the corresponding public key from the auth server's public key endpoint (or local key cache, if it's there already).
  3. It decrypts the signature using that public key, which yields the original hash.
  4. It independently computes the SHA-256 hash of the received header and payload.
    1. It also validates the claims that matter: issuer, audience, expiration, nonce, and any application-specific requirements
  5. It compares the two hashes. If they match, the token was signed by the entity holding the corresponding private key and the content has not been modified since it was signed.

Refresh Token

Access tokens and ID tokens expire in minutes. Requiring users to fully re-authenticate with MFA every 15 minutes is not a usable product. The refresh token solves this.

Refresh tokens are long-lived — valid for days, weeks, or sometimes months depending on the application's configuration. They are guarded heavily by the client application. A refresh token should never be sent to the resource server. For backend web applications, it should stay server-side and never touch the browser. Browser-based apps require much stricter handling, usually with refresh-token rotation and storage controls, because anything exposed to the browser is harder to protect.

When the access token expires and the resource server starts returning 401 errors, the client opens a direct back-channel connection to the auth server's token endpoint, presents the refresh token, and requests a new access token. The auth server validates the refresh token and issues a fresh access token, entirely invisible to the user. The user never sees a login screen.

To receive a refresh token in the first place, the client must request the offline_access scope during the initial authorization request. If that scope isn't included, no refresh token is issued. Some authorization servers may issue refresh tokens in other contexts based on their own policies, but offline_access is the clean protocol-level signal for “this client needs longer-lived renewal.


The Authorization Code Grant

The most widely used and secure flow in OAuth 2.0 and OIDC. The user never shares their password with the client application. The architecture splits into a front-channel — communication that travels through the user's browser — and a back-channel — a direct, secured server-to-server connection.

Step 1 — Front-channel redirect:
The user clicks login on the HR portal. The portal doesn't show a username and password form. Instead, it generates an HTTP 302 redirect, sending the browser to the authorization server's /authorize endpoint. Embedded in the URL's query string:

  • client_id: the application's public identity
  • redirect_uri: the address to send the user back to after login
  • scope: the permissions being requested (typically openid to trigger OIDC, plus profile and email for user information)
  • response_type=code: telling the auth server not to return tokens yet — return only a temporary authorization code

Step 2 — Authentication and consent:
The browser arrives at the auth server. The auth server prompts for credentials — password, hardware key, MFA. Once identity is verified, the auth server presents a consent screen listing the scopes the client requested in step 1. The resource owner reviews and approves.

Step 3 — Front-channel return:
The auth server generates another 302 redirect, sending the browser back to the client application using the exact redirect_uri from step 1. Appended as a query parameter is the authorization code — a short-lived, single-use, opaque string, typically valid for about 60 seconds. The browser lands back at the client application with this code in the URL.

Even if an attacker captures this code from browser history or a referrer header, it's not immediately useful. To exchange the code for tokens at the token endpoint, the attacker would need the client application's secret — a credential stored securely in the application's backend that never touches the browser.

Step 4 — Back-channel token exchange:
The client application's backend server retrieves the code from the URL. It bypasses the browser entirely, opens a direct TLS connection to the auth server's token endpoint, and sends an HTTP POST request containing:

  • The authorization code
  • The redirect_uri (as a binding check — must match what was sent in step 1)
  • The client ID and client secret in the HTTP Authorization header

Step 5 — Token issuance:
The auth server validates the client secret (confirming the application identity), verifies the authorization code (ensuring it's unused, matches this client, and hasn't expired), and responds with a JSON payload containing the access token, ID token, and refresh token.

The client secret is provisioned before the OAuth flow ever runs. During application registration with the auth server, the developer receives a client ID and a client secret. The secret is generated by the platform or entered manually. It's shown exactly once and stored in the application's backend environment variables — never in client-side code.

Step 6 — Token validation and session establishment:
The client validates the tokens received in step 5. It confirms the ID token's signature, checks the audience and expiration claims, and verifies that the nonce embedded in the ID token matches the value it stored locally before step 1. Once all checks pass, the client establishes a local session, stores the access token for API calls, and stores the refresh token for later renewal. The user has access.


Public Clients and the Authorization Code Interception Attack

Mobile applications and single-page applications running in a browser are public clients. Their source code is exposed. You can inspect a web app's JavaScript or decompile an Android APK. If a developer hardcodes the client secret into a mobile app, an attacker downloads the app, decompiles the binary, and extracts the secret string. Public clients cannot hold secrets. The back-channel security of step 3 above no longer applies.

Without a client secret, the auth server only verifies the client ID when issuing tokens. This created a specific attack:

An attacker installs a malicious application on a target device and registers it with the operating system using the same custom URL scheme as the legitimate application — for example, my-hr-app://auth. A custom URL scheme is an app-specific protocol name registered with the OS that tells it to route links matching that prefix to a particular application. When the browser completes authentication and redirects to my-hr-app://auth?code=..., the OS needs to know which application to deliver that to.

Early mobile operating systems did not enforce global uniqueness for custom URL schemes. Multiple installed applications could register for the same scheme. The OS, seeing two apps registered for my-hr-app://, might deliver the authorization code to the malicious app instead of the legitimate one. The attacker then has a valid code and the client ID. Without a secret requirement, they can trade the code for tokens at the token endpoint and access the user's account.


PKCE

PKCE (Proof Key for Code Exchange), pronounced "pixie," solves this without requiring a static secret. It injects a dynamic cryptographic challenge directly into the existing flow.

Before step 1, the client generates a random, high-entropy string of 43 to 128 characters. This is the code verifier. The client stores it locally.

The client then hashes the verifier using SHA-256 and base64-URL encodes the result. This is the code challenge. The code challenge is a one-way transformation — you can derive the challenge from the verifier, but you cannot reverse the process to recover the verifier from the challenge.

In step 1, the client includes the code challenge and the hash method (S256) in the redirect to the auth server. The auth server stores the challenge.

In step 4, when the client makes the back-channel token request, it sends the original raw code verifier — not the challenge. The auth server hashes the verifier and compares the result to the challenge it stored earlier. If they match, it proves the entity making the token request is the same one that initiated the flow, because only that entity knew the original verifier.

Brute-forcing this in the interception window is not practical. The code verifier is a high-entropy string with a minimum of 43 characters — the number of possible inputs is large enough that exhaustive search against a SHA-256 hash would take far longer than the 60-second authorization code validity window. By the time any brute-force attempt could complete, the code it was trying to exchange has already expired.

PKCE was designed initially for native/mobile apps and other public clients, but current OAuth security guidance extends the recommendation much further. Public clients must use PKCE, authorization servers must support it, and confidential clients are recommended to use it as an additional defense. The defense is additive, not exclusive.


State and Nonce

State

The state parameter protects the front-channel redirect against CSRF.

Before step 1, the client generates a random unique string — the state — and stores it in the user's local session. It sends this state in the initial redirect to the auth server. In step 3, the auth server echoes the exact same state value back in the redirect to the client. The client checks that the returned state matches what it sent. If it doesn't, the request is dropped.

Why this stops CSRF:

An attacker completes the first part of their own OAuth flow using their own legitimate credentials. The auth server returns an authorization code tied to the attacker's account. The attacker constructs a malicious link pointing to the client application's callback URL with their code injected into it — for example, hr-app.com/callback?code=[ATTACKER_CODE]. They trick the victim's browser into loading that URL.

Without state validation, the client sees a valid-looking authorization code and trades it for tokens. A session is established, but it's tied to the attacker's account. The victim is now logged in as the attacker. Anything the victim does — entering personal data, submitting forms — is recorded against the attacker's identity.

With state validation, the attack fails. The attacker doesn't know the random state string the victim's browser generated locally when the victim clicked the login button. The state value returned in the attacker's crafted URL won't match the victim's session state. The client detects the mismatch and drops the code.

The state parameter travels as a query parameter in the front-channel redirect and is technically accessible to browser extensions or local malware. That's acceptable because the attack being prevented is a pre-generated, externally crafted request. An attacker trying to inject a forged callback doesn't know what state string was generated on the victim's device in real time. An attacker who has already compromised the device has larger problems to worry about.

Nonce

The nonce protects the ID token against replay attacks — specifically, preventing a stolen, still-valid ID token from being used to establish a new session.

The client generates a unique random string (call it Nonce_Old) before step 1 and stores it in the local session. It includes Nonce_Old in the authorization request. The auth server embeds this exact value as a claim inside the ID token it mints.

Original login flow:
The client receives the ID token, validates the signature, decodes the payload, and checks that the nonce claim matches Nonce_Old from the local session. It does. Session established. An attacker, at some point, steals this ID token.

Replay attempt:
The attacker tries to start a new session using the stolen token the next day. To do this, they need the client to be in a state where it's expecting an incoming token response — so they trigger a new login flow. The client, starting fresh, generates a new random string, Nonce_New, and stores it locally.

The attacker injects the stolen token into the client's incoming response. The client validates the signature — it passes, because the token is legitimately signed. The client checks the nonce claim in the payload. The token contains Nonce_Old. The client is currently expecting Nonce_New. They don't match. The token is rejected. The replay fails.

Each of these three parameters defends a different layer: state protects the redirect, nonce protects the ID token, PKCE protects the authorization code.


Refresh Token Rotation

Refresh tokens are long-lived and must be guarded accordingly. If an attacker steals one, they can silently maintain access for days or weeks by using it to request new access tokens without ever triggering a login screen.

Refresh token rotation removes this persistence. Every time the client uses a refresh token at the token endpoint, the auth server issues both a new access token and a new refresh token, immediately invalidating the old refresh token. The client updates its stored token with the replacement.

Scenario 1: The legitimate client uses the token first.

An attacker steals Refresh Token A from the user's device. The legitimate client application on the same device also holds Token A. The client makes an API request, the access token has expired, and it uses Token A to refresh. The auth server issues Access Token 2 and Refresh Token B. Token A is now invalidated.

The attacker subsequently sends Token A to the auth server. The server sees it has already been used and invalidated. It denies the request and triggers a defense: the active refresh token for that grant is revoked, forcing the user to re-authenticate. If the attacker only stole the refresh token and had not already obtained a fresh access token, their access window was effectively zero.

Scenario 2: The attacker uses the token first.

The attacker sends Token A to the auth server before the legitimate client does. The server has no reason to reject it — it hasn't been used yet. It issues Access Token 2 and Refresh Token B to the attacker. Token A is now marked as used.

The legitimate client eventually tries to refresh using its copy of Token A. The auth server sees Token A has already been used. Reuse detection triggers: a used token being presented again means either the client or the token has been compromised. The auth server immediately revokes the entire token family, including the attacker's active Refresh Token B. The attacker's session is terminated. The legitimate user is logged out and must re-authenticate from scratch.

The outcome in both scenarios: the attacker's window is limited to the period between stealing the token and either the client or the auth server detecting the reuse. Refresh token rotation converts a potentially unlimited compromise into a temporary one with a guaranteed detection event.


Session Termination and Back-Channel Logout

Closing a browser tab clears some temporary in-page memory and might clear session cookies if the browser is configured strictly — but it doesn't touch access or refresh tokens stored in application local storage. The IDP never receives any signal that the session is over. The IDP session and each application's local session operate independently.

When a user clicks the logout button inside a client application, that application deletes its locally stored tokens and ends its own session. The central IDP session — maintained via a secure HTTP-only cookie set on the IDP's own domain — remains active. If the user navigates to any other application and tries to log in, they're redirected to the IDP, which sees its session cookie is still valid and silently signs them in without prompting for credentials.

For an employee termination or a "sign out of all devices" action, this requires a deliberate teardown mechanism. In OIDC back-channel logout, the OpenID Provider sends a specialized JWT called a logout token (signed with the same private key used for ID tokens) to relying parties that support the mechanism. The token identifies the user or session being logged out and includes an event claim explicitly stating this is a logout action.

The IDP sends a direct server-to-server HTTP POST containing this logout token to the pre-registered logout endpoint of every relying party application the user interacted with during the session. Each application's backend receives the POST, verifies the signature to confirm it came from the IDP, reads the user ID from the sub claim, and immediately destroys all local session data for that user — revoking active tokens, rejecting any in-flight requests from that user, and preventing any new requests from going forward.

The result is coordinated, cryptographically verified session termination across participating applications. One signal from the IDP. Every door integrated with the logout mechanism closed.


OIDC Auto-Discovery Document

Manually configuring an application with an IDP's endpoint URLs, supported scopes, and public keys is operationally brittle. URLs change. Keys rotate. OIDC solves this with the auto-discovery document.

Every OIDC-compliant IDP publishes a JSON metadata document at a standardized URL:

https://<your-idp-domain>/.well-known/openid-configuration

A client application makes an HTTP GET to that URL and receives a flat JSON object containing everything it needs to integrate:

  • issuer: The auth server's canonical identity URL. The client checks this matches the authority it expects.
  • authorization_endpoint: The URL for step 1 of the authorization code flow.
  • token_endpoint: The URL for step 4, the back-channel token exchange.
  • response_types_supported: Confirms the IDP supports the authorization code flow.
  • scopes_supported: Lists available scopes — the client can check whether it can request email or profile data before attempting to use those scopes.
  • jwks_uri: The URL of the IDP's JWKS (JSON Web Key Set) — a JSON document listing the IDP's currently active public keys, identified by their key IDs.

The jwks_uri field enables zero-downtime cryptographic key rotation, which is worth understanding in detail because it's one of the more elegant operational improvements OIDC offers over SAML.

Key rotation without auto-discovery: The security team decides the signing key is due for rotation. Every application has the old public key hardcoded or stored in its configuration. The IDP switches to the new private key. Every application immediately starts failing to validate ID tokens because the public key they have doesn't match. Administrators scramble to manually update the public key in every application. During this window, authentication is broken.

Key rotation with auto-discovery:

  1. The IDP generates a new private/public key pair and begins using the new private key to sign tokens.
  2. The new tokens include the new key's ID in the kid header field.
  3. The IDP updates its JWKS document to include the new public key alongside the old one (during the transition period, both may be in use).
  4. A client application receives an ID token, reads the kid from the header, and doesn't recognize it — it's not in the client's local key cache.
  5. The client fetches the JWKS document from the jwks_uri it learned during discovery, finds the key matching the new KID, caches it, and validates the token.

No application configuration changes. No downtime. No manual intervention from any administrator. The key rotation is fully transparent to every integrated application.


Continuous Access Evaluation

CAE is the modern answer to the revocation gap in stateless token-based authentication.

The problem: once an access token is issued, it's valid until it expires. If an employee is fired five minutes after logging in and holds a one-hour access token, they have 55 minutes of uninterrupted access to every resource that token covers. The resource servers aren't checking with the IDP on each request — they're validating the token's signature locally. There's no mechanism for the IDP to reach out and say the token is no longer valid.

CAE (Continuous Access Evaluation) creates a near real-time event channel between the IDP and participating resource servers. The IDP and applications subscribe to a Shared Signals Framework (SSF). Instead of waiting for token expiry, the IDP pushes security events to subscribed applications the moment they occur.

Four main categories of events trigger immediate revocation:

Account deactivation or disablement: The user is deleted or disabled in Active Directory or the IDP. Any active tokens for that user are invalidated immediately.

Password reset: A password change indicates either a legitimate security action or a possible account compromise. All active tokens become suspect and are revoked.

Token theft via impossible travel: The user authenticates from Atlanta. Ten minutes later, a request arrives from London. Human travel makes this impossible. The IDP flags the discrepancy and revokes the session.

Device non-compliance: The user's device is reported as no longer meeting security requirements — the endpoint protection software was uninstalled, the OS is unpatched, disk encryption was disabled. The session associated with that device is invalidated.

The revocation is not instant but is near-real-time. Applications that support CAE typically enforce the revoked state within roughly 30 seconds of the event, rather than waiting for the token's full expiry window to close. The gap between a security event and its enforcement drops from up to an hour to under a minute.

Note: In Microsoft Entra, the goal for critical-event evaluation is near real time, though event propagation latency of up to 15 minutes can be observed. The important architectural point is that enforcement becomes event-driven instead of purely expiration-driven.


That's the full series. SASE covers the network and security architecture. SAML covers how federated identity with XML assertions works and breaks. OIDC and OAuth 2.0 cover the modern JSON-based identity framework running underneath most authentication today. These three topics connect: SASE's ZTNA component uses SAML or OIDC to authenticate users at the POP. CASB uses OAuth 2.0 to connect to SaaS tenants. The logout flows and token revocation mechanisms in OIDC directly address the session persistence problems that create exposure when SAML alone isn't enough.

Part 3 of 3 in the SASE, SAML & Modern Identity series.