Securing ASP.NET Core 2.0 Applications with JWTs
โหลดตัวอย่างมาจาก Git Using JWTs in .NET Core 2
เปิดขึ้นมาแล้วลองรัน
browser จะชี้ไปที่ http://localhost:63939/api/books ซึ่งจะ Error ดังนี้
This page isn’t working
If the problem continues, contact the site owner.
HTTP ERROR 401
แล้วถ้าเรียกไปที่ http://localhost:63939/api/token ก็จะ Error แบบนี้
This localhost page can’t be found
No webpage was found for the web address: http://localhost:63939/api/token
Search Google for localhost 63939 api token
HTTP ERROR 404
เป็นเพราะไม่ได้ส่งพารามิเตอร์ที่จำเป็นไปให้
ทีนี้ลองใช้ Postman เรียกไปที่ http://localhost:63939/api/token
พร้อมส่งค่าไปดังนี้
[code]
{
"username": "mario",
"password": "secret"
}
[/code]
จะได้ค่าคืนมาเป็นแบบข้างล่างนี้ก็เป็นอันใช้ได้
[code]
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImJpcnRoZGF0ZSI6IjI1MjYtMDktMjMiLCJqdGkiOiJhYzdlYWRhZS02YzI4LTRlMDctOWM2OC1mMjAxMjNjYWY1OTUiLCJleHAiOjE1MzU1MzI2NTUsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NjM5MzkvIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo2MzkzOS8ifQ.dCmNVlejU4kjQI1NNZRbBPXZ19AKQmWwekHT8aSCAsY"
}
[/code]
ASP.NET Core 2 provides native support to JSON Web Tokens.
In this article, we will take a look at how to enable JWTs when creating a Web API application based on ASP.NET Core 2.
A Quick Introduction to JWTs
JSON Web Tokens, often shortened with JWTs
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.
JWT technology allows a client to store session data on its side and to provide the token to the server whenever it tries to access a protected resource. Usually, the token is sent to the server in the Authorization HTTP header using the Bearer schema, and it should contain all the information that allows to grant or deny access to the resource.
Securing ASP.NET Core 2.0 Applications with JWTs
สร้างโปรเจ็กส์แบบ ASP.NET Core Web Application
ชื่อโปรเจ็กส์ JWT เลือก API แบบ No Authen
ทดลองรันดู จะขึ้นหน้า https://localhost:44307/api/values
และคืนค่า
[code]
["value1","value2"]
[/code]
Startup.cs
แก้ไขไฟล์ Startup.cs
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System.Text; 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 add services to the container. 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(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseMvc(); } } } |
appsettings.json
แก้ไขไฟล์ appsettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" } }, "Console": { "LogLevel": { "Default": "Warning" } } }, "Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:62763/" } } |
ลองรันอีกทีก็ยังใช้ได้ปกติ
Securing ASP.NET Core 2.0 Endpoints with JWTs
สร้าง BooksController เปล่าขึ้นมาแล้ว ลบ ValuesController
เมื่อรันจะเข้า https://localhost:44307/api/values แล้ว Error ว่า
This localhost page can’t be found
No webpage was found for the web address: https://localhost:44307/api/values
HTTP ERROR 404
เพราะ ValuesController ลบไปแล้ว
Properties/launchSettings.json
แก้ไขไฟล์ Properties/launchSettings.json
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 |
{ "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62763", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "api/books", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "JWT": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/books", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:62763/" } } } |
เมื่อรันจะเรียกไปที่ http://localhost:62763/api/books
แต่ Error ว่า
This localhost page can’t be found
No webpage was found for the web address: http://localhost:62763/api/books
Search Google for localhost 62763 api books
HTTP ERROR 404
แก้ไขไฟล์ BooksController.cs
แก้ไขไฟล์ BooksController.cs
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 37 38 39 40 41 42 43 44 45 46 |
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; 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; } } } } |
บรรทัดที่ 13 : กำหนดให้ต้อง Authorize ถึงจะเข้าใช้งานเมธอด Get ได้
บรรทัดที่ 14 : ประกาศเมธอด Get
เมื่อรันจะเรียกไปที่ http://localhost:62763/api/books
และขึ้น Error ว่า
This page isn’t working
If the problem continues, contact the site owner.
HTTP ERROR 401
ซึ่งถูกแล้ว เพราะที่ BooksController มีการใช้ [HttpGet, Authorize] ทำให้การใช้งานต้องส่ง Token ไปด้วยถึงจะใช้งานได้
Creating JWT on Authentication
แก้ไขไฟล์ TokenController.cs
สร้างและแก้ไขไฟล์ TokenController.cs
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
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 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); } private UserModel Authenticate(LoginModel login) { UserModel user = null; if (login.Username == "mario" && login.Password == "secret") { user = new UserModel { Name = "Mario Rossi", Email = "mario.rossi@domain.com", Birthdate = new DateTime(1983, 9, 23) }; } if (login.Username == "mary" && login.Password == "barbie") { user = new UserModel { Name = "Mary Smith", Email = "mary.smith@domain.com", Birthdate = new DateTime(2001, 5, 13) }; } 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; } } } } |
ทีนี้ลองใช้ Postman เรียกไปที่ http://localhost:62763/api/token
พร้อมส่งค่าไปดังนี้
[code]
{
"username": "mario",
"password": "secret"
}
[/code]
เลือกเมธอด POST ตั้งค่า Header เป็น Content-Type: application/json (น่าจะ default อยู่แล้ว)
Body ใส่ค่า json ของ username และ password ลงไป
จะได้ค่าคืนมาเป็นแบบข้างล่างนี้
[code]
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImJpcnRoZGF0ZSI6IjI1MjYtMDktMjMiLCJqdGkiOiI5NGE1YWY5NC0zYjVkLTQ3MzQtYTUxZC1jYjNjNThhOGYwZWMiLCJleHAiOjE1MzU1MzY3NDYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQzMDcvIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo0NDMwNy8ifQ.itv-IFTvbuA0HkVFvzNlN6FmicMfdzDEdAJhPqyVrGM"
}
[/code]
Note: ใน Postman ตรง Test Results มีขึ้นแจ้งว่า There was an error in evaluating the test script: SyntaxError: Unexpected token : ไม่ต้องตกใจ ตัวอย่างก็ขึ้น 555
หรือจะใช้คำสั่งแทน Postman เช่น
[code]
curl -X POST -H ‘Content-Type: application/json’ \
-d ‘{"username": "mario", "password": "secret"}’ \
0:5000/api/token
[/code]
แต่อันนี้ยังไม่ได้ลอง (แค่เอาไว้เปรียบเทียบ)
โดยค่าของ token ที่ได้กลับมาจะมี 3 ส่วนคือ Header, Payload และ Signature แต่ละส่วนแบ่งด้วยจุด
จากนั้นนำค่า token ที่ได้มาไปเรียกใช้งาน http://localhost:62763/api/books
[code]
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImJpcnRoZGF0ZSI6IjI1MjYtMDktMjMiLCJqdGkiOiI5NGE1YWY5NC0zYjVkLTQ3MzQtYTUxZC1jYjNjNThhOGYwZWMiLCJleHAiOjE1MzU1MzY3NDYsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDQzMDcvIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo0NDMwNy8ifQ.itv-IFTvbuA0HkVFvzNlN6FmicMfdzDEdAJhPqyVrGM
[/code]
โดยสามารถส่งจาก Postman ได้ 2 แบบ คือ
1. ส่ง Token ที่แทบ Header
ส่งด้วยเมธอด Get
ไปที่ Headers ยกเลิก Content-Type แล้วเพิ่ม Authorization และกำหนดค่าเป็น Bearer ตามด้วย ค่าของ Token
2. ส่ง Token ที่แทบ Authorization
ส่งด้วยเมธอด Get
ยกเลิกค่าใน Headers ทั้งหมด
ไปที่ Authorization เลือก Type เป็น Bearer Token แล้วใส่ค่า Token
จะได้ข้อมูลส่งกลับดังนี้
[code]
[
{
"author": "Ray Bradbury",
"title": "Fahrenheit 451",
"ageRestriction": false
},
{
"author": "Gabriel García Márquez",
"title": "One Hundred years of Solitude",
"ageRestriction": false
},
{
"author": "George Orwell",
"title": "1984",
"ageRestriction": false
},
{
"author": "Anais Nin",
"title": "Delta of Venus",
"ageRestriction": true
}
]
[/code]
การส่งโดยใช้คำสั่งทำได้ดังนี้
[code]
curl -H ‘Authorization: Bearer ‘$JWT 0:5000/api/books
[/code]
โดย $JWT คือ Token ที่ได้มา
Handling JWT Claims on ASP.NET Core 2.0
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.
Enabling Cross-Origin Requests (CORS) in ASP.NET Core 2.0
Aside: Securing ASP.NET Core 2.0 with Auth0
Link
- Securing ASP.NET Core 2.0 Applications with JWTs
- JWT คืออะไร ?
- ASP.NET Core Authentication with JWT and Angular – Part 1
- Token Authentication in ASP.NET Core 2.0 – A Complete Guide
- Asp.NET Core 2.0 WebApi JWT Authentication with Identity & MySQL
Link – Roles
- Using Roles with the ASP.NET Core JWT middleware ไม่แน่ใจว่า .NET Core 1 รึเปล่า เพราะ Published ตั้งแต่ 12 July 2016
- ASP.NET Core JWT mapping role claims to ClaimsIdentity
- ASP.Net Core Identity JWT Role-Base Authentication is Forbidden
- QuotAPI