SamlAuthenticationHandler Challenge redirectUri

Hello,

I am in the process of implementing SAML auth with Identity Server 4 as a Service Provider. I have everything setup and working properly except for the handling of the External Login callback after authenticating with and external idp.

It seems no matter what I pass in the AuthenticationProperties object that is passed to the ChallengeResult, after I successfully login with the Idp I am always redirected to the default callback url which is: /Identity/Account/ExternalLogin?handler=Callback.

Here is my code for setting up the AuthenticationProperties and ChallengeResult:

var props = new AuthenticationProperties()
{
RedirectUri = Url.Action(“ExternalLoginCallback”),
Items =
{
{ “returnUrl”, returnUrl },
{ “scheme”, provider }
}
};
return new ChallengeResult(provider, props);


based on that code I see the following in the HandleChallengeAsync method of the SamlAuthenticationHandler for the AuthenticationProperties object:
Items:
{[.redirect, /account/ExternalLoginCallback]}
{[returnUrl, /connect/authorize/callback…}
{[scheme, saml2-okta-idsrv]}

And the redirectUri is “/account/ExternalLoginCallback”.

Not only am I not getting redirected back to the right callback url, but the Items I provided in the authentication properties (scheme and returnUrl) are not available either.

When I put in a route for the default callback url I am able to inspect the authentication result with this code:

var result = await HttpContext.AuthenticateAsync(_appConfiguration.Value.ExternalCookieAuthenticationSchemeEnvironment)


That result comes back with Succeeded = true and I see that the claims from the principal are correct. But the result properties has the following:
redirectUri = /Identity/Account/ExternalLogin?handler=Callback
Properties.Items:
{[LoginProvider, saml2-okta-idsrv]}
{[.redirect, /Identity/Account/ExternalLogin?handler=Callback]}
{[.issued, Thu, 06 Dec 2018 00:03:46 GMT]}
{[.expires, Thu, 20 Dec 2018 00:03:46 GMT]}

Shouldnt the authentication properties include the proper redirect and the extra items I provided when passed into the ChallengeResult?

Here is my code where I am setting up the saml provider

builder.AddSaml(externalIdentityProviderModel.AuthenticationScheme,
externalIdentityProviderModel.DisplayName ?? “”,
options =>
{
options.SignInScheme = configurationOptions.Value.ExternalCookieAuthenticationSchemeEnvironment;
options.PartnerName = () => externalIdentityProviderModel.SamlConfig.Name;
});


I know that I can provide a LoginCompletionUrl setting in the options as well, and while that does override the default url and redirect me to where I want to go, it still does not have the extra Items that I provided in the AuthenticationProperties (returnUrl, and scheme).

In my search for answers I also came across this forum post that seems to be the same issue as I am having, if it helps any.
https://www.componentspace.com/Forums/9181/RelayState-is-overwritten-by-SamlAuthenticationHandler

Can you tell me if this is a bug or if I am doing something wrong.

Let me know if you need any other information from me.

Thanks

Once SSO completes, the SAML authentication handler will redirect to the URL specified by AuthenticationProperties.RedirectUri.
So in this case it should redirect to /account/ExternalLoginCallback.
Please enable SAML trace and send the generated log file as an email attachment to support@componentspace.com mentioning your forum post.
https://www.componentspace.com/Forums/7936/Enabling-SAML-Trace

Thanks for the log.
We use a SAML session cookie to keep track of state associated with SAML SSO.
Two separate sub-domains were being used and the SAML session cookie wasn’t being presented and so the session state was effectively lost.
Specifying a Domain in the DistributedSsoSessionStoreOptions.CookieOptions resolved the issue.

I seem to be having the same problem.
Our setup is as follows:
SP-initiated login, using an OpenIdConnect test client, an IdentityServer4 IdP, which in it’s turn connects using SAML to an external IdP.
The test client and the IdP runs on the same domain, but on different sub domains. The external IdP is -of course- a completely different domain.

OpenIdConnect test client: https://testclient.dev.mydomain.nl
IdP: https://auth.dev.mydomain.nl
External IdP: https://dummy-auth.dev.myExternalIdPdomain.nl

After the succesful login in the external IdP, a post is done to https://auth.dev.mydomain.nl/SAML/AssertionConsumerService.
But then there is always a redirect to “/Identity/Account/ExternalLogin?handler=Callback” .

However, because we don’t want to use the “Identity” part in the path, I have configured in the Startup class:

services.Configure(options =>
{
options.CookieOptions = new ComponentSpace.Saml2.Bindings.CookieOptions()
{
Domain = “mydomain.nl”
};
});

And:

services.AddAuthentication().AddSaml(options =>
options.LoginCompletionUrl = relayState => !string.IsNullOrEmpty(relayState)
? QueryHelpers.AddQueryString(“/Account/ExternalLogin?handler=Callback”, “ReturnUrl”, relayState)
: “/Account/ExternalLogin?handler=Callback”;


Is the specified domain correct? Or should it be: “dev.mydomain.nl” ?
Or could there be another reason why the configured LoginCompletionUrl is not used?

[quote]
WoutervdW - 8/19/2019
I seem to be having the same problem.
Our setup is as follows:
SP-initiated login, using an OpenIdConnect test client, an IdentityServer4 IdP, which in it's turn connects using SAML to an external IdP.
The test client and the IdP runs on the same domain, but on different sub domains. The external IdP is -of course- a completely different domain.

OpenIdConnect test client: https://testclient.dev.mydomain.nl
IdP: https://auth.dev.mydomain.nl
External IdP: https://dummy-auth.dev.myExternalIdPdomain.nl

After the succesful login in the external IdP, a post is done to https://auth.dev.mydomain.nl/SAML/AssertionConsumerService.
But then there is always a redirect to "/Identity/Account/ExternalLogin?handler=Callback" .

However, because we don't want to use the "Identity" part in the path, I have configured in the Startup class:

services.Configure(options =>
{
options.CookieOptions = new ComponentSpace.Saml2.Bindings.CookieOptions()
{
Domain = "mydomain.nl"
};
});

And:

services.AddAuthentication().AddSaml(options =>
options.LoginCompletionUrl = relayState => !string.IsNullOrEmpty(relayState)
? QueryHelpers.AddQueryString("/Account/ExternalLogin?handler=Callback", "ReturnUrl", relayState)
: "/Account/ExternalLogin?handler=Callback";


Is the specified domain correct? Or should it be: "dev.mydomain.nl" ?
Or could there be another reason why the configured LoginCompletionUrl is not used?

[/quote]

Mmm, I just saw that the "saml-session" cookie was set with a property "same-site=lax".

So I added a CookiePolicy in my Startup of my IdP:

app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always,
MinimumSameSitePolicy = SameSiteMode.None
});


This solved my problem, now the redirect is going to "/External/Callback".

But is this the correct solution?
Or am I now creating a security risk?

You shouldn’t have to set the domain property of the cookie. My understanding of your configuration is that the cookie isn’t being shared across sub-domains etc. It only applies to the IdentityServer4 server where the SAML code is running. By default we don’t specify a domain and this should work in most scenarios.

We default the SameSite flag to None. I’m not sure why you’re seeing SameSite=lax although it can be changed through the DistributedSsoSessionStoreOptions.

What might be a good idea is to revert the code so the cookie settings are back to their defaults and the issue occurs. Using the browser developer tools, record the network traffic for the SSO, save it as an HAR file and send this to support@componentspace.com mentioning your forum post. We can take a look at the flow and what’s happening with the cookie.

[quote]
ComponentSpace - 8/19/2019
You shouldn't have to set the domain property of the cookie. My understanding of your configuration is that the cookie isn't being shared across sub-domains etc. It only applies to the IdentityServer4 server where the SAML code is running. By default we don't specify a domain and this should work in most scenarios.

We default the SameSite flag to None. I'm not sure why you're seeing SameSite=lax although it can be changed through the DistributedSsoSessionStoreOptions.

What might be a good idea is to revert the code so the cookie settings are back to their defaults and the issue occurs. Using the browser developer tools, record the network traffic for the SSO, save it as an HAR file and send this to support@componentspace.com mentioning your forum post. We can take a look at the flow and what's happening with the cookie.
[/quote]

If I don't set the domain property indeed everything still works fine.
But not setting the "MinimumSameSitePolicy = SameSiteMode.None" results in the error.

So I have just send the har- and logfiles like you suggested.
I am looking forward to seeing your findings.

Perhaps the UseCookiePolicy is overriding our configuration. I’ll need to investigate this. It’s still not clear to me why you’re seeing SameSite=lax as we default to None in our code.

I’m afraid we haven’t received an email from you. Please zip up these files and try again. Perhaps also send a separate email with no attachments to let us know.

For everybody who is following/reading this thread:

After succesfully sending the files and the analysis by ComponentSpace the suggested solution was to indeed add in Startup:

app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.None
});

Thanks Wouter. I’m still not sure why this is required and will investigate further. I will report my findings here for everyone’s benefit.

I can confirm that the following or equivalent code is required in an application’s start-up.


services.Configure(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});



The SAML session cookie (“saml-session”), which is used to maintain SAML session state and support the SAML protocol, is created with SameSiteMode.None. However, if the MinimumSameSitePolicy is set to SameSiteMode.Lax or SameSiteMode.Strict, the SAML session cookie will take on this minimum setting.

SAML protocol exchanges are, in most use cases, cross-site. The identity provider and service provider are different sites. Furthermore, these flows do not involve users clicking navigation links from one site to the other. For example, when an IdP sends an SP a SAML response, it returns a 200 HTTP response to the browser containing an HTML form and some JavaScript to automatically submit the form to the SP via an HTTP Post. From the browser’s perspective, the current site is the IdP and destination site for the HTTP Post is the SP. If the SAML session cookie is marked as SameSite=Strict, the browser won’t include it with the SAML response as the sites are different. If the SAML session cookie is marked as SameSite=Lax, the browser still won’t include it as this isn’t considered a top-level navigation action. Specifically, the SameSite specification doesn’t consider Post to be a safe HTTP method.

These issues aren’t specific to SAML SSO or ASP.NET Core. Other external authentication protocols and other platforms potentially have the same issues.

To circumvent these issues, the recommended approach is to set MinimumSameSitePolicy to SameSiteMode.None and to specify the SameSite setting on individual cookies as required.

One other point to note is that no cookies, not just the SAML session cookie, marked as Strict or Lax will be included with the HTTP Post. This may impact other aspects of the site’s functionality. However, a subsequent redirect by the site back to itself will make these cookies available as this will no longer be cross-site.

Thanks for the investigation and your clarification why the setting is needed :slight_smile:

Regards, Wouter

You’re very welcome.