Keycloak SP-Initiated vs IdP-Initiated SAML SSO

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.8.0
libxml2-2.12.5-10.el10_2.2.x86_64
Applies to Any Linux host with Podman or Docker
Privilege Normal user (rootless Podman). Realm setup uses the Keycloak bootstrap admin
Scope How a Keycloak SAML login differs when it starts at the service provider versus when it starts at Keycloak's IdP-initiated client URL: which request exists, how the target application is selected, and what RelayState and InResponseTo look like. Includes the broker URL that accepts an unsolicited response. Does not cover OpenID Connect login initiation, single logout, or assertion signing.
Related guides What is Keycloak?
Keycloak realms
Keycloak SAML NameID
Map Keycloak roles and groups into SAML assertions
Authentication vs authorization
Short on time?Beta Get the gist of this 14-minute read in seconds, then ask follow-up questions

SP-initiated and IdP-initiated SAML are two places a login can start. SP means Service Provider, the application or system the user wants to access. IdP means Identity Provider, the system that authenticates the user. "SP-initiated" and "IdP-initiated" describe where the login starts, not which product owns the user account.

Start at the application and the service provider sends an AuthnRequest. Start at Keycloak's IdP-initiated client URL and the browser posts a SAMLResponse with no request to answer.

This test uses the two-realm login from Keycloak SAML SSO. Realm company is the identity provider. Realm apps is the broker that plays the service provider. The excerpts use ports 18093 and 18094.


SP-Initiated vs IdP-Initiated: Quick Difference

The identity provider authenticates the user in both flows. What changes is where the login starts.

SP-initiated IdP-initiated
Where the browser starts The application, here the apps account console /realms/company/protocol/saml/clients/{url-name}
Request the IdP receives SAMLRequest (AuthnRequest) None
Response type Response to an AuthnRequest Unsolicited SAML response
How the application is chosen Issuer and assertion consumer URL on that request The URL name configured on one SAML client
InResponseTo on the response Present, equal to the request ID Absent
RelayState Chosen by the service provider and echoed back The client's IdP-initiated relay state, a RelayState query parameter, or omitted

"Unsolicited" simply means the IdP sends a SAMLResponse without first receiving an AuthnRequest from that service provider. RelayState is a small opaque value carried alongside the SAML message. It is commonly used to remember application state, such as the page or transaction the user was working on.

SP-initiated                                    IdP-initiated
Browser opens the application                   Browser opens the client URL
        │                                               │
        ▼                                               ▼
AuthnRequest (SAMLRequest)                      No AuthnRequest
ACS URL and a request ID                        URL name selects the SAML client
        │                                               │
        ▼                                               ▼
User signs in at realm company                  User signs in at realm company
        │                                               │
        ▼                                               ▼
SAMLResponse with InResponseTo                  SAMLResponse with no InResponseTo
RelayState echoed from the request              RelayState from the client setting
Setting Value in this test
IdP Realm company on http://127.0.0.1:18093, container keycloak-saml-init-idp
SP/broker Realm apps on http://127.0.0.1:18094, container keycloak-saml-init-sp
SAML client ID http://127.0.0.1:18094/realms/apps
IdP-initiated URL name apps-portal
User alice / Alice!2026

SP-Initiated SAML Flow in Keycloak

Open the apps account console and choose Sign in with Company IdP. The apps realm builds an AuthnRequest, and the browser carries that request to the company realm using the configured SAML binding. The form field is SAMLRequest, plus a RelayState the broker created for this login.

These captures used HTTP-POST, so the parsed form values can be Base64-decoded directly. Redirect-binding messages need URL decoding and DEFLATE inflation, as shown in Decode Keycloak SAML. Save the SAMLRequest value from that POST and decode it. printf prints nothing when the XML file is written:

bash
printf '%s' "$SAML_REQUEST" | base64 -d | xmllint --format - > authn-request.xml

Read the request ID and the Assertion Consumer Service (ACS) from the opening element. The ACS is the service provider endpoint that receives the SAML response. Here it is the broker endpoint, not an application page:

bash
xmllint --xpath 'concat("ID=", string(/*/@ID), " ACS=", string(/*/@AssertionConsumerServiceURL))' authn-request.xml
output
ID=ID_73bb55f5-5227-49bd-987d-8ca6199ba908 ACS=http://127.0.0.1:18094/realms/apps/broker/company-idp/endpoint

The Destination on the same element is the company realm SSO URL, http://127.0.0.1:18093/realms/company/protocol/saml. That is the SSO endpoint published in the IdP metadata.

After Alice signs in, the company realm returns a form containing SAMLResponse, and the browser POSTs it to the ACS from the request. Decode that field the same way:

bash
printf '%s' "$SAML_RESPONSE_SP" | base64 -d | xmllint --format - > saml-response-sp.xml

InResponseTo on the response is the request id from the line above. The two values match, so this response answers that AuthnRequest and not some earlier login:

bash
xmllint --xpath 'string(/*/@InResponseTo)' saml-response-sp.xml
output
ID_73bb55f5-5227-49bd-987d-8ca6199ba908

RelayState on the response was the same value the request carried:

text
UFEluvk2pEIDIahEAtNLIbBPe6lcEkRPPTwKx60pTyY.05bhd3qB27Q.BkW69NjvSmiXXDXCRcCiqw

The apps realm accepted the POST. On the first login, Keycloak stops on Update Account Information. That form is the broker review step, not a rejected assertion. The messages above are from a later login that reached the account console.


Configure and Test IdP-Initiated Login

IdP-initiated SSO stays off until the SAML client has an IDP-Initiated SSO URL name. An empty name means Keycloak will not publish a client URL. The name is a single path segment with no spaces. It is not the SAML Client ID. It is only a short alias Keycloak uses in the IdP-initiated launch URL. IDP Initiated SSO Relay State is optional. When it is set, Keycloak sends that string as RelayState on the unsolicited response.

On the company realm client whose client ID is http://127.0.0.1:18094/realms/apps, set the URL name to apps-portal and the relay state to from-idp-portal.

SAML client settings with IDP-Initiated SSO URL name apps-portal and Relay State from-idp-portal

The browser URL is the company host, the realm, and that name:

text
http://127.0.0.1:18093/realms/company/protocol/saml/clients/apps-portal

A name that exists returns the company login page. Ask curl for the status only, because the body is the HTML login form:

bash
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18093/realms/company/protocol/saml/clients/apps-portal
output
200

A name that is not configured on any client in the realm returns HTTP 400. The page text is Client not found.

bash
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18093/realms/company/protocol/saml/clients/missing-client
output
400

Open the apps-portal URL in a browser that has no company session, sign in as Alice, and watch the next POST. There is no SAMLRequest. The body is SAMLResponse and RelayState=from-idp-portal. The POST target is the client's master SAML processing URL, which in this test is the broker endpoint http://127.0.0.1:18094/realms/apps/broker/company-idp/endpoint.

The normal broker ACS and the IdP-initiated broker URL do different jobs:

         SP-initiated broker endpoint
                      ↓
         /broker/company-idp/endpoint
                      ↓
expects a session from an earlier AuthnRequest

          IdP-initiated broker endpoint
                        ↓
 /broker/company-idp/endpoint/clients/<url-name>
                        ↓
routes an unsolicited login to a configured client

The apps page then says Invalid Request. The first URL is the ACS the broker puts on an AuthnRequest. An unsolicited response has no matching login in progress, so the broker rejects it. The company realm still issued a normal assertion. The broker rejected it because this ACS expects an in-progress SP-initiated login. The second URL is the one that accepts the same response, covered later.

Decode the posted SAMLResponse. printf prints nothing when the XML file is written:

bash
printf '%s' "$SAML_RESPONSE_IDP" | base64 -d | xmllint --format - > saml-response-idp.xml

Count InResponseTo on the Response element. Zero means Keycloak did not send the attribute:

bash
xmllint --xpath 'count(//*[local-name()="Response"]/@InResponseTo)' saml-response-idp.xml
output
0

SubjectConfirmationData on this response also has no InResponseTo. The SP-initiated response had one, and it matched the request id. Setting apps-portal did not remove the AuthnRequest. The account-console login above was captured with that URL name already set.


IdP-Initiated SSO URL and Target Client

The path segment after /protocol/saml/clients/ is not the SAML client ID. It is the URL name. apps-portal selected the client whose client ID is http://127.0.0.1:18094/realms/apps. missing-client matched nothing, which is why the page said Client not found.

Keycloak then picks the URL it will POST to, in this order:

  • Assertion Consumer Service POST Binding URL, when the fine-grain SAML endpoint is set
  • Otherwise the Master SAML Processing URL
  • Otherwise the Assertion Consumer Service Redirect Binding URL, and then the binding is redirect rather than POST

This apps-portal client had no fine-grain POST URL. The POST went to the Master SAML Processing URL, http://127.0.0.1:18094/realms/apps/broker/company-idp/endpoint.

RelayState is not taken from the page the user had open in the application. Three launches of the same client showed three different results:

  • Client field from-idp-portal, no query string: the POST contained RelayState=from-idp-portal
  • Same field, but the browser opened .../clients/apps-portal?RelayState=deep-link: the POST contained RelayState=deep-link
  • Field cleared, and no query string: the POST had no RelayState parameter

The query parameter overrides the saved field. An empty field does not send an empty RelayState. It omits the parameter. Treat RelayState as opaque application state. Do not assume Keycloak validates it as a safe redirect URL.


AuthnRequest, RelayState and InResponseTo Differences

The differences are easiest to see by comparing the two captures. Both used Alice and the company realm. Only the starting URL changed.

Check SP-initiated IdP-initiated (apps-portal)
SAMLRequest on the way to the company realm Present (AuthnRequest) Absent
Response Destination Broker endpoint from the request ACS Same broker endpoint, from the client's master URL
InResponseTo ID_73bb55f5-5227-49bd-987d-8ca6199ba908 Not present
RelayState Opaque broker value, echoed unchanged from-idp-portal

The SP-initiated RelayState is a handle the apps realm created for that login, and Keycloak echoed it. The application can use it to resume the page that started the login. The IdP-initiated value is the client field, unless the browser passes ?RelayState=. It does not point at a page the service provider chose when the user clicked sign-in.


Security and Application-State Trade-Offs

The two flows have different security and state-correlation properties. SP-initiated SSO gives the service provider an AuthnRequest ID that the returned InResponseTo can match. It can also send RelayState to preserve application state, such as where the user should return after login.

IdP-initiated SSO has no AuthnRequest ID to correlate, because no AuthnRequest started the flow. RelayState can still be present, but it does not provide the same request-response correlation as InResponseTo.

What both flows still validate:

  • The signature on the response or assertion
  • The audience
  • The destination
  • The assertion time conditions

A user who can authenticate at the company realm can open the client URL and cause a response to be posted to that client's ACS. This apps broker refused that POST on its normal ACS. A different service provider can accept the same kind of response.

Prefer SP-initiated when the application needs to correlate every response with a login request it created, or to preserve application-specific state from the start of the login. Use the client URL when the user should start at Keycloak and land on one configured ACS.


Advanced: IdP-Initiated SAML Through a Keycloak Broker

A normal IdP-initiated login posts straight to an application ACS. This section is the extra path for an unsolicited response that has to enter through a Keycloak broker. The /clients/{url-name} suffix is not required for every IdP-initiated login.

The Invalid Request page is what this Keycloak 26.8.0 broker does with an unsolicited response on /realms/apps/broker/company-idp/endpoint. That URL is the ACS for SP-initiated login. It is not the IdP-initiated broker endpoint.

Keycloak accepts an unsolicited response when the upstream identity provider posts it to this broker URL:

text
http://127.0.0.1:18094/realms/apps/broker/company-idp/endpoint/clients/portal

The two aliases in that login are different fields on different clients:

  • Upstream IdP launch alias: broker-portal, on the company realm
  • Broker target-client alias: portal, on the apps realm

portal in the URL above is the target client in the apps realm, not the company client's launch alias:

/broker/company-idp/endpoint/clients/portal
                                        ↑
                         target client in the apps realm
Alias Realm Where that hop posts the response
broker-portal company The broker URL ending in /clients/portal
portal apps http://127.0.0.1:18099/acs

The company client's Assertion Consumer Service POST Binding URL was the /clients/portal address, so the fine-grain POST URL won over the master URL. Opening http://127.0.0.1:18093/realms/company/protocol/saml/clients/broker-portal still produced a response with no InResponseTo. RelayState on that first POST was from-broker-portal, the company client's relay state. The destination was the /clients/portal URL, and the apps realm accepted it.

The two hops are separate SAML responses:

     User opens /clients/broker-portal
                     ↓
       company realm (upstream IdP)
                     ↓
         unsolicited SAMLResponse
                     ↓
/broker/company-idp/endpoint/clients/portal
                     ↓
        authenticate and link Alice
                     ↓
     new SAMLResponse from realm apps
                     ↓
        http://127.0.0.1:18099/acs

The broker accepted the unsolicited response and entered the first-broker-login flow, which showed Update Account Information. The first-login review belonged to the target SAML client in realm apps, http://127.0.0.1:18099/notes.

After Alice completed that step, realm apps started a separate IdP-initiated SAML login for its local SAML client and posted a new response to http://127.0.0.1:18099/acs. That second response is a new login, with the apps realm as the identity provider:

  • Issuer http://127.0.0.1:18094/realms/apps
  • No InResponseTo
  • RelayState notes-home, which is the relay state configured on the apps client named portal, not from-broker-portal

The first RelayState belonged to the company-to-broker hop. The second SAML response used the RelayState configured on the apps client. RelayState from the first hop does not automatically become RelayState on the second hop.


Troubleshoot IdP-Initiated Invalid Request

The Invalid Request text on the broker endpoint means the response arrived without an in-progress SP-initiated login. The company realm can still have issued a valid assertion. Read the POST target before changing signing or the user record.

Symptom Likely cause Fix
Page says Client not found and HTTP status is 400 The URL name is empty or does not match a client in that realm Set the IDP-Initiated SSO URL name and open /realms/{realm}/protocol/saml/clients/{that-name}
Broker page says Invalid Request In this Keycloak 26.8.0 broker test, an unsolicited response posted to /broker/{alias}/endpoint returned Invalid Request Start at the application, or set the ACS to /broker/{alias}/endpoint/clients/{url-name}
Service provider rejects a response that has no InResponseTo IdP-initiated responses omit InResponseTo Use SP-initiated when the service provider requires a request id, or allow unsolicited responses on that service provider
RelayState is not the page the user was viewing The value comes from the client field or from ?RelayState= Set the field, pass the query parameter, or start at the service provider when the application must choose the value
The POST lands on the wrong application Another client owns that URL name, or the ACS URL points elsewhere Confirm which client has the name. The response Destination is that client's ACS

On the broker, the server event for the rejected POST was IDENTITY_PROVIDER_LOGIN_ERROR with error invalidRequestMessage. That log line matches the Invalid Request page. It does not mean the URL name was wrong. A wrong URL name fails earlier, on the company host, with Client not found.


Summary

SP-initiated SAML starts at the application. The apps realm sent an AuthnRequest, the company realm answered it, and InResponseTo matched the request id. RelayState was the broker's own handle, and the response copied it back. IdP-initiated SAML starts at /realms/{realm}/protocol/saml/clients/{url-name}. There is no SAMLRequest and no InResponseTo. The URL name selects the client, and the client's ACS URL is where the response is posted.

RelayState on that path is the client setting, unless the browser adds ?RelayState=, which overrides the setting. Clear the field and omit the query parameter, and the POST does not include RelayState at all.

Posting the unsolicited response to the broker's normal ACS returned Invalid Request on Keycloak 26.8.0. The same company realm reached the broker when the ACS was /broker/company-idp/endpoint/clients/portal, where portal is the URL name of a client in the apps realm. After the first-login review, the apps realm posted a second response to that client's ACS, with its own relay state and still no InResponseTo. Prefer SP-initiated when the application needs to correlate every response with a login request it created. Use the client URL when users should start at Keycloak and land on one configured ACS.


References


Frequently Asked Questions

What is SP-initiated SSO?

SP-initiated SSO starts at the service provider. The service provider sends an AuthnRequest to the identity provider. The SAMLResponse that comes back includes InResponseTo set to that request's ID, and it echoes the RelayState the service provider sent.

How do I start IdP-initiated SSO from Keycloak?

Set the IDP-Initiated SSO URL name on the SAML client, then open {host}/realms/{realm}/protocol/saml/clients/{url-name}. An empty name disables the URL. A name that does not match a client returns Client not found.

Does IdP-initiated SSO send an AuthnRequest?

No. The browser opens the Keycloak client URL, the user signs in, and the browser POSTs a SAMLResponse to the client's ACS. There is no SAMLRequest, and the response has no InResponseTo.

What is RelayState used for?

RelayState is state the service provider wants back with the response, often the page or transaction that started the login. In an SP-initiated login, Keycloak echoes the value the service provider sent. In an IdP-initiated login, Keycloak sends the client's IDP Initiated SSO Relay State, or the RelayState query parameter if the browser supplied one. If the field is empty and the query parameter is absent, the POST omits RelayState.

Can Keycloak broker an IdP-initiated SAML login?

Yes, but not by posting the response to the broker's normal ACS. That endpoint expects an in-progress SP-initiated login and returns Invalid Request for an unsolicited response. Point the upstream client's Assertion Consumer Service POST Binding URL at {broker}/realms/{realm}/broker/{alias}/endpoint/clients/{url-name}, where {url-name} is the IDP-Initiated SSO URL name of a client in the broker realm. Keycloak 26.8.0 accepted that POST and then started an IdP-initiated login to that client.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)