There are a few resources that you can find that teach how to secure an ASP.NET Core web application. I’ve written a few, for example ASP.NET Core Identity From Scratch, External Login Providers in ASP.NET Core and Facebook Authentiation with ASP.NET Core.
For web apis using ASP.NET Core it’s a little bit harder to find information. That’s what this blog post is about.
In this blog post I’ll explain how you can use Json Web Tokens (JWT) to secure a Web Api in ASP.NET Core. There’s a demo project in github that you can use to follow along.
Using a token instead of a cookie
The most common way to keep track of a signed in user in a web application is to use cookies.
The normal flow is: the user clicks login, goes to a login page and after entering valid credentials the response that is sent to the user’s browser contains a Set-Cookie
header that contains encrypted information.
Every time a cookie is set for a domain, e.g. blinkingcaret.com, on every subsequent request for that domain the browser will include the cookie automatically.
On the server that cookie is decrypted and its contents are used to create the user’s identity.
This process works very well if the client is a browser, it’s a different story when we the client is a mobile app.
JWT
What we can use instead of a cookie is a token. A token also represents the user, but when we use it we don’t rely on the browser’s built-in mechanism to deal with cookies.
We have to explicitly ask for a token, store it ourselves somewhere, and then manually send it with every request. There are some ways to make this as painless as possible and I’ll discuss some of them later in this the post.
The token format I’ll be talking about here is JWT. JWT stands for Json Web Token.
A JWT token has the following format: base64-encoded-header.base64-encoded-payload.signature
.
An example of a header is
{
“alg”: “HS265”,
“typ”: “JWT”
}
The payload contains a list of claims, for example:
{
"name": "Rui",
"admin": true
}
Finally the signature is created by taking: “base64(header).base64(payload)” and cryptographically signing it using the algorithm specified in the header. For example HMAC-SHA256. The signing part involves using a secret that is only known at the server.
Here’s how a real JWT looks like:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoicnVpIiwic3ViIjoidGVzdCIsIm5iZiI6MTUwMzYxNDU4NSwiZXhwIjoxNTA2MDMzNzg1LCJpc3MiOiJibGlua2luZ2NhcmV0IHN0cyIsImF1ZCI6ImJsaW5raW5nY2FyZXQgYXBwIn0.F7PFoYcQXez3zV98BFKLpyON6d_1p-6IAeihZRSv0VM
It is important to be aware that the information contained in the JWT is not encrypted. To get the payload you just need to base64-decode it. You can even do that from your developer tools console (for example in chrome). Use the atob
method and pass the payload as an argument. You’ll see that you’ll get JSON back.
The signature guarantees that if someone tries to replace the payload, the token becomes invalid. For someone to be successful in replacing the payload and producing a valid token they would need to know the secret used in the signature, and that secret never goes to the client.
This is something to have in mind when you decide what to put in the payload.
Using JWT in ASP.NET Core
To use JWT in ASP.NET Core we need to know how to manually create JWT tokens, how to validate them and how to create an endpoint so that the client app can request them.
How to create a JWT token
First you need to install the package System.IdentityModel.Tokens.Jwt:
$ dotnet add package System.IdentityModel.Tokens.Jwt
Now we need to create a secret key. We are going to use a symmetric key, here’s how we can do that:
var secretKey = new SymmetricSecurityKey(Endoding.UTF8.GetBytes("a secret that needs to be at least 16 characters long"));
Our token will contain a set of claims. So lets create them:
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "John"),
new Claim(JwtRegisteredClaimNames.Email, "[email protected]")
}
I’ve used both ClaimTypes
(from System.Security.Claims) and JwtRegisteredClaimNames
(from System.IdentityModel.Tokens.Jwt) to highlight that JwtRegisteredClaimNames
contains the claims that are listed in the JWT RFC. You should use those as much as possible if you are planning to use the produced tokens with different frameworks for compatibility reasons. However, there are some claim types that enable certain functionalities in ASP.NET. For example ClaimTypes.Name
is the default claim type for the user’s name (User.Identity.Name
). Another example is ClaimTypes.Role
that will be checked if you use the Roles property in an Authorize attribute (e.g. [Authorize(Roles="Administrator")]
).
After creating the list of claims we want to encode in the token we can create the token itself, here’s how we can do it:
var token = new JwtSecurityToken(
issuer: "your app",
audience: "the client of your app",
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddDays(28),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
There are a few concepts here that I didn’t mention before, namely the issuer, audience and expiration dates.
The issuer represents the entity that generates the tokens, in this case it will be the ASP.NET Core web application. The audience is who the tokens are intended to, i.e. the client application. This issuer and the audience are important if your are relying on a third party to create the tokens (not the case here). You can validate both the issuer and the audience when you validate the token.
The notBefore
and expires
define after what point in time the token can be used and until when it is valid, respectively.
Finally in the signingCredentials
you specify which security key and what algorithm to use to create the signature. In the example we used HMAC-SHA256.
If you don’t care about the issuer
and audience
(which are optional in the JWT specification), you can use the simpler constructor overload of JwtSecurityToken
that expects a JwtSecurityHeader
and JwtSecurityPayload
. You’ll have to manually add the expires
and notBefore
claims yourself to the payload, for example:
var claims = new Claim[] {
new Claim(ClaimTypes.Name, "John"),
new Claims(JwtRegisteredClaimNames.Email, "[email protected]"),
new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds()}"),
new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}")
}
var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
Note that the Exp
(expires) and Nbf
(notBefore) claims’ value is a string with Unix time. The easiest way to convert a DateTime
to that format is using DateTimeOffset
.
After creating an instance of JwtSecurityToken
, the way to actually generate the token is to call the WriteToken
method of an instance of JwtSecurityTokenHandler
and pass the JwtSecurityToken
as a parameter:
string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
Creating an endpoint to get the token
Now that we know how to create our JWT tokens we need a way to enable the client to get them. The easiest way to do that is to create a web api controller action that expects a post request, for example, this would work:
public class TokenController : Controller
{
[Route("/token")]
[HttpPost]
public IActionResult Create(string username, string password)
{
if (IsValidUserAndPasswordCombination(username, password))
return new ObjectResult(GenerateToken(username));
return BadRequest();
}
//...
Where in IsValidUserAndPasswordCombination
you can use, for example ASP.NET Identity to validate the user’s credentials (if you need a resource for how to setup ASP.NET Identity check ASP.NET Identity Core From Scratch).
GenerateToken
is the process we just described in the previous section.
Validating the token and “signing in” the user
Now that we have a way to issue tokens we need a way to validate them. We’ll be using ASP.NET Core’s authentication middleware and configure it to accept JWT tokens.
Add the Microsoft.AspNetCore.Authentication.JwtBearer
NuGet package to your project.
$ dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Next open Startup.cs and update the ConfigureServices
method:
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = "JwtBearer";
options.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your secret goes here")),
ValidateIssuer = true,
ValidIssuer = "The name of the issuer",
ValidateAudience = true,
ValidAudience = "The name of the audience",
ValidateLifetime = true, //validate the expiration and not before values in the token
ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
};
});
}
If you are not familiar with ASP.NET Core’s authentication middleware I recommend reading External Login Providers in ASP.NET Core. Even though it’s about enabling signing in through Google, Facebook, etc it provides a detailed explanation of how authentication middleware works.
Also, be aware that this is the new ASP.NET Core 2.0 syntax where authentication is fully configured via the ConfigureServices
method, however the concepts are the same.
More importantly in this example is the TokenValidationParameters
class. It’s the class you have to instantiate to configure how the token should be validated.
In Startup.cs
you need to update the Configure
method and add the authentication middleware:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//...
app.UseAuthentication(); //needs to be up in the pipeline, before MVC
//...
app.UseMvc(ConfigureRoutes);
//..
Client
The web api client can be a desktop app, mobile or even a browser. The example I’ll be describing is that of a web application that signs in, saves the token and then uses it to perform authenticated requests. You can find a working example here.
First, to sign in you need to send a POST request to “/token” (or wherever you’ve setup the web api endpoint to generate the token) with a username and password. You can easily do that with jQuery:
$.post("/token", $.param({username: "the username", password: "the password"})).done(function(token){
//save the token in local storage
localStorage.setItem("token", token);
//...
}).fail(handleError);
If all goes well with the POST you get the JWT back and you can save it somewhere, usually in local storage in the case of a web application. On mobile it depends on the platform you are using but they all have facilities that allow you to save the token (e.g. Android’s SharedPreferences
).
For the authentication middleware in the previous section to accept a JWT token and transform it in a User
that you can then access in your controller action the request must have an Authorization header. The value of the header should be “Bearer ” followed by the JWT token, for example:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1l...
Although you can “manually” add the Authorization
header to every request, there’s usually ways to do that automatically. For example in jQuery there’s an event that you can use that runs before every request where you can check if you have a saved token, and if that’s the case, add the Authorization
header to the request:
$.ajaxSetup({
beforeSend: function(xhr) {
if (localStorage.getItem("token") !== null) {
xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem("token"));
}
}
});
There are similar mechanisms if you are using other frameworks, for example Angular has HttpInterceptors.
Finally, to logout you just need to remove the token from local storage:
localStorage.removeItem("token")
One thing to be aware of is that if the client performs an action that requires the user to be authenticated and there’s no (valid) Authorization
header in the request, the server will respond to that request with a response with status code 401. The response will also have a WWW-Authenticate:Bearer header. If you receive a response like this you can notify the user that authentication is required.