When using SAML, we have two methods of starting Single Sign-On (SSO).
- Service Provider (SP) initiated SSO involves the SP creating a SAML request, forwarding the user and the request to the Identity Provider (IdP), and then, once the user has authenticated, receiving a SAML response and assertion from the IdP. A login button would typically initiate this flow within the SP.
- Identity Provider (IdP) initiated SSO involves the user clicking on a button in the IdP and then being forwarded to an SP along with a SAML message containing an assertion. This flow would typically be initiated by a page within the IdP that shows a list of all available SPs that a user can log into.
Our SAML Service Provider Component supports both SP and IdP-initiated SSO. This page covers how you can handle IdP-initiated SSO in your Service Provider implementation.
We recommend avoiding IdP-initiated SSO whenever possible, as this flow is insecure. In this flow, the SP accepts unsolicited messages from the IdP. You can read more on this in our article The Dangers of SAML IdP-Initiated SSO.
Handling IdP-Initiated SSO
You will need to explicitly turn on the IdP-initiated SSO feature for an IdP by setting the configuration option AllowIdpInitiatedSso
to true
.
In the IdP-initiated SSO, our SAML authentication handler will receive a SAML response from the external provider at your Assertion Consumer Service (ACS) endpoint, which is defined by the CallbackPath
. We will sign the user into the given SignInScheme
and redirect to the IdPInitiatedSsoCompletionPath
.
If the IdPInitiatedSsoCompletionPath
configuration option is not set, the user will be redirected to the root page after sign-in.
.AddSaml2p("saml2p", options =>
{
// Other configuration code removed for brevity
// your ACS endpoint
options.CallbackPath = "/signin-saml";
options.AllowIdpInitiatedSso = true;
// Optional - Defaults to "/"
options.IdPInitiatedSsoCompletionPath = "/sso-completion";
})
IdentityServer as an SP
If your IdentityServer acts as an SP to an external identity provider, we recommend following our IdentityServer as a SAML SP quickstart.
A temporary cookie is used as the SignInScheme
, and the IdPInitiatedSsoCompletionPath
is set to the callback function that performs custom business logic to map claims and link the external user to a local user.
It's important to note that in the IdP-initiated SSO, the SAML scheme is not challenged.
Instead, your IdentityServer receives unsolicited assertions from the external provider directly at your SAML ACS endpoint.
This means the IdentityServer ExternalLogin Challenge function is never invoked.
You must set the IdPInitiatedSsoCompletionPath
to the external Callback function.
We will sign the user into the given SignInScheme
and redirect to the IdPInitiatedSsoCompletionPath
.
services.AddAuthentication()
.AddSaml2p("saml2p", options =>
{
// Other configuration code removed for brevity
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.AllowIdpInitiatedSso = true;
options.IdPInitiatedSsoCompletionPath = "/external/callback";
});
The IdentityServer quickstart challenges the external scheme with a scheme
authentication property, which is later used within the Callback function.
Unfortunately, as the Challenge function is never invoked in IdP-initiated SSO, this property will be missing in the Callback function.
You may be able to determine the scheme from the external identity.
For example, use one of the user claims or the authentication type the external provider sends you.
This entirely depends on the agreement between you and your provider.
However, you must ensure that this value is persistent over time.
// External Callback
public async Task<IActionResult> OnGet()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
var externalUser = result.Principal;
var isSchemePresent = result.Properties.Items.TryGetValue("scheme", out var provider);
if (!isSchemePresent)
{
/* if the scheme is not present, determine the scheme from the external identity.
This depends on the agreement between you and your provider. For example:
provider = externalUser?.FindFirst("idp")?.Value;
provider = externalUser?.Identity.AuthenticationType; */
}
// Other code removed for brevity
}
Handling IdP-initiated RelayState
In IdP-initiated SSO, the RelayState
parameter can be used to indicate how to handle subsequent interactions with the user agent. This can be a URL to redirect to after authentication.
However, due to the insecurities of this flow, it cannot be trusted.
As a result, we enforce strict validation of relay states, only allowing known values.
We introduced the AllowedIdpInitiatedRelayStates
configuration option, which allows you to define the known valid values for the RelayState
.
If the relay state value sent by the IdP is valid, it will be returned in the authentication properties after a successful sign-in.
.AddSaml2p("saml2p", options =>
{
// Other configuration code removed for brevity
options.AllowedIdpInitiatedRelayStates = new List<string> { "value 1", "value 2" };
})
If you are using a version prior to Rsk.Saml v4.2.0, you will need to extract the RelayState
value from the HTTP Request yourself. You can access the incoming request form/parameters inside of Saml2pAuthenticationOptions.Events.OnTicketReceived
.
For example, if the SAML Response is sent using HTTP Redirect, you can get the RelayState
value from the query string:
.AddSaml2p("saml2p", options =>
{
// Other configuration code removed for brevity
options.Events = new RemoteAuthenticationEvents()
{
OnTicketReceived = context =>
{
context.Request.Query.TryGetValue("RelayState", out var relayStateValue);
// do something with relayStateValue
return Task.FromResult(0);
}
};
});