Back to all posts

Using Multiple Authentication/Authorization Providers in ASP.NET Core

Posted on Mar 20, 2018

Posted in category:
Development
ASP.NET

I find that in many cases application developers need to create web applications that can support not only the web application itself but an API that might be used by a Mobile Application or otherwise. I have seen some methods used in the past to create these two interfaces, everything from home-grown security to the creation of two separate applications that do the same thing. With ASP.NET Core we can use multiple authentication providers so we can easily support various providers and control when each should apply. In this post, I explore the process of supporting multiple authentication providers.

The Goal

Before I get into the specifics of how to accomplish, let's discuss the goal. This post assumes that you have an already working ASP.NET Core 2.0 application using ASP.NET Identity. Your application is working, you have various parts of the application secured, and users can access what they should. You now want to expand your application to have an API for a mobile application. The existing application and controllers should continue to use Identity. However, you would like to use Bearer Tokens to support the mobile API's.

We want this to be in the same project to ensure the best reusability within the application, and we want to do this with the least amount of impact to the existing application.

Supporting Items

Before we get into the "meat" of the code, there are a few supporting objects that we need to create first. Each of these items should be added to your project as individual classes.

CredentialsViewModel.cs

This view model is what will be used to accept authentication credentials from the user asking for a bearer token. This is a simple POCO object with data annotations for validation.

CredentialsViewModel.cs Contents
public class CredentialsViewModel
{
    [Required]
    public string UserName { get; set; }

    [Required]
    public string Password { get; set; }
}

JwtIssuerOptions.cs

This object stores information and options regarding the configuration of the token and how the token will be issued.

JwtIssuerOptions.cs Contents
public class JwtIssuerOptions
{
    //Borrowed from: https://github.com/mmacneil/AngularASPNETCore2WebApiAuth/blob/master/src/Models/JwtIssuerOptions.cs
    public string Issuer { get; set; }
    public string Subject { get; set; }
    public string Audience { get; set; }
    public DateTime Expiration => IssuedAt.Add(ValidFor);
    public DateTime NotBefore { get; set; } = DateTime.UtcNow;
    public DateTime IssuedAt { get; set; } = DateTime.UtcNow;
    public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120);
    public bool RequireHttpsMetadata { get; set; } = true;
    public Func<task<string>> JtiGenerator =>
        () => Task.FromResult(Guid.NewGuid().ToString());
    public SigningCredentials SigningCredentials { get; set; }
}

IJwtFactory.cs

This interface defines a factory that will actually work to issue needed items for tokens.

IJwtFactory
public interface IJwtFactory
{
    Task<string< GenerateEncodedToken(string userName, ClaimsIdentity identity);
    ClaimsIdentity GenerateClaimsIdentity(string userName, string id);
}

JwtFactory.cs

This is the concrete implementation of the factory that will create the token as needed.

JwtFactory Implementation
public class JwtFactory : IJwtFactory
{
    private readonly JwtIssuerOptions _jwtOptions;

    public JwtFactory(IOptions<jwtissueroptions> jwtOptions)
    {
        _jwtOptions = jwtOptions.Value;
    }

    public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
    {
        var claims = new[]
        {
                new Claim(JwtRegisteredClaimNames.Sub, userName),
                new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
                new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
                identity.FindFirst(Constants.JwtRol),
                identity.FindFirst(Constants.JwtId)
            };

        // Create the JWT security token and encode it.
        var jwt = new JwtSecurityToken(
            issuer: _jwtOptions.Issuer,
            audience: _jwtOptions.Audience,
            claims: claims,
            notBefore: _jwtOptions.NotBefore,
            expires: _jwtOptions.Expiration,
            signingCredentials: _jwtOptions.SigningCredentials);

        var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

        return encodedJwt;
    }

    public ClaimsIdentity GenerateClaimsIdentity(string userName, string id)
    {
        return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[]
        {
            new Claim(Constants.JwtId, id),
            new Claim(Constants.JwtRol, Constants.JwtClaim)
        });
    }

    /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC).
    private static long ToUnixEpochDate(DateTime date)
        => (long)Math.Round((date.ToUniversalTime() -
                            new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero))
                            .TotalSeconds);
}

TokenHelper.cs

This class contains a helper method that actually creates the token response that will be returned to the user requesting a token.

TokenHelper
public class Tokens
{
    public static async Task<string> GenerateJwtResponse(ClaimsIdentity identity, 
IJwtFactory jwtFactory, string userName, 
JwtIssuerOptions jwtOptions, JsonSerializerSettings serializerSettings)
    {
        var response = new
        {
            id = identity.Claims.Single(c => c.Type == "id").Value,
            auth_token = await jwtFactory.GenerateEncodedToken(userName, identity),
            expires_in = (int) jwtOptions.ValidFor.TotalSeconds
        };

        return JsonConvert.SerializeObject(response, serializerSettings);
    }
}

Configuration Elements

With the foundational items ready for us, we need to set up our configuration elements. These can be added to your appsettings.json file, or to environment variables as needed. The below snippet shows the basic configuration, assuming a local installation.

"JwtIssuerOptions": {
  "Issuer": "webApi",
  "Audience": "http://localhost:5193/",
  "RequireHttpsMetadata":  false 
}

In each of your environments, you will want to make sure that you have a proper audience value matching your current URL.

Startup.cs Changes

The last piece is to add a few items to the Startup.cs file to actually work this in!

Additions to ConfigureServices()

In this section, we need to configure our options file, as well as set up how the tokens will be verified. This can be done with the following code.

ConfigureServices() Additions
var jwtAppSettingsOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
services.Configure(options =>
{
    options.Issuer = jwtAppSettingsOptions[nameof(JwtIssuerOptions.Issuer)];
    options.Audience = jwtAppSettingsOptions[nameof(JwtIssuerOptions.Audience)];
    options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = true,
    ValidIssuer = jwtAppSettingsOptions[nameof(JwtIssuerOptions.Issuer)],
    ValidateAudience = true,
    ValidAudience = jwtAppSettingsOptions[nameof(JwtIssuerOptions.Audience)],
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = "YourSecuretKey-StoreThisSecurely",
    RequireExpirationTime = false,
    ValidateLifetime = true,
    ClockSkew = TimeSpan.Zero
};

We also need to update our call to AddAuthorization, setting up a policy that outlines the expected claim for a valid API user. This is done with a one-line addition to any existing authorization configuration for policies.

 options.AddPolicy("ApiUserPolicy", policy => policy.RequireClaim("JwtRole", "ID"));

The last change is to set a default authentication scheme as well as to configure how JwtBearer should be used. This is done using the following snippet.


services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = tokenValidationParameters;
    options.Audience = jwtAppSettingsOptions[nameof(JwtIssuerOptions.Audience)];
    options.RequireHttpsMetadata = bool.Parse(jwtAppSettingsOptions[nameof(JwtIssuerOptions.RequireHttpsMetadata)]);
});

Understandably, there is a bit of code here, but with this all out of the way, we can now focus on the implementation.

Issuing Tokens - AuthController.cs

The final big code addition is the creation of an AuthController API method that will allow users to request a token. The desired process for this is for a user to make an HTTP Post to /api/auth/login with passed credentials if the login is valid the user will receive a token back. The below snippet is a complete implementation of an AuthController, assuming a UserObject of UserProfile.

[Route("api/[controller]")]
public class AuthController : Controller
{
    private readonly UserManager<userprofile< _userManager;
    private readonly IJwtFactory _jwtFactory;
    private readonly JwtIssuerOptions _jwtOptions;

    public AuthController(UserManager<userprofile> userManager, IJwtFactory jwtFactory, IOptions<jwtissueroptions> jwtOptions)
    {
        _userManager = userManager;
        _jwtFactory = jwtFactory;
        _jwtOptions = jwtOptions.Value;
    }

    /// /// 
    /// Sample request:
    ///
    ///     POST /login
    ///     {
    ///        "userName": "[email protected]",
    ///        "password": "UserPassword"
    ///     }
    ///
    ///  
    [ProducesResponseType(typeof(string), 400)]
    [HttpPost("login")]
    public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
        if (identity == null)
        {
            return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
        }

        var jwt = await Tokens.GenerateJwtResponse(identity, _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
        return Content(jwt);
    }

    private async Task<ClaimsIdentity> GetClaimsIdentity(string userName, string password)
    {
        if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
            return await Task.FromResult<ClaimsIdentity>(null);

        // get the user to verifty
        var userToVerify = await _userManager.FindByNameAsync(userName);

        if (userToVerify == null) return await Task.FromResult<ClaimsIdentity>(null);

        // check the credentials
        if (await _userManager.CheckPasswordAsync(userToVerify, password))
        {
            return await Task.FromResult(_jwtFactory.GenerateClaimsIdentity(userName, userToVerify.Id.ToString()));
        }

        // Credentials are invalid, or account doesn't exist
        return await Task.FromResult<ClaimsIdentity>(null);
    }
}

A user making a request will get a JSON response with their token. that they can then use for all future requests.

Using this New Method

With all of the setup out of the way, we can now focus on the fun part, how do we use this? Since we set a default authentication scheme all existing [Authorize] attributes will attempt to validate based on the cookie-based authentication. When we want to adjust to use the token-based authentication, we can simply use this syntax.

[Authorize(Policy = "ApiUserPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

This is all that we need to do! Nice and simple to implement.

Getting the Current Username

Given that we are using claims as part of the token, we do need to use a different method to lookup the current username. Rather than checking the name of the identity, we need to look for the nameidentifier claim. You can do this using the following method.

public string GetCurrentApiUserName()
{
    var nameClaim = _context.HttpContext.User?.Claims?.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
    if (nameClaim != null)
        return nameClaim.Value;

    return string.Empty;
}

Conclusion

This post ended up being longer than I had hoped, but it provides you with a 100% working solution to implement both Cookie and Jwt based authentication in your own project. If there is enough demand I can work to try and setup a sample project on GitHub, but for the time being I hope that this helps those of you looking for a method to support both API and website users inside of the same application.