Saml2: Idp-initiated SSO w/ AspNetCore2 Handler

Created on 13 Oct 2018  路  10Comments  路  Source: Sustainsys/Saml2

Idp-Initiated SSO (Unsolicited SSO) Auth does not seem to work when using AspNetCore2 Handler

I used SSOCircle as the identity provider and AspNetCore2 sample to test. Apart from the default settings, I have set ReturnUrl to "/account/externallogin?handler=callback" and set AllowUnsolicitedAuthnResponse to true

Observation:
When the execution reaches OnGetCallbackAsync handler (ExternalLogin.cshtml.cs) via Idp Initiated SSO, call to the following line of code returns null (where as the same line of code successfully retrieves ExternalLoginInfo with UserPrincipal and Claims for SP Initiated Logins).

var info = await _signInManager.GetExternalLoginInfoAsync();

Prior to reaching ExternalLogin Callback handler, Logs indicate "Successfully processed SAML response" and "Identity.External signed in", however the callback handler fails to retrieve ExternalLoginInfo.

Library/Framework Versions:
TargetFramework: net472
Sustainsys.Saml2.AspNetCore2: 2.0.0

I believe this could be a bug (Unless I'm missing something). Any help is appreciated.

bug invalid

Most helpful comment

@AndersAbel can you expand on that?
I assume you mean to remove this, but can you confirm:
if (providerKey == null || provider == null) { return null; }

And then what value did you use to hardcode the provider key?

Edit: I was able to figure this out. Posting the steps I took for reference.
As Anders said, create a custom implementation of the SignInManger class. Make sure to change the namespace and the class name, then register it in Startup:
services.AddTransient<CustomSignInManager<IdentityUser>>();

Remove || !items.ContainsKey(LoginProviderKey) (commented out below) at line 593:

public virtual async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
        {
            var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
            var items = auth?.Properties?.Items;
            if (auth?.Principal == null || items == null) // || !items.ContainsKey(LoginProviderKey)) -- Items does not contain LoginProviderKey in Idp-initiated flow
            {
                return null;
            } 

Then hardcode (or figure out a better way to substitute) the value of the provider variable where it tries to retrieve the LoginProviderKey at line 612:

var provider = "Saml2"; //items[LoginProviderKey] as string; - LoginProviderKey isn't present in Idp-initiated flow

At this point, authentication should work. New logins will still be prompted to register with an email, - and confirm that email - but they are authenticated.

All 10 comments

This needs debugging.

This is due to how SignInManager.GetExternalLoginInfoAsync works. It expects and looks for a provider in the relayData:

https://github.com/aspnet/Identity/blob/2c9e1940204837bcd425e8f4488bf2564434b357/src/Identity/SignInManager.cs#L612

With an Idp-initiated sign on, there is no relayData. So you need to make your own implementation of GetExternalLoginInfoAsync that covers for that.

Thank you, Anders for looking into this. I'll try to implement custom logic for GetExternalLoginInfoAsync like you suggested.

@sai450 Well, I just had to implement it myself for a customer :) It's mostly a matter of copying the existing code and removing the provider check. Then you somehow have to find out the provider key - can be hard coded if Saml2 is the only external provider.

@AndersAbel can you expand on that?
I assume you mean to remove this, but can you confirm:
if (providerKey == null || provider == null) { return null; }

And then what value did you use to hardcode the provider key?

Edit: I was able to figure this out. Posting the steps I took for reference.
As Anders said, create a custom implementation of the SignInManger class. Make sure to change the namespace and the class name, then register it in Startup:
services.AddTransient<CustomSignInManager<IdentityUser>>();

Remove || !items.ContainsKey(LoginProviderKey) (commented out below) at line 593:

public virtual async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
        {
            var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
            var items = auth?.Properties?.Items;
            if (auth?.Principal == null || items == null) // || !items.ContainsKey(LoginProviderKey)) -- Items does not contain LoginProviderKey in Idp-initiated flow
            {
                return null;
            } 

Then hardcode (or figure out a better way to substitute) the value of the provider variable where it tries to retrieve the LoginProviderKey at line 612:

var provider = "Saml2"; //items[LoginProviderKey] as string; - LoginProviderKey isn't present in Idp-initiated flow

At this point, authentication should work. New logins will still be prompted to register with an email, - and confirm that email - but they are authenticated.

@mickey-stringer Thanks for taking the time to post the how-to!

@AndersAbel thank _you_ for pointing me in the right direction!

@mickey-stringer and @AndersAbel
I have a similar issue with IdP-initiated login. For my setup, please see https://stackoverflow.com/questions/63853661/authenticateresult-succeeded-is-false-with-okta-and-sustainsys-saml2.

As you can see from the code samples on SO, I am not using a sign-in manager, but am directly calling:
await HttpContext.AuthenticateAsync(ApplicationSamlConstants.External);
But for IdP-initiated, it has no information. (It is now working for SP-initiated, though).
AuthenticateResult.Succeeded is false, and AuthenticateResult.None is true.

Can either of you offer any advice?
Thanks so much for any assistance you can offer.

Update, just FYI:
When I run the solution in Visual Studio, it works, using the test IdP provider, i.e., https://stubidp.sustainsys.com/.
And when I deploy the solution to my local IIS instance on my desktop, it still works, using the test IdP provider
But when I deploy that same exact code to our production server (a VM), it fails as described above.
Any thoughts on what to look for or how to debug further?

I have captured the following debugging information (from the production VM):
2020-09-15 16:01:40.574 -05:00 [DBG] Received unsolicited Saml Response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id which is allowed for idp http://www.okta.com/exk1jic9zn7QommF00h8
2020-09-15 16:01:40.590 -05:00 [DBG] Signature validation passed for Saml Response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id
2020-09-15 16:01:40.652 -05:00 [DBG] Extracted SAML assertion id16338952065129118260692652
2020-09-15 16:01:40.652 -05:00 [INF] Successfully processed SAML response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id and authenticated [email protected]
2020-09-15 16:01:41.433 -05:00 [ERR] SAML Authentication Failure:
authenticateResult.Failure (Exception object) is null;
No information was returned for the authentication scheme;
authenticateResult.Principal is null;
authenticateResult.Properties is null.
authenticateResult.Ticket is null.

Was this page helpful?
0 / 5 - 0 ratings