Enterprise SSO and SCIM: A Security Checklist That Goes Past "We Have SAML"

SSO moves the risk, it does not remove it

Single sign-on is the right default for enterprises. One place to enforce MFA, one place to disable a leaver, one audit trail. But it also means every application now trusts a signed blob from the identity provider (IdP). If an application validates that blob badly, an attacker does not need a password at all: they need a crafted assertion.

Most SSO vulnerabilities found in the wild are not in the IdP. They are in the service provider (SP): the application consuming the assertion, often using a library configured with defaults that were never reviewed. This checklist is for both sides: security teams buying SaaS, and engineering teams building the SP.

SAML: the assertion checklist

A SAML response is XML, signed with XML Signature, and XML Signature is notoriously easy to get wrong. Go through these in order.

Signature validation

  • Require a signature on the assertion (or on the whole response, covering the assertion). Reject unsigned assertions, always. Several historical bypasses came from SPs that validated a signature if present.
  • Validate against a pinned IdP certificate from metadata you configured, never against a certificate embedded in the incoming message. The KeyInfo element in the response is attacker-controlled.
  • Make sure you read what you verified. XML signature wrapping attacks insert a second, unsigned assertion; the library verifies the signed one while the application reads attributes from the other. Use a maintained library that returns the verified node, and read identity only from it.
  • Reject weak algorithms. SHA-1 based signature and digest methods should be refused unless a legacy IdP genuinely forces them, and then tracked as debt.
  • Disable DTD processing and external entities in the XML parser. XXE in a pre-authentication endpoint is about as bad as it gets.

Inspect the IdP signing certificate you are pinning (issuer, validity, key size) with the X.509 decoder, and put its expiry in a calendar. Expired signing certificates cause outages that tempt people to switch validation off "temporarily".

Audience, recipient, destination

Field Check Why
Audience in AudienceRestriction Equals your SP entity ID exactly Stops an assertion issued for another app at the same IdP from being replayed to yours
Recipient in SubjectConfirmationData Equals your Assertion Consumer Service URL Binds the assertion to this endpoint
Destination on the response Equals your ACS URL Same, at the response level
Issuer Equals the configured IdP entity ID Stops a different IdP (or tenant) from asserting identities
InResponseTo Matches an AuthnRequest ID you issued and have not consumed Ties the response to a login you started (SP-initiated)

The audience check is the one most often missing in homegrown integrations. In a multi-tenant IdP, every app shares the same signing key; without an audience check, any assertion for any app is valid for yours.

Time and replay

  • Enforce NotBefore and NotOnOrAfter on both Conditions and SubjectConfirmationData.
  • Allow a small clock skew, typically a few minutes at most. Generous skews (an hour, a day) seen in the wild turn a short-lived assertion into a long-lived token.
  • Cache consumed assertion IDs until they expire and reject repeats. Without this, an assertion captured from a browser history, proxy log or shared screen can be replayed within its validity window.
  • Keep NTP healthy on SP servers. Skew problems are often fixed by widening tolerance instead of fixing time sync.

Never "fix" a validation error by turning the check off. Audience mismatch, expired assertion, unknown issuer: each of these is the control working. Fix the configuration, and log every rejection with the reason so you can tell a misconfiguration from an attack.

IdP-initiated SSO: convenient, weaker

In SP-initiated flow, the user starts at the application, which sends an AuthnRequest; the response comes back with InResponseTo, and the SP can verify it asked for it. In IdP-initiated flow, the user clicks a tile in the IdP portal and the IdP posts an unsolicited response to the SP.

That unsolicited response has no InResponseTo, so the SP cannot bind it to a session it started. Consequences:

  • Login CSRF and session injection: an attacker can cause a victim's browser to post the attacker's valid assertion, logging the victim into the attacker's account, where anything the victim enters becomes visible to the attacker.
  • Replay is harder to rule out, which makes the assertion ID cache mandatory rather than nice to have.
  • RelayState is often used as a post-login redirect, which becomes an open redirect if not allow-listed.

If business users want portal tiles, a safer pattern is to have the tile hit an SP URL that immediately starts an SP-initiated flow. Users see no difference.

OIDC: the token checklist

OpenID Connect avoids XML, but it brings its own sharp edges. The ID token is a JWT, and you can inspect any test token with the JWT decoder to see exactly which claims your IdP sends. Our JWT security guide covers the token format in depth; for SSO specifically:

  • Use the authorization code flow with PKCE. The implicit flow is deprecated in current OAuth security guidance.
  • Validate state (CSRF protection for the redirect) and nonce (binds the ID token to this login).
  • Verify the signature using the IdP's JWKS, with an algorithm allow-list. Reject alg: none and never let the token header choose between symmetric and asymmetric verification.
  • Check iss equals the expected issuer and aud contains your client ID. In multi-tenant IdPs, validate the tenant identifier too, not just the generic issuer pattern.
  • Check exp, iat, and apply the same small skew as SAML.
  • Register exact redirect URIs. No wildcards.

Identify users by stable IDs, not email

The most common logic flaw in both SAML and OIDC integrations is matching users by email address. Email is often a mutable, user-editable attribute at the IdP, and in multi-tenant IdPs another tenant can assert any email it likes. Key accounts on the immutable subject identifier (sub plus iss in OIDC, a persistent NameID or immutable object ID attribute in SAML). If you must link existing accounts by email, only trust it when the IdP marks it verified and the domain is one you have explicitly bound to that IdP connection.

Provisioning: JIT versus SCIM

Getting users into applications is easy. Getting them out is where SSO deployments fail.

Just-in-time (JIT) provisioning creates the account on first login from assertion attributes. It is simple, but:

  • it never removes anyone; a leaver's account stays until someone cleans up
  • role and group attributes are only refreshed at login, so a demoted admin stays admin until they log in again
  • anyone the IdP will authenticate can create an account, so scope the SSO app assignment tightly at the IdP

SCIM (System for Cross-domain Identity Management, RFC 7643 and 7644) lets the IdP push creates, updates and deactivations to the application through an API. It is the only reliable way to deprovision.

{
  "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
  "Operations": [
    { "op": "replace", "path": "active", "value": false }
  ]
}

The deprovisioning checklist

Disabling the IdP account is necessary but not sufficient. For each application, verify that deactivation via SCIM (or your offboarding process) also:

  • [ ] Terminates existing sessions. Many apps keep long-lived sessions; a disabled user stays logged in for days.
  • [ ] Revokes API tokens, personal access tokens and OAuth grants the user created. These do not go through SSO at all.
  • [ ] Removes local credentials if the app ever allowed password login alongside SSO.
  • [ ] Removes group-derived permissions when group membership changes, not only on full deactivation.
  • [ ] Transfers ownership of shared resources (integrations, service accounts, scheduled jobs) before deactivation breaks them.

Also secure the SCIM endpoint itself: it can create admins. Use a dedicated, long random bearer token stored only at the IdP, rotate it, restrict source IPs to the IdP's published ranges where the vendor supports it, and log every SCIM call.

Enforce SSO, then close the side doors

An app with SSO enabled but password login still allowed is not an SSO app. Check:

  • Password login disabled for all users in the SSO domain once SSO is enforced.
  • Password reset flows cannot re-enable local login.
  • Domain verification is required before a tenant can claim SSO for a domain.
  • MFA at the IdP applies to the SSO app (conditional access policies sometimes exclude apps by accident).
  • Admin consoles of the SaaS vendor are behind SSO too, not a separate local admin.

Break-glass accounts

If the IdP is down, misconfigured, or compromised, you need a way in. Break-glass accounts are that way, and they are high-value targets.

  • Keep two per critical system (IdP tenant, cloud root, core SaaS), so one lockout is not fatal.
  • Exclude them from SSO and from conditional access policies that depend on the IdP, but protect them with phishing-resistant MFA (hardware security keys stored in separate physical locations).
  • Use long random passwords held in a sealed or split-custody process.
  • Alert on every sign-in. A break-glass login outside a declared incident or test is an incident.
  • Test them on a schedule (quarterly is common) and document the test. A break-glass account nobody has used in two years tends to fail when it is finally needed.

What to do Monday

  1. List every SSO-connected app and record: protocol, SP- or IdP-initiated, JIT or SCIM, password login still enabled or not.
  2. For homegrown or less mature SPs, test audience, issuer, signature-required and replay handling with modified assertions in a staging environment.
  3. Find every app that matches users by email and plan the move to immutable IDs.
  4. Run an offboarding test with a test user: disable at the IdP and check each app for live sessions and tokens one hour later.
  5. Put IdP signing certificate expiry dates in your monitoring.
  6. Confirm break-glass accounts exist, are alerted on, and were tested this quarter.

SSO is a single point of trust. Treat its validation logic with the same suspicion you apply to any other authentication code, because that is exactly what it is.