DotNet Core Authentication 2 - APIs
(Updated from the original version of 16th 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
Update: This continues to work with DotNet 7. The only difference is the lack of a
Startup.cs
file which involves a trivial change noted at the appropriate point below.
Using Visual Studio 2019 v16.5 Preview or later, create as follows:
- ASP.Net Core Web Application
- C# and DotNet Core 3 or later (tested up to DotNet 7)
- API
- No authentication
- Configure for HTTPS
- If you get the option, include Swagger/OpenAPI support
- Change the launch dropdown from IIS Express to your solution
- Only necessary on Windows as there is no IIS on Mac/Linux
- Optional extra stuff
- Remove any unneccesary
using
statements - Manually add any extra initial files like a README, licence, gitignore file and so on
- Remove any unneccesary
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 trying the built-in weather forecast, 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 on Core 3 and v7.0.2 on DotNet 7).
Now we need to configure the APIs startup.cs
file to ensure JWT authentication is switched on.
Startup.cs
Update: In later versions of DotNet this file has gone and we edit
Program.cs
instead. The code is the same but is now placed in theMain
method, andservices
is replaced withbuilder.Services
andConfiguration
withbuilder.Configuration
.
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 any links you have so that all references to weatherforecast
become api/weatherforecast
. Remember to check Properties\launchSettings.json
too.
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.
Update: In later versions you probably added Swagger (OpenAPI) support so there is no need to navigate to the API as the Swagger UI will open upon launch.
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 using
s.
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.
You may also see a message stating Invalid Signature. That's just due to the site not knowing your JWT secret. There's an option to paste it in there for signature verification but there's no need.
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