When ASP.NET Core 2 shipped the early previews, I knew one large change was going to be the Identity subsystem. The Identity for ASP.NET Core 1 worked ok, but the setup was very confusing with identical configuration is more than one place.
I’m happy to say that in ASP.NET Core 2 it’s much better. Implementing JWT Tokens for APIs was more confusing than I liked back when I wrote my Implementing an API in ASP.NET Core course for Pluralsight. I was hoping that it changed to simplify the way it works.
Now that I’m re-writing my ASP.NET Core End-to-End course for Pluralsight, I wanted to be able to both Cookies and JWT without having to split the projects. While this should work in ASP.NET Core 1, I couldn’t figure it out.
With some help from the Issues tab in Github and some gentle nudges I got it to work. Here’s a short tutorial about how it works. Be clear, this is is for ASP.NET Core 2 only!
ASP.NET Core Identity
If you build the full Model-View-Controller and use Individual Accounts for your project, it will generate a whole Login/Logout/Register set of views and such:
This enables the default behavior of Cookies being supported. The only setup is already done for you. That is calling UseAuthentication():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public void Configure(IApplicationBuilder app, IHostingEnvironment env, DataSeeder seeder) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); app.UseDatabaseErrorPage(); } else { app.UseExceptionHandler( "/Home/Error" ); } app.UseStaticFiles(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default" , template: "{controller=Home}/{action=Index}/{id?}" ); }); seeder.SeedAsync().Wait(); } |
But if we want to configure the cookies, we can use AddAuthentication to add options to the cookies like so:
1
2
| services.AddAuthentication() .AddCookie(cfg => cfg.SlidingExpiration = true ); |
If we don’t use AddAuthentication, the default behavior of using Cookies will happen since that is such a common path (not sure how I feel about this since I like the opt-in model of ASP.NET Core in general, but nonetheless).
Once this is all setup, if we have an API, we can visit the API in the browser once we’re logged in and it just works:
This is pretty insecure. For users we want to have decent length cookies to make login easier, for APIs, hanging on the top of the cookies for authentication is nasty. So we’d like to use tokens to authenticate the APIs. Welcome to JSON Web Tokens.
Using JSON Web Tokens (JWTs)
Using JWTs is pretty simple. We first need to call AddJwtBearer and setup the configuration for how our tokens will work. I won’t repeat how this works since the documentation is pretty good on this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| services.AddAuthentication() .AddJwtBearer(cfg => { cfg.RequireHttpsMetadata = false ; cfg.SaveToken = true ; cfg.TokenValidationParameters = new TokenValidationParameters() { ValidIssuer = Configuration[ "Tokens:Issuer" ], ValidAudience = Configuration[ "Tokens:Issuer" ], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration[ "Tokens:Key" ])) }; }); |
Note that all the configuration for the Identity middleware is now when we add the services, not in the Configure method any longer. This is the big change in the new version of Identity.
In order to issue JWTs, we need an anonymous method for issuing the JWT Token (again, I won’t belabor how this works, see the documentation):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| [AllowAnonymous] [HttpPost] public async Task { if (ModelState.IsValid) { var user = await _userManager.FindByEmailAsync(model.Email); if (user != null ) { var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false ); if (result.Succeeded) { var claims = new [] { new Claim(JwtRegisteredClaimNames.Sub, user.Email), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config[ "Tokens:Key" ])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken(_config[ "Tokens:Issuer" ], _config[ "Tokens:Issuer" ], claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return Ok( new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } } } return BadRequest( "Could not create token" ); } |
The trick here is to allow callers to send in a credential (in this case, username/pwd) and we can issue an expiring token that you would use.
For example, you can just use the token and set an Authorization header:
1
2
3
4
| GET /api/customers HTTP/1.1 Host: localhost:8889 Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib2JAYW9sLmNvbSIsImp0aSI6ImZmNjhhMDA0LTlmNjYtNGIxNC05OTE4LWZmZWVhMDZiNTllYyIsImV4cCI6MTUwNTY5MTc1MiwiaXNzIjoiaHR0cDovL215Y29kZWNhbXAuaW8iLCJhdWQiOiJodHRwOi8vbXljb2RlY2FtcC5pbyJ9.CyD7nDAvlgt37C3wDcRG6DnlLOx3a7ih5UskL3xSUH0 |
This would work fine. But, what about cookies?
Dual Authorization
In order to add support for JWT, we replaced the AddCookie with AddJwtBearer. Having websites require the token in the header would be a headache, especially for projects that aren’t purely SPA or API. So what I really wanted was support for both Cookies and JWTs.
At first I didn’t get what should happen, but David Fowler came to my rescue. he explained we could just chain the authorization types:
1
2
3
4
5
6
7
| services.AddAuthentication() .AddCookie(cfg => cfg.SlidingExpiration = true ) .AddJwtBearer(cfg => { // REMOVED FOR BREVITY, SEE ABOVE FOR IMPL }); |
Cool, we now have cookies and bearer token. We should be good. So when I get a token and go to my API, it tries and redirect me to the login page. It’s because I’m not logged in. Seems to be that the cookies are taking precedence. Kinda.
When we use the Authorize attribute, it actually binds to the first authentication system by default. The trick is to change the attribute to specify which auth to use:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [Route( "/api/customers" )] public class ProtectedController : Controller { public ProtectedController() { } public IActionResult Get() { return Ok( new [] { "One" , "Two" , "Three" }); } } |
Note that we’re specifying which schemes to use. The Cookies and JwtBearer both have default scheme names so unless we’re renamed the scheme (which we could do in Startup.cs), we can just use the scheme name to tell the API to use JWT only, not cookies at all.
If we try again after this, it works with a JWT Token *only*. If you did want to support both (but don’t), the property AuthenticationSchemes takes a comma delimited list of scheme names.
You can get the code here if you want to play with my very naïve implementation (the validation key is the appconfig file and you really should make it much stronger, especially in production. You’ve been warned!)
More