Who starts a single sign-on flow decides which attacks the application can stop, yet most estates still accept logins that nobody at the application ever asked for. In SP-initiated SSO the application (service provider or relying party) creates a request, remembers it, and only accepts an answer that matches. In IdP-initiated SSO the identity provider pushes a response to the application unprompted, so the application has nothing to match it against. This guide compares the two flows across SAML 2.0 and OpenID Connect: the threat model of each, replay and login CSRF exposure, RelayState and target link handling, OIDC third-party initiated login, when to switch IdP-initiated off and how to do it without breaking portal users. It is written for IAM engineers, application owners and security architects. SAML message structure and validation basics are covered in the SAML Security Guide and are not repeated here.
Key takeaways
- The difference is request correlation. SP-initiated responses carry an InResponseTo (SAML) or state and nonce (OIDC) that the application checks against what it sent; an unsolicited SAML response by definition carries none.
- IdP-initiated SAML removes login CSRF protection. OWASP describes unsolicited responses as "inherently less secure by design due to the lack of login CSRF protection", and NIST SP 800-63C-4 says the transaction SHALL be initiated by the RP at FAL2.
- OIDC has no unsolicited ID token. Its equivalent, third-party initiated login, only asks the RP to start its own authentication request, so state, nonce and PKCE stay in force; the risk moves to the initiation endpoint and target_link_uri.
- RelayState and target_link_uri are redirect inputs. Validate them against an allowlist of the application's own paths, keep them short and free of sensitive state, or they become open redirects.
- You can keep portal tiles and drop unsolicited responses. Point the portal tile at the application's login URL so the click starts an SP-initiated flow, then disable unsolicited acceptance at the SP.
How the two flows differ
- SP-initiated SAML: the user opens the application, the SP builds an AuthnRequest with a unique ID, stores that ID in the user's pre-login session and redirects the browser to the IdP. The IdP authenticates the user and POSTs a response whose InResponseTo equals the request ID. OWASP notes that InResponseTo "helps guarantee authenticity of the response from the trusted IdP".
- IdP-initiated SAML: the user starts at the IdP portal and clicks an application tile. The SAML 2.0 Profiles specification (section 4.1.5) says an IdP "MAY initiate this profile by delivering an unsolicited <Response> message" and that such a response "MUST NOT contain an InResponseTo attribute". The SP has no pre-login session and no request to compare.
- SP-initiated OIDC: the RP sends an authorisation request with state (bound to a browser cookie for CSRF protection), nonce (OIDC Core: "to mitigate replay attacks") and, under current best practice, PKCE. The RP checks all three when the code comes back.
- Third-party initiated OIDC: OIDC Core section 4 lets an OP or another party send the user to the RP's registered initiate_login_uri with iss (required), login_hint and target_link_uri (both optional). The RP then sends a normal authentication request to that issuer. The final response is still solicited.
The practical consequence: in SAML, "IdP-initiated" changes what the SP must accept. In OIDC, "third-party initiated" only changes how the flow is kicked off. For a protocol-level comparison see the SAML vs OIDC Guide.
Side-by-side comparison
| Property | SP-initiated SAML | IdP-initiated SAML | OIDC (RP-initiated or third-party initiated) |
|---|---|---|---|
| Who starts the flow | Application | IdP portal or a link to the IdP | RP, or a third party calling initiate_login_uri, which makes the RP start |
| Request correlation | InResponseTo matches stored AuthnRequest ID | None; InResponseTo must be absent | state, nonce and PKCE code_verifier |
| Login CSRF protection | Yes, via pre-login session and InResponseTo | No built-in protection | Yes; RFC 9700 accepts state, PKCE or nonce as CSRF protection |
| Replay defence | Correlation plus assertion ID cache until NotOnOrAfter | Assertion ID cache only; no correlation | Single-use code, nonce check, short exp |
| Response injection | Unexpected responses rejected | Any validly signed, in-window response for the SP is accepted | Code is only redeemed by the client that holds the PKCE verifier |
| Post-login landing | SP stores the target itself | RelayState from the IdP (if agreed), untrusted | RP stores the target; target_link_uri from initiator is untrusted |
| Fit with NIST SP 800-63C-4 | Meets "initiated by the RP" | FAL1 only ("SHOULD be initiated by the RP") | Meets "initiated by the RP" |
Threat model: SP-initiated SSO
SP-initiated SSO is the stronger default, but it is not risk-free. Its protections hold only if the SP actually checks them, and many SAML libraries make InResponseTo validation optional or silently skip it when the session is missing.
- Main threats: stolen assertions replayed within their validity window, assertions minted for another SP (cross-SP replay), forged responses after IdP signing key theft (MITRE ATT&CK T1606.002 SAML Tokens), and session cookie theft after login (T1539 Steal Web Session Cookie). Request correlation does nothing against a forged response that the attacker injects into a legitimate pending flow, which is why Destination, Audience and signature checks remain essential.
- Failure modes: the SP stores the AuthnRequest ID in a cookie that is lost when the browser blocks third-party cookies or when the user has two tabs open, and the developer "fixes" the resulting errors by turning correlation off; the SP accepts a response with no InResponseTo when one was expected (this quietly turns every SP-initiated integration into an IdP-initiated one); the return URL stored before login is taken from a query parameter without validation.
- Controls: store the pending request ID server-side keyed to a SameSite pre-login cookie; reject any response whose InResponseTo is missing or unknown; keep an assertion ID cache until NotOnOrAfter, as SAML Profiles section 4.1.4.5 requires ("The service provider MUST ensure that bearer assertions are not replayed"); validate Destination and Recipient against the ACS URL; sign AuthnRequests where the IdP supports it so the IdP can reject requests for unregistered ACS URLs.
Threat model: IdP-initiated SSO
The SP receives a signed response it did not ask for. It can verify that the IdP issued it and that it is for this SP and still in date, but not that this browser, at this moment, meant to log in.
- Login CSRF: an attacker obtains a valid response for an account they control (for example their own low-privilege account at a shared-tenant SaaS application) and causes the victim's browser to POST it to the SP's ACS URL. The victim is silently logged in as the attacker and may upload data or enter credentials into the attacker's account. OWASP explains that the SP "has no opportunity to create a pre-login session or verify that the authentication request was intentionally initiated by the user". MITRE CAPEC-62 Cross Site Request Forgery describes the general pattern; see the CSRF Prevention Guide.
- Replay and message insertion: without correlation, a captured response (from logs, a proxy, browser history of an auto-submitting form or a compromised endpoint) can be posted again from another browser until NotOnOrAfter, unless the SP enforces an assertion ID cache. OWASP lists replay and message insertion as the attacks the SAML Profiles 4.1.5 validation counters (CAPEC-60 Reusing Session IDs covers the general replay class).
- RelayState open redirect: with unsolicited responses the IdP chooses the landing page through RelayState. If the SP redirects to whatever RelayState contains, the trusted login domain becomes an open redirect for phishing. See the Open Redirect Guide.
- Failure modes: no replay cache because developers rely on correlation they do not have; RelayState used to carry tenant or role hints that the SP trusts.
- Controls if it must stay: OWASP's three considerations: follow the SAML Profiles 4.1.5 validation, allowlist RelayState when it is a URL, and implement replay detection at response or assertion level. Add short NotOnOrAfter windows, OneTimeUse where honoured, and treat unsolicited sign-ins as a separate, monitored category.
OIDC equivalents and third-party initiated login
OpenID Connect has no profile in which the OP sends an unsolicited ID token to the RP's redirect URI. Any response that arrives without a matching state and nonce should be rejected. What OIDC offers instead is third-party initiated login (OIDC Core section 4), which gives portal-style launch without unsolicited responses.
| Parameter | Status in OIDC Core section 4 | Security handling at the RP |
|---|---|---|
| iss | Required; must use the https scheme | Accept only issuers the RP is configured to trust. An unknown issuer must not trigger discovery or dynamic registration from the initiation endpoint. For multi-issuer RPs, also validate the iss authorisation response parameter (RFC 9207) to prevent mix-up. |
| login_hint | Optional; free-form hint for the OP | Pass through only as a hint. Never use it to choose the local account or skip authentication. |
| target_link_uri | Optional; where the RP is asked to send the user after login | The spec says RPs "MUST verify the value of the target_link_uri to prevent being used as an open redirector to external sites". Allowlist paths on the RP's own origin. |
| Transport | GET query parameters or auto-submitted POST form | Apply clickjacking defences (the spec points to frame busting and RFC 6819 section 4.4.1.9); see the Clickjacking Guide. |
| Unknown parameters | Extensions may add parameters; clients ignore what they do not understand | Do not let extension parameters alter scopes, prompt or acr_values without a server-side policy check. |
Residual risk: anyone can link to initiate_login_uri, so a hostile page can start a login at the user's real OP. Because the user authenticates as themselves and the RP still checks state and nonce, this does not produce the account-swap login CSRF seen with unsolicited SAML. It can still be used for nuisance logins, consent phishing when combined with broad scopes, or redirects if target_link_uri is not validated. RFC 9700 adds two related requirements: clients "MUST prevent Cross-Site Request Forgery", and they "MUST NOT expose URLs that forward the user's browser to arbitrary URIs". Implicit flow responses (response_type containing token or id_token without code) are the closest OIDC gets to unsolicited delivery risk; RFC 9700 says clients SHOULD NOT use the implicit grant. For flow design detail see the OAuth 2.0 and OpenID Connect Guide.
RelayState and replay controls in practice
RelayState is where IdP-initiated deployments most often go wrong, and it matters in SP-initiated SAML too. The SAML 2.0 Bindings specification (sections 3.4.3 and 3.5.3) sets the rules:
- Size: RelayState "MUST NOT exceed 80 bytes in length". Long URLs in RelayState are non-conformant and are a sign that the SP is round-tripping data it should hold itself.
- Integrity: it "SHOULD be integrity protected by the entity creating the message"; because signing is impractical at 80 bytes, the specification suggests a checksum or pseudo-random value. In SP-initiated flows the best pattern is an opaque random handle that maps to a server-side record of the return path.
- Binding to the message: section 3.5.5.2 warns there is no mechanism protecting the relationship between the SAML message and RelayState, so an attacker could swap RelayState between two valid responses. Producers and consumers "MUST take care not to associate sensitive state information" with it.
- Disclosure: SAML Profiles section 4.1.3.1 says the SP "SHOULD reveal as little of the request as possible in the RelayState value".
Replay controls apply whichever way the flow starts:
| Control | What to configure | Attack it stops |
|---|---|---|
| Assertion ID cache | Store each accepted assertion ID until its NotOnOrAfter; reject repeats across all SP nodes (shared cache, not per-node memory) | Replay of a captured response |
| Short validity window | IdP issues assertions valid for minutes, not hours; SP allows small clock skew only | Replay of old responses from logs or browser history |
| Destination and Recipient match | Reject a response if Destination is missing or differs from this ACS URL | Cross-SP replay (OWASP: "prevents cross-SP assertion replay") |
| Audience restriction | Audience equals this SP's entity ID | Assertions minted for another application |
| InResponseTo enforcement | Required for every SP-initiated flow; separate code path for unsolicited | Injection of unsolicited or captured responses |
| RelayState allowlist | Exact paths or a fixed set of route names on the SP's own origin | Open redirect after login |
When to disable IdP-initiated SSO
Disable unsolicited response acceptance at the SP by default, and keep it only with a documented reason, an owner and an expiry date. Use these criteria:
- Disable now: applications that hold sensitive data or admin functions; any application that must meet FAL2 or above under NIST SP 800-63C-4; multi-tenant SaaS where an attacker can easily obtain their own valid account (the login CSRF payoff is highest here); applications whose SAML library cannot keep a shared replay cache.
- Convert, then disable: applications launched from an IdP portal or dashboard. Change the tile so it links to the application's own login URL (sometimes called a sign-on URL or bookmark launch). The click then starts SP-initiated SSO and the user still gets one-click access, usually with an invisible extra redirect.
- Keep with compensating controls: legacy applications that only implement unsolicited responses (OWASP notes support persists "for backward compatibility (notably with SAML 1.1)"), and partner integrations where the partner's SP cannot issue AuthnRequests. Apply replay caching, short windows, RelayState allowlisting, stronger monitoring and a migration date.
- Small vs large organisations: a small organisation with a handful of SaaS applications can usually switch every tile to SP-initiated in a day using each vendor's documented sign-on URL. A large estate needs an inventory first, because hundreds of custom SPs will have been built with different libraries and some will not handle SP-initiated at all.
Worked scenario: removing unsolicited SAML from a portal estate
A company has 140 SAML applications launched from its IdP portal. An inventory shows 120 accept unsolicited responses. Thirty of those are multi-tenant SaaS products, twelve are internally built on two different SAML libraries, and one partner benefits platform only supports IdP-initiated.
- Triage: the SaaS applications are checked against their vendor documentation: 27 publish an SP-initiated login URL and an option to reject unsolicited responses. Their tiles are switched to the login URL in a pilot group, then unsolicited acceptance is turned off at the SP.
- Internal applications: one library enforces InResponseTo when it finds a stored request ID but silently accepts responses without one. The team adds a configuration flag that rejects responses lacking InResponseTo, moves the request ID store from in-memory to the shared session store (behind a load balancer the in-memory store caused intermittent failures, which is why correlation had been disabled), and adds an assertion ID cache.
- Exceptions: the benefits platform keeps IdP-initiated with a two-minute assertion lifetime, a RelayState allowlist of three paths, a replay cache confirmed by the provider in writing, and a contract clause to support SP-initiated by a fixed date.
- Result: unsolicited-capable applications fall from 120 to 4 in a quarter, with no help desk spike, because users kept the same tiles.
Implementation and rollback
- Inventory: for each SAML SP and OIDC RP record the initiation modes accepted, whether InResponseTo or state and nonce are enforced, how RelayState or target_link_uri is handled, the library and version, and the owner.
- Measure current use: from IdP sign-in logs, split launches by portal versus direct; from SP logs, count responses with and without InResponseTo. Applications with zero unsolicited logins can be switched immediately.
- Pilot: change tiles to SP-initiated launch for a pilot ring of users, watch error rates for a week, then extend.
- Enforce at the SP: reject unsolicited responses and responses with missing correlation. Changing the IdP tile alone is not enough; an attacker does not use your portal.
- Harden what remains: replay cache, short lifetimes, RelayState allowlists, monitored exception list with expiry.
- Rollback: keep the previous SP configuration exported and versioned; reverting the tile URL restores the old launch path within minutes. Roll back a single application at a time rather than reverting the policy estate-wide. See IdP configuration backup and restore for Entra ID and Okta.
Detection and measurement
- Log at the SP: whether each accepted response was solicited, the assertion ID, Issuer, Destination, RelayState value and the result of each check. Alert on replayed assertion IDs, responses with unknown InResponseTo, and RelayState values outside the allowlist.
- Correlate with the IdP: every SP sign-in should match an IdP issuance event for the same user and application. Unmatched SP sign-ins can indicate forged responses; see the Golden SAML and Federation Trust Abuse Defence Guide.
- Metrics: share of SAML SPs that reject unsolicited responses (target above 95 percent); count of approved IdP-initiated exceptions past their expiry date (target zero); share of SPs with a shared replay cache verified by test; replayed or uncorrelated responses rejected per month (non-zero is useful signal, investigate each); OIDC RPs with target_link_uri and redirect validation verified by test.
Common failure patterns
- Changing the tile but not the SP: users now launch SP-initiated, but the SP still accepts unsolicited responses, so the attack surface is unchanged.
- Correlation disabled to fix intermittent errors: request IDs held in per-node memory or third-party cookies fail behind load balancers or in modern browsers; the fix is a shared server-side store, not switching checks off.
- RelayState used as a full URL with no validation: the login endpoint becomes an open redirect on a domain users trust.
- Replay cache only in memory per node: a captured response posted to a different node is accepted.
- Treating OIDC third-party login as unsolicited: developers add code paths that accept an ID token without state or nonce "for portal launches", recreating the SAML weakness in OIDC.
Framework mapping
| Framework | Reference | Relevance to initiation choice |
|---|---|---|
| NIST SP 800-63C-4 | FAL1, FAL2, FAL3 | FAL1: transaction SHOULD be initiated by the RP; FAL2: SHALL be initiated by the RP and assertions strongly protected from injection; replay protection required per RP. See the NIST SP 800-63C-4 Federation Assurance Levels Guide. |
| NIST SP 800-53 Rev. 5 | IA-8, IA-8(4), SC-23, SI-10 | Authenticating non-organisational users through defined federation profiles; session authenticity against CSRF and replay; validating RelayState and target_link_uri input |
| NIST CSF 2.0 | PR.AA-03 | Users, services and hardware are authenticated |
| MITRE ATT&CK | T1606.002, T1539, T1550.004 | Forged SAML tokens; theft and reuse of post-login session cookies |
| MITRE CAPEC | CAPEC-62, CAPEC-60 | Login CSRF; replay of captured session material |
| IETF | RFC 9700, RFC 9207 | OAuth CSRF, redirect and mix-up requirements for OIDC RPs |
Practitioner checklist
- Record the accepted initiation modes for every SAML SP and OIDC RP, with owner and library version.
- Switch portal tiles to the application's own login URL so clicks start SP-initiated flows.
- Configure every SP to reject unsolicited responses and responses with missing or unknown InResponseTo, unless an approved exception exists.
- Store pending request IDs and the post-login return path server-side, shared across nodes.
- Run a shared assertion ID replay cache until NotOnOrAfter and keep assertion lifetimes short.
- Allowlist RelayState and target_link_uri to the application's own paths; keep RelayState under 80 bytes and opaque.
- For OIDC, require state, nonce and PKCE on every flow, including those started via initiate_login_uri, and validate iss against trusted issuers.
- Give each IdP-initiated exception a reason, compensating controls and an expiry date, and report the count monthly.
Standards and references
- OASIS: Profiles for the OASIS Security Assertion Markup Language (SAML) V2.0
- OASIS: Bindings for the OASIS Security Assertion Markup Language (SAML) V2.0
- OpenID Foundation: OpenID Connect Core 1.0
- NIST: SP 800-63C-4 Digital Identity Guidelines, Federation and Assertions
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- IETF: RFC 9207 OAuth 2.0 Authorization Server Issuer Identification
- OWASP: SAML Security Cheat Sheet
- MITRE ATT&CK: T1606.002 SAML Tokens
Related NHI Mgmt Group resources: SAML Security Guide · SAML vs OIDC Guide · SAML to OIDC Migration · Identity Provider and SSO Security Guide · Open Redirect Guide