宝剑锋从磨砺出,梅花香自苦寒来。这篇文章主要讲述Securing ASP.NET Core 2.0 Applications with JWTs相关的知识,希望能为你提供帮助。
A Quick Introduction to JWTsJSON Web Tokens, often shortened with JWTs, are gathering more and more popularity in the Web environment. It is an open standard that allows transmitting data between parties as a JSON object in a compact and secure way. They are usually used in authentication and information exchange scenarios, since the data transmitted between a source and a target are digitally signed so that they can be easily verified and trusted.
The JWTs are structured in three sections:
- the
Header
: this is a JSON object containing meta-information about the type of JWT and hash algorithm used to encrypt the data. - the
Payload
: even this is a JSON object containing the actual data shared between source and target; these data are coded in claims, that is statements about an entity, typically the user. - the
Signature
: this section allows to verify the integrity of the data, since it represents a digital signature based on the previous two sections.
Authorization
HTTP header using the Bearer schema, and it should contain all the information that allows to grant or deny access to the resource.Of course, this is a very quick overview of JWT, just to have a common terminology and a basic idea of what the technology is. You can find more information in Introduction to JSON Web Tokens.
"JSON Web Tokens are a compact and self-contained way for securely transmitting information between parties as a JSON object."Securing ASP.NET Core 2.0 Applications with JWTsLet‘s take a look at how to set up a ASP.NET Core 2 application with JWT support by creating a Web API application. You can create it by using Visual Studio or via command line. In the first case you should choose the ASP.NET Core Web Application project template, as shown in the following picture:
TWEET THIS
文章图片
文章图片
Then you need to select the type of ASP.NET application, that in our case will be Web API, as we can see in the following picture:
文章图片
For simplicity, we have not enabled any type of authentication since we want to focus on JWT management.
If you prefer to create your application from command line, you can do so through the following command:
dotnet new webapi -n JWT
This will create an ASP.NET Web API project named JWT in the current folder.
文章图片
Regardless the way you have created your project, you will get in the folder the files defining the classes to setup a basic ASP.NET Core 2 Web API application.
First of all, we change the body of
ConfigureServices
method in Startup.cs
in order to configure support for JWT-based authentication. The following is the resulting implementation of ConfigureServices
:using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace JWT
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}public IConfiguration Configuration { get;
}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddMvc();
}
}
}
Here we register JWT authentication schema by using
AddAuthentication
method and specifying JwtBearerDefaults.AuthenticationScheme
. Then we configure the authentication schema with options for JWT bearer. In particular, we specify which parameters must be taken into account in order to consider valid a JSON Web Token. Our code is saying that to consider a token valid we must:- validate the server that created that token (
ValidateIssuer = true
); - ensure that the recipient of the token is authorized to receive it (
ValidateAudience = true
); - check that the token is not expired and that the signing key of the issuer is valid (
ValidateLifetime = true
); - verify that the key used to sign the incoming token is part of a list of trusted keys (
ValidateIssuerSigningKey = true
).
appsettings.json
file to make them accessible via Configuration
object://appsettings.json
{
// ...
"Jwt": {
"Key": "veryVerySecretKey",
"Issuer": "http://localhost:63939/"
}
}
This step configures the JWT-based authentication service. In order to make the authentication service available to the application, we need to create the
Configure
method in the Startup
class to invoke app.UseAuthentication()
:// other methods
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}app.UseAuthentication();
app.UseMvc();
}
This change completes the configuration of our application to support JWT-based authentication.
ASP.NET Core 1.0 vs ASP.NET Core 2.0If you already knew how ASP.NET Core 1.x supported JWT, you can find that it has been made easier.
First of all, in previous version of ASP.NET Core you needed to install a few external packages. Now it is no longer needed since JSON Web Tokens are natively supported.
In addition, the configuration steps have been simplified as a consequence of the overall authentication system. In fact, while in ASP.NET Core 1.0 we had a middleware for each authentication schema we would support, ASP.NET Core 2.0 uses a single middleware handing all authentication and each authentication schema is registered as a service.
This allows us to create a more compact and cleaner code.
Securing ASP.NET Core 2.0 Endpoints with JWTsOnce we have enabled JWT-based authentication, let‘s create a simple Web API to return a list of books when invoked with an HTTP
GET
request. This API will be held by a new class called BooksController
in the Controllers
namespace:using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
namespace JWT.Controllers
{
[Route("api/[controller]")]
public class BooksController : Controller
{
[HttpGet, Authorize]
public IEnumerable<
Book>
Get()
{
var currentUser = HttpContext.User;
var resultBookList = new Book[] {
new Book { Author = "Ray Bradbury",Title = "Fahrenheit 451" },
new Book { Author = "Gabriel Garcí
a Má
rquez", Title = "One Hundred years of Solitude" },
new Book { Author = "George Orwell", Title = "1984" },
new Book { Author = "Anais Nin", Title = "Delta of Venus" }
};
return resultBookList;
}public class Book
{
public string Author { get;
set;
}
public string Title { get;
set;
}
public bool AgeRestriction { get;
set;
}
}
}
}
As we can see, the API simply returns an array of books. However, as we marked the API with the
Authorize
attribute, requests to this endpoint will trigger the validation check of the token passed with the HTTP request.If we run the application (through our IDE or the
dotnet run
command) and make a GET request to the /api/books
endpoint, we will get a 401
HTTP status code as a response. You can try it by running the UnAuthorizedAccess
test in the Test
project attached to the project‘s source code or by using a generic HTTP client such as curl or Postman.文章图片
Of course, this result is due to the lack of the token, so that the access to the API has been denied.
Creating JWT on AuthenticationLet‘s add an authentication API to our application so that user can authenticate to get new JWTs. To do that, let‘s create a controller called
TokenController
in the Controllers
namespace with the following code:using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace JWT.Controllers
{
[Route("api/[controller]")]
public class TokenController : Controller
{
private IConfiguration _config;
public TokenController(IConfiguration config)
{
_config = config;
}[AllowAnonymous]
[HttpPost]
public IActionResult CreateToken([FromBody]LoginModel login)
{
IActionResult response = Unauthorized();
var user = Authenticate(login);
if (user != null)
{
var tokenString = BuildToken(user);
response = Ok(new { token = tokenString });
}return response;
}private string BuildToken(UserModel user)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}private UserModel Authenticate(LoginModel login)
{
UserModel user = null;
if (login.Username == "mario" &
&
login.Password == "secret")
{
user = new UserModel { Name = "Mario Rossi", Email = "[email
protected]"};
}
return user;
}public class LoginModel
{
public string Username { get;
set;
}
public string Password { get;
set;
}
}private class UserModel
{
public string Name { get;
set;
}
public string Email { get;
set;
}
public DateTime Birthdate { get;
set;
}
}
}
}
The first thing to notice is the presence of
AllowAnonymous
attribute. This is very important, since this must be a public API, that is an API that anyone can access to get a new token after providing his credentials.The API responds to an HTTP
POST
request and expects an object containing username and password (a LoginModel
object).The
Authenticate
method verifies that the provided username and password are the expected ones and returns a UserModel
object representing the user. Of course, this is a trivial implementation of the authentication process. A production-ready implementation should be more accurate as all we know.If the
Authentication
method returns a user, that is the provided credentials are valid, the API generates a new token via the BuildToken
method. And this is the most interesting part: here we create a JSON Web Token by using the JwtSecurityToken
class. We pass a few parameters to the class constructor, such as the issuer, the audience (in our case both are the same), the expiration date and time and the signature. Finally, the BuildToken
method returns the token as a string, by converting it through the WriteToken
method of the JwtSecurityTokenHandler
class.Authenticating to Access the APIsNow we can test the two APIs we created.
First, let‘s get a JWT by making an HTTP POST request to
/api/token
endpoint and passing the following JSON in the request body:{"username": "mario", "password": "secret"}
This can be easily done with Postman or any HTTP client. For example, with
curl
this would be the command:curl -X POST -H ‘Content-Type: application/json‘-d ‘{"username": "mario", "password": "secret"}‘0:5000/api/token
As a response we will obtain a JSON like the following:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImJpcnRoZGF0ZSI6IjE5ODMtMDktMjMiLCJqdGkiOiJmZjQ0YmVjOC03ZDBkLTQ3ZTEtOWJjZC03MTY4NmQ5Nzk3NzkiLCJleHAiOjE1MTIzMjIxNjgsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NjM5MzkvIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo2MzkzOS8ifQ.9qyvnhDna3gEiGcd_ngsXZisciNOy55RjBP4ENSGfYI"
}
If we look at the value of the token, we will notice the three parts separated by a dot, as discussed at the beginning of this article.
Now we will try again to request the list of books, as in the previous section. However, this time we will provide the token as an authentication HTTP header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImJpcnRoZGF0ZSI6IjE5ODMtMDktMjMiLCJqdGkiOiJmZjQ0YmVjOC03ZDBkLTQ3ZTEtOWJjZC03MTY4NmQ5Nzk3NzkiLCJleHAiOjE1MTIzMjIxNjgsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NjM5MzkvIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo2MzkzOS8ifQ.9qyvnhDna3gEiGcd_ngsXZisciNOy55RjBP4ENSGfYI
Again, this can be easily done with Postman or any HTTP client. In
curl
, this would be the command:curl -H ‘Authorization: Bearer ‘$JWT 0:5000/api/books
Of course,
JWT
env variable must be set with the token received while signing in: JWT="eyJhbG..."
.This time we will get the list of books.
Handling JWT Claims on ASP.NET Core 2.0When introducing JWTs, we said that a token may contain some data called claims. These are usually information about the user that can be useful when authorizing the access to a resource. Claims could be, for example, user‘s e-mail, gender, role, city, or any other information useful to discriminate users while accessing to resources. We can add claims in a JWT so that they will be available while checking authorization to access a resource. Let‘s explore in practice how to manage claims in our ASP.NET Core 2 application.
Suppose that our list contains books not suitable for everyone. For example, it contains a book subject to age restrictions. We should include in the JWT returned after the authentication, an information about the user‘s age. To do that, let‘s update the
BuildToken
method of TokenController
as follows:private string BuildToken(UserModel user)
{var claims = new[] {
new Claim(JwtRegisteredClaimNames.Sub, user.Name),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Birthdate, user.Birthdate.ToString("yyyy-MM-dd")),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
The main differences with respect to the previous version concern the definition of the
claims
variable. It is an array of Claims
instances, each created from a key and a value. The keys are values of a structure (JwtRegisteredClaimNames
) that provides names for public standardized claims. We created claims for the user‘s name, email, birthday and for a unique identifier associated to the JWT.This
claims
array is then passed to the JwtSecurityToken
constructor so that it will be included in the JWT sent to the client.Now, let‘s take a look at how to change the API code in order to take into account the user‘s age when returning the list of books:
[Route("api/[controller]")]
public class BooksController : Controller
{
[HttpGet, Authorize]
public IEnumerable<
Book>
Get()
{
var currentUser = HttpContext.User;
int userAge = 0;
var resultBookList = new Book[] {
new Book { Author = "Ray Bradbury", Title = "Fahrenheit 451", AgeRestriction = false },
new Book { Author = "Gabriel Garcí
a Má
rquez", Title = "One Hundred years of Solitude", AgeRestriction = false },
new Book { Author = "George Orwell", Title = "1984", AgeRestriction = false },
new Book { Author = "Anais Nin", Title = "Delta of Venus", AgeRestriction = true }
};
if (currentUser.HasClaim(c =>
c.Type == ClaimTypes.DateOfBirth))
{
DateTime birthDate = DateTime.Parse(currentUser.Claims.FirstOrDefault(c =>
c.Type == ClaimTypes.DateOfBirth).Value);
userAge = DateTime.Today.Year - birthDate.Year;
}if (userAge <
18)
{
resultBookList = resultBookList.Where(b =>
!b.AgeRestriction).ToArray();
}return resultBookList;
}public class Book
{
public string Author { get;
set;
}
public string Title { get;
set;
}
public bool AgeRestriction { get;
set;
}
}
}
We added the
AgeRestriction
property to the Book class. It is a boolean value that indicates if a book is subject to age restrictions or not.When a request is received, we check if a claim
DateOfBirth
is associated with the current user. In case of affirmative, we calculate the user‘s age. Then, if the user is under 18, the list will contain only the books without age restrictions, else the whole list will be returned.You can test this new scenario by running the tests
GetBooksWithoutAgeRestrictions
and GetBooksWithAgeRestrictions
included in the project‘s source code or by issue the following curl
commands:# signing in
curl -X POST -H ‘Content-Type: application/json‘ -d ‘{username: "mary", password: "barbie"}‘ 0:5000/api/token# setting JWT variable (replace AAA.BBB.CCC with token received)
JWT="AAA.BBB.CCC"# getting books
curl -H ‘Authorization: Bearer ‘$JWT 0:5000/api/books
The last command will now send a list containing all books but the restricted one: Delta of Venus.
"Just learnt how to secure ASP.NET Core 2.0 apis."Enabling Cross-Origin Requests (CORS) in ASP.NET Core 2.0More often than not, we will want to specify that our API accepts requests coming from other origins (other domains). When issuing AJAX requests, browsers make preflights to check if a server accepts requests from the domain hosting the web app. If the response for these preflights don‘t contain at least the
TWEET THIS
文章图片
Access-Control-Allow-Origin
header specifying that accepts requests from the original domain, browsers won‘t proceed with the real requests (to improve security).To include support for CORS (and add this header alongside with a few more), we need to make two more changes in the
Startup
class. First, we need to add:services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.Build());
});
as the last invocation in the
ConfigureServices
method. Second, we need to add:app.UseCors("CorsPolicy");
Note that this basically makes our API accept requests from any origin. To make it more secure, we can change the
AllowAnyOrigin
with WithOrigins
and define a specific origin (e.g. https://mydomain.com
).Aside: Securing ASP.NET Core 2.0 with Auth0Securing ASP.NET Core 2.0 applications with Auth0 is easy and brings a lot of great features to the table. With Auth0, we only have to write a few lines of code to get solid identity management solution, single sign-on, support for social identity providers (like Facebook, GitHub, Twitter, etc.), and support for enterprise identity providers (like Active Directory, LDAP, SAML, custom, etc.).
With ASP.NET Core 2.0, we just need to create an API in our Auth0 Management Dashboard and change two things on our code. To create an API, we need to sign up for a free Auth0 account. After that, we need to go to the API section of the dashboard and click on "Create API". On the dialog shown, we can set the Name of our API as "Books", the Identifier as "http://books.mycompany.com", and leave the Signing Algorithm as "RS256".
文章图片
After that, we have to replace the call to
services.AddAuthentication
in the Startup
to:string domain = $"https://{Configuration["Auth0:Domain"]}/";
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Authority = domain;
options.Audience = Configuration["Auth0:Audience"];
});
And add the following element to
appsettings.json
:{
"Logging": {
// ...
},
"Auth0": {
"Domain": "bk-samples.auth0.com",
"Audience": "http://books.mycompany.com"
}
}
Note that the domain in this case have to be changed to the domain that we specified when creating our Auth0 account.
Testing the Integration
That‘s it. This is all we need to secure our ASP.NET Core 2.0 API with Auth0. However, to test this integration we need a client to communicate with our application. As the focus of this article is ASP.NET Core 2.0, we will use a generic web application that is secured with a configurable Auth0 application. All we need to configure in this application are the
clientID
, domain
, and audience
properties.To get the
clientID
and domain
properties, we need to create a new Application in the management dashboard. In the Applications section, we can click on "Create Application", name it as "Book Application" on the dialog shown, and choose "Single Page Web Applications" as the type. After creating the application, we have to go to the "Settings" tab and add http://auth0.digituz.com.br
in the "Allowed Callback URLs" field and hit "Save" (ctrl/command + s). In this same tab, we can fetch both properties that we are interested in (Client ID
and Domain
) and then add to the generic application. There, we can also set the audience to be the identifier of our API (i.e. http://books.mycompany.com
). Now we can hit "Sign In with Auth0" to authenticate ourselves.文章图片
After authenticating, we can use the web app to issue requests to the API (e.g.
http://localhost:5000/api/books
). As this web app automatically adds the access_token
generated in the authentication process in the Authorization
header, our API checks its validity and sends us the list of books.文章图片
SummaryIn this article, we had an overview of the JSON Web Token technology and introduced how to use it in ASP.NET Core 2. While developing a simple Web API application, we saw how to configure support for JWT authentication and how to create tokens on authentication. We also described how to insert claims into a JWT and how to use them while authorizing the access to a resource.
【Securing ASP.NET Core 2.0 Applications with JWTs】In conclusion, we experienced how easy it is to manage JSON Web Tokens in ASP.NET Core 2 applications.
推荐阅读
- 原创Android Retrofit学习之旅
- android webview设置和遇到的坑
- PrimeFaces Ajax poll例子
- PrimeFaces Ajax事件示例
- PrimeFaces Ajax下拉列表
- PrimeFaces Ajax ActionListener示例
- PrimeFaces Ajax使用示例
- PrimeFaces AccordionPanel实例
- PowerShell Write-Host写入主机