Supporting Multiple WS-Federation Authentication Schemes in ASP.NET Core

Supporting Multiple WS-Federation Authentication Schemes in ASP.NET Core

ASP.NET Core allows you to add support for multiple authentication schemes, such as social auth providers or WS-Federation. This support can be added with or without ASP.NET Identity Core. Users can then use these schemes to authenticate with your web application.

While adding support for social authentication providers (such as Twitter and Facebook) is useful, sometimes applications need to support for multiple authentication schemes of the same type. An example of this would be supporting WS-Federation login for more than one partner organisation.

The following shows the steps needed to add support for multiple WS-Federation authentication schemes, and how to handle the sign in and sign out logic.

Defining multiple WS-Federation Authentication Schemes

The first step is to enable the authentication provider and define the two authentication schemes. In this example, I am going to use OrgA and OrgB as the two authentication schemes.

services
    .AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    })
    .AddWsFederation("OrgA", options =>
    {
        options.Wtrealm = Configuration["OrgA:WsFed:Realm"];
        options.MetadataAddress = Configuration["OrgA:WsFed:Metadata"];
        options.CallbackPath = "/signin-wsfed-orga";
        options.Events.OnTicketReceived = (context) => HandleOnTicketReceivedAsync(context, "OrgA");
    })
    .AddWsFederation("OrgB", options =>
    {
        options.Wtrealm = Configuration["OrgB:WsFed:Realm"];
        options.MetadataAddress = Configuration["OrgB:WsFed:Metadata"];
        options.CallbackPath = "/signin-wsfed-orgb";
        options.Events.OnTicketReceived = (context) => HandleOnTicketReceivedAsync(context, "OrgB");
    })
    .AddCookie(options =>
    {
        options.LoginPath = new PathString("/Account/SignIn");
    });

In the example above you can also see that there is a handler for the OnTicketReceived event. This is used to add a claim which contains the authentication scheme the user used. This makes it easier when it comes to signing the user out.

The HandleOnTicketReceivedAsync method looks like this.

private Task HandleOnTicketReceivedAsync(TicketReceivedContext context, string scheme)
{
    ClaimsIdentity claimsIdentity = context.Principal.Identity as ClaimsIdentity;

    if (claimsIdentity is null)
    {
        return Task.CompletedTask;
    }

    claimsIdentity.AddClaim(new Claim("local:authscheme", scheme));

    return Task.CompletedTask;
}

Selecting the Authentication Scheme

The user will need to select which authentication scheme they would like to use. To support this, we need to provide the user with the options to select from and then let the authentication middleware know which scheme it should use. 

To do this two actions have been added to the AccountController, one for the user to select their option and then the other to handle the sign in process.

[AllowAnonymous]
public IActionResult SignIn()
{
    return View();
}

[HttpPost]
[AllowAnonymous]
public IActionResult SignIn(string scheme)
{
    if (User.Identity.IsAuthenticated)
    {
        return RedirectToAction("Index", "Home");
    }

    return Challenge(scheme);
}

After these are setup, we need a view to allow the user to make their selection. You could implement this a few different ways, however I chose to use two forms for simplicity.

<form asp-action="SignIn" method="POST">
    <input type="hidden" name="scheme" value="OrgA" />
    <button type="submit" class="btn btn-primary">Login with Organisation A</button>
</form>
<form asp-action="SignIn" method="POST">
    <input type="hidden" name="scheme" value="OrgB" />
    <button type="submit" class="btn btn-primary">Login with Organisation B</button>
</form>

Handling Sign Out

The last part is letting a user sign out of the web application. To do this we need to find the authentication scheme the user used and then call the SignOut method passing a parameter to indicate which authentication scheme to sign the user out with.

public IActionResult SignOut()
{
    string scheme = User.Claims.FirstOrDefault(claim => claim.Type == "local:authscheme")?.Value;
    return SignOut(new[] { CookieAuthenticationDefaults.AuthenticationScheme, scheme });
}

The code for the SignOut action shown above retrieves the authentication scheme from the claim added earlier. This is then passed to the SignOut method with the default cookie authentication scheme to clear the web application authentication cookie.

Summary

In general, the above steps apply for adding multiple authentication schemes for any provider.

The outline of the steps are to:

  • Define with authentication schemes with unique names and paths specified.
  • Add a claim when the user is authenticated to indicate which authentication scheme was used.
  • Allow the user to select which authentication scheme to use and send the appropriate challenge.
  • Allow the user to sign out by specifying the appropriate authentication scheme retrieve from the claim.

I have added an example GitHub repository if you would like a complete web app.

The authentication scheme names and the claim type should be extracted into a constant to make them easier to use and ensure consistency.