| Tested on | Red Hat Enterprise Linux 10.2 (Coughlan) |
|---|---|
| Package | podman-5.8.2-5.el10_2.x86_64quay.io/keycloak/keycloak:26.8.0libxml2-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 |
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:
printf '%s' "$SAML_REQUEST" | base64 -d | xmllint --format - > authn-request.xmlRead 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:
xmllint --xpath 'concat("ID=", string(/*/@ID), " ACS=", string(/*/@AssertionConsumerServiceURL))' authn-request.xmlID=ID_73bb55f5-5227-49bd-987d-8ca6199ba908 ACS=http://127.0.0.1:18094/realms/apps/broker/company-idp/endpointThe 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:
printf '%s' "$SAML_RESPONSE_SP" | base64 -d | xmllint --format - > saml-response-sp.xmlInResponseTo 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:
xmllint --xpath 'string(/*/@InResponseTo)' saml-response-sp.xmlID_73bb55f5-5227-49bd-987d-8ca6199ba908RelayState on the response was the same value the request carried:
UFEluvk2pEIDIahEAtNLIbBPe6lcEkRPPTwKx60pTyY.05bhd3qB27Q.BkW69NjvSmiXXDXCRcCiqwThe 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.
The browser URL is the company host, the realm, and that name:
http://127.0.0.1:18093/realms/company/protocol/saml/clients/apps-portalA name that exists returns the company login page. Ask curl for the status only, because the body is the HTML login form:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18093/realms/company/protocol/saml/clients/apps-portal200A name that is not configured on any client in the realm returns HTTP 400. The page text is Client not found.
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18093/realms/company/protocol/saml/clients/missing-client400Open 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 clientThe 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:
printf '%s' "$SAML_RESPONSE_IDP" | base64 -d | xmllint --format - > saml-response-idp.xmlCount InResponseTo on the Response element. Zero means Keycloak did not send the attribute:
xmllint --xpath 'count(//*[local-name()="Response"]/@InResponseTo)' saml-response-idp.xml0SubjectConfirmationData 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 containedRelayState=from-idp-portal - Same field, but the browser opened
.../clients/apps-portal?RelayState=deep-link: the POST containedRelayState=deep-link - Field cleared, and no query string: the POST had no
RelayStateparameter
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:
http://127.0.0.1:18094/realms/apps/broker/company-idp/endpoint/clients/portalThe 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/acsThe 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 RelayStatenotes-home, which is the relay state configured on the apps client namedportal, notfrom-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
- Keycloak 26.8.0 Server Administration: IdP Initiated login describes the client URL, the relay-state query parameter, ACS selection order, and the broker endpoint
/broker/{idp-name}/endpoint/clients/{client-id}. - Keycloak 26.8.0 Server Administration: SAML clients documents the IDP-Initiated SSO URL name and IDP Initiated SSO Relay State fields.
- SAML V2.0 Profiles section 4.1.5 defines unsolicited responses, which are responses sent without a preceding
AuthnRequest.
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.

