.Net Core Authentication 2 - APIs

16 March 2020

As mentioned in part 1 (authentication for sites), sometimes the built-in options for authentication are not what you want. What follows are steps to get the basics in place without pre-generation.

You’ll get [Authorize] attributes for controller/route protection using JWT tokens. It doesn’t go as far as creating the database, but it gets you to the point where you just need to add your own supporting logic. You will have a JWT-token-generating endpoint and a stub protected one.

There is a Github repository which contains all the below steps as a sequence of commits. That repo includes a sample Postman file too.

Create the solution

Using Visual Studio 2019 v16.5 Preview or later, create as follows:

  • ASP.Net Core Web Application
  • C# and DotNet Core 3
  • API
  • No authentication
  • Configure for HTTPS
  • Change the launch dropdown from IIS Express to your solution
  • Optional extra stuff
    • Remove any unneccesary using statements
    • Manually add any extra initial files like a README, licence, gitignore file and so on

Add the JWT app settings

In your projects appsettings.json file add a section for JWT.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "JWT": {
    "Key": "This is a secret. But not a very good one, so change it.",
    "Issuer": "EXAMPLE-API",
    "Audience": "EXAMPLE-UI",
    "LifetimeSeconds": 3600
  },
  "AllowedHosts": "*"
}

This example specifies who issues the token (so the UI knows it is from the API) and who it is generated for (the Audience). It also has a one hour lifespan.

At this point it’s worth running your API and hitting the built-in weather forecast route which should show some JSON data.

Register JWT in the startup configuration

In the Manage nuget packages for solution area add the Microsoft.AspNetCore.Authentication.JwtBearer package (v3.1.2 for me).

Now we need to configure the APIs startup.cs file to ensure JWT authentication is switched on.

Add some extra references at the top.

using System;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

Now in the ConfigureServices method, just before services.AddControllers(), update to register JWT authentication.

public void ConfigureServices(IServiceCollection services)
{
    // added from here
    services.AddAuthentication(x =>
    {
        x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(x =>
    {
        x.RequireHttpsMetadata = true;
        x.SaveToken = true;
        x.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["JWT:Key"])),

            ValidateIssuer = true,
            ValidIssuer = Configuration["JWT:Issuer"],

            ValidAudience = Configuration["JWT:Audience"],
            ValidateAudience = true,

            ValidateLifetime = true,
            RequireExpirationTime = true,
            ClockSkew = TimeSpan.FromSeconds(2),
        };
    });
    // to here
    services.AddControllers();
}

This sets up the JWT token support in the framework. It also pulls in values you have just added to your appsettings.json file.

The ClockSkew bit at the end is a fiddle-factor to allow for slightly out of sync clocks across systems.

Finally in the Configure method, just before app.UseAuthorization(), add another call. The order matters.

app.UseAuthentication();  //  <-- add this
app.UseAuthorization();

Protect the sample endpoint

The easiest bit. Thanks to the startup.cs changes all you need to do is add the [Authorize] attribute to the WeatherForecastController.cs class.

First reference the package.

using Microsoft.AspNetCore.Authorization;

Then update the controller attributes.

[ApiController]
[Authorize]  //  <-- add this attribute
[Route("api/[controller]")]  //  <-- add api prefix
public class WeatherForecastController : ControllerBase
// etc ...

Update your Properties\launchSettings.json so that all references to "launchUrl": "weatherforecast" become "launchUrl": "api/weatherforecast".

Now run it and if you visit the new url (note the api/ prefix) you should be denied access this time with a 401 Unauthorized HTTP error (). This is because we have not presented a valid token.

Add the ability to fetch a token

Add the code

Right-click the Controllers folder. Choose Add, then Controller. You need an API Controller - Empty. Call it TokenController. It may take a while to download some extra design-time packages for the controller code generation.

Add some extra references.

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using System.Text;

Add a constructor as you need the configuration. Include a backing field for the config.

private readonly IConfiguration configuration;

public TokenController(IConfiguration configuration)
{
    this.configuration = configuration;
}

Now add an endpoint for callers to get a token.

[HttpPost]
public string GetToken([FromBody]string subject)
{
    var now = DateTime.UtcNow;
    var seconds = int.Parse(configuration["Jwt:LifetimeSeconds"]);
    var exp = now.AddSeconds(seconds);
    var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["Jwt:Key"]));
    var iss = configuration["Jwt:Issuer"];
    var aud = configuration["Jwt:Audience"];
    var tokenHandler = new JwtSecurityTokenHandler();
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new Claim[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, subject),
            new Claim(ClaimTypes.Role, "user"),
        }),
        IssuedAt = now,
        NotBefore = now,
        Expires = exp,
        Issuer = iss,
        Audience = aud,
        SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature)
    };
    var token = tokenHandler.CreateToken(tokenDescriptor);
    return tokenHandler.WriteToken(token);
}

This sets up some defaults, calculates some times (expiry etc), adds the subject from the request JSON body, then signs and returns a token. Note the claims; that’s where you will mostly want to change stuff. To tidy up, remove the redundant usings.

Fetch a token

If you call https://localhost:5001/api/token as a POST request (note the slightly changed URL now contains api in the route), with the header for Content-Type being set to application/json, and a raw body with a value of "Ms Example" (including the double-quotes), you’ll get a token back (eyJhbGciOiJIUzUxMiIsInR5cC...NtpEPIQ).

This is the value used in subsequent calls via a header of Authorization with a value of “Bearer <token>”.

Peek into the token

Paste this into the box at https://jwt.io and it decodes it into a header and a payload.

Header:

{
  "alg": "HS512",
  "typ": "JWT"
}

Payload:

{
  "sub": "Ms Example",
  "role": "user",
  "nbf": 1584480171,
  "exp": 1584483771,
  "iat": 1584480171,
  "iss": "EXAMPLE-API",
  "aud": "EXAMPLE-UI"
}

These values should be obviously related to the ones from the C# GetToken() method.

Using the token

An example is in the Postman script included in the repository, which will get the token and automatically use it on the next request.

When doing it manually, do a GET to /api/weatherforecast with an Authorization header of "Bearer <token>" and you’ll get a proper response. Drop the header or mangle the token, or let it expire, and you’ll get 401 unauthorized.

Remember that this is a bearer token. By very definition if someone has the token they can use the token. You should probably keep the lifetime quite short. If the API is being consumed by a website you could also consider adding the token into a secure https-only cookie and checking the header and cookie match before accepting it. This gives a higher level of security as more than one aspect needs breaking to misuse a token (both Javascript and cookies).

Next steps

  • Add a database backend, with your own custom migration
    • Consider using Migratable, my own migration library
  • Remember to not store passwords (either plain or encrypted)
    • Use one-way repeatable hashes
    • Include both salt and pepper