10

Does anyone have a working sample for Sustainsys Saml2 library for ASP.NET Core WebAPI only project (no Mvc) and what's more important without ASP Identity? The sample provided on github strongly relies on MVC and SignInManager which I do not need nor want to use.

I added Saml2 authentication and at first it worked fine with my IdP (I also checked the StubIdP provided by Sustainsys) for first few steps so:

  • IdP metadata get properly loaded
  • My API properly redirects to sign-in page
  • Sign-in page redirects to /Saml2/Acs page, and I see in the logs that it parses the result successfully

However I don't know how to move forward from there and extract user login and additional claims (my IdP provided also an e-mail, and it is included in SAML response which I confirmed in the logs).

Following some samples found on the web and modyfing a little bit the MVC Sample from GitHub I did the following:

In Startup.cs:

...
.AddSaml2(Saml2Defaults.Scheme,
                       options =>
                       {
                           options.SPOptions.EntityId = new EntityId("...");
                           options.SPOptions.ServiceCertificates.Add(...));
                           options.SPOptions.Logger = new SerilogSaml2Adapter();
                           options.SPOptions.ReturnUrl = new Uri(Culture.Invariant($"https://localhost:44364/Account/Callback?returnUrl=%2F"));

                           var idp =
                               new IdentityProvider(new EntityId("..."), options.SPOptions)
                               {
                                   LoadMetadata = true,
                                   AllowUnsolicitedAuthnResponse = true, // At first /Saml2/Acs page throwed an exception that response was unsolicited so I set it to true
                                   MetadataLocation = "...",
                                   SingleSignOnServiceUrl = new Uri("...") // I need to set it explicitly because my IdP returns different url in the metadata
                               };
                           options.IdentityProviders.Add(idp);
                       });

In AccountContoller.cs (I tried to follow a somewhat similar situation described at how to implement google login in .net core without an entityframework provider):

[Route("[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
    private readonly ILog _log;

    public AccountController(ILog log)
    {
        _log = log;
    }

    [HttpGet("Login")]
    [AllowAnonymous]
    public IActionResult Login(string returnUrl)
    {
        return new ChallengeResult(
            Saml2Defaults.Scheme,
            new AuthenticationProperties
            {
                // It looks like this parameter is ignored, so I set ReturnUrl in Startup.cs
                RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
            });
    }

    [HttpGet("Callback")]
    [AllowAnonymous]
    public async Task<IActionResult> LoginCallback(string returnUrl)
    {

        var authenticateResult = await HttpContext.AuthenticateAsync(Constants.Auth.Schema.External);

        _log.Information("Authenticate result: {@authenticateResult}", authenticateResult);

// I get false here and no information on claims etc.
        if (!authenticateResult.Succeeded)
        {
            return Unauthorized();
        }

// HttpContext.User does not contain any data either


// code below is not executed
        var claimsIdentity = new ClaimsIdentity(Constants.Auth.Schema.Application);
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));

        _log.Information("Logged in user with following claims: {@Claims}", authenticateResult.Principal.Claims);           

        await HttpContext.SignInAsync(Constants.Auth.Schema.Application, new ClaimsPrincipal(claimsIdentity));

        return LocalRedirect(returnUrl);
    }

TLDR: Configuration for SAML in my ASP.NET Core WebApi project looks fine, and I get success response with proper claims which I checked in the logs. I do not know how to extract this data (either return url is wrong or my callback method should work differently). Also, it is puzzling why successfuly redirect from SSO Sign-In page is treated as "unsolicited", maybe this is the problem?

Thanks for any assistance

LizardErrtu
  • 121
  • 1
  • 6
  • Nice that you were able to make it work. I'm having a hard time here to adapt it to my application. Would you show how you configure this "Constant" object? What do you set for "Application", and "external"? – Arturio Oct 29 '19 at 12:17
  • These are equals to simple strings "Application" and "External" and that's it. – LizardErrtu Oct 30 '19 at 14:51
  • "Application" is the URL to you application then? – Arturio Oct 30 '19 at 15:03
  • No, literal strings "Application" and "External". These are just identifiers for schemas which are simple strings. I followed the pattern described in this article: https://stackoverflow.com/questions/53654020/how-to-implement-google-login-in-net-core-without-an-entityframework-provider – LizardErrtu Nov 04 '19 at 08:31
  • How did you configure the SpO.EntityId Parameter? My AD FS server logs and error saying "The relying party trust with identifier 'https://localhost:3000/Saml2' could not be located". Do I need to implement that route? Should it be handled in the backend? – Arturio Nov 18 '19 at 18:07
  • We were using PingFederate, not AD FS and EntityId is just a name for identification than needs to be the same on both sides. However if this was the case, Saml2 package would return more specific error that EntityId does not match. The /Saml2 and /Saml2/Acs paths are added by Saml2 handler class and you don't need to change any routes yourself. – LizardErrtu Nov 20 '19 at 08:30
  • Were you able to implement the LogOut functionality? Here I tryied a very simple approach, just like it's shown in the samples, but it never hits the Idp server. – Arturio Nov 20 '19 at 18:22
  • No, I have only local logout (so, deleting the cookie). – LizardErrtu Nov 22 '19 at 09:22
  • Even if you delete the cookie, if I call the authentication method again, the session will still be open, and so I will still be authenticated in my application. I guess you don't call the authentication method after the logout then, is it right? – Arturio Nov 22 '19 at 11:42
  • Yes, you are right, you are logging out from your application but not from the SSO service itself. This would need additional Single-Sign Out functionality, which I didn't configure. – LizardErrtu Nov 25 '19 at 08:49

2 Answers2

11

For anyone who still needs assistance on this issue, I pushed a full working example to github which uses a .Net Core WebAPI for backend and an Angular client using the WebAPI. you can find the example from here:

https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample

antistar
  • 198
  • 2
  • 8
  • 1
    This rules, not sure why they don't have something like this in the official examples. Thanks! – Casey May 05 '20 at 16:47
  • 1
    You are a lifesaver! This is great. I agree with Casey that they should have had this in the official examples. Thanks for posting!! – JRS May 08 '20 at 20:05
2

As it turned out, the various errors I've been getting were due to my solution being hosted inside container. This caused a little malfunction in internal aspnet keychain. More details can be found here (docker is mentioned almost at the end of the article):

https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x&view=aspnetcore-2.2

Long story short, for the code to be working I had to add only these lines:

services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo("/some/volume/outside/docker")); // it needs to be outside container, even better if it's in redis or other common resource

It fixed everything, which includes:

  • Sign-in action to external cookie
  • Unsolicited SSO calls
  • Exceptions with data protection key chain

So it was very difficult to find, since exceptions thrown by the code didn't point out what's going on (and the unsolicited SSO calls made me think that the SSO provider was wrongly configured). It was only when I disassembled the Saml2 package and tried various code pieces one by one I finally encoutered proper exception (about the key chain) which in turned led me to an article about aspnet data protection.

I provide this answer so that maybe it will help someone, and I added docker tag for proper audience.

LizardErrtu
  • 121
  • 1
  • 6