Adding Authentication to ASP.NET Core
[code]
> dotnet add package JWT
> dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
> dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
[/code]
Configuring JWT Properties
appsettings.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=grocery;Trusted_Connection=True;MultipleActiveResultSets=true" }, "JWTSettings": { "SecretKey": "jwts-are-awesome", "Issuer": "dotnet_grocery_list", "Audience": "GroceryListAPI" } } |
สร้างไฟล์ JWTSettings.cs ไว้ที่ root directory
JWTSettings.cs
1 2 3 4 5 6 7 8 9 |
namespace grocery { public class JWTSettings { public string SecretKey { get; set; } public string Issuer { get; set; } public string Audience { get; set; } } } |
Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext<GroceryListContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings")); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } |
Enabling User Registration
สร้างไฟล์ Controllers/AccountController.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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
using grocery.Models; using JWT; using JWT.Algorithms; using JWT.Serializers; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace grocery.Controllers { [Route("api/[controller]")] public class AccountController : Controller { private readonly UserManager<IdentityUser> _userManager; private readonly SignInManager<IdentityUser> _signInManager; private readonly JWTSettings _options; public AccountController( UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IOptions<JWTSettings> optionsAccessor) { _userManager = userManager; _signInManager = signInManager; _options = optionsAccessor.Value; } [HttpPost] public async Task<IActionResult> Register([FromBody] Credentials Credentials) { if (ModelState.IsValid) { var user = new IdentityUser { UserName = Credentials.Email, Email = Credentials.Email }; var result = await _userManager.CreateAsync(user, Credentials.Password); if (result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent: false); return new JsonResult(new Dictionary<string, object> { { "access_token", GetAccessToken(Credentials.Email) }, { "id_token", GetIdToken(user) } }); } return Errors(result); } return Error("Unexpected error"); } private string GetIdToken(IdentityUser user) { var payload = new Dictionary<string, object> { { "id", user.Id }, { "sub", user.Email }, { "email", user.Email }, { "emailConfirmed", user.EmailConfirmed }, }; return GetToken(payload); } private string GetAccessToken(string Email) { var payload = new Dictionary<string, object> { { "sub", Email }, { "email", Email } }; return GetToken(payload); } private string GetToken(Dictionary<string, object> payload) { var secret = _options.SecretKey; payload.Add("iss", _options.Issuer); payload.Add("aud", _options.Audience); payload.Add("nbf", ConvertToUnixTimestamp(DateTime.Now)); payload.Add("iat", ConvertToUnixTimestamp(DateTime.Now)); payload.Add("exp", ConvertToUnixTimestamp(DateTime.Now.AddDays(7))); IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); return encoder.Encode(payload, secret); } private JsonResult Errors(IdentityResult result) { var items = result.Errors .Select(x => x.Description) .ToArray(); return new JsonResult(items) { StatusCode = 400 }; } private JsonResult Error(string message) { return new JsonResult(message) { StatusCode = 400 }; } private static double ConvertToUnixTimestamp(DateTime date) { DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); TimeSpan diff = date.ToUniversalTime() - origin; return Math.Floor(diff.TotalSeconds); } } } |
สร้างไฟล์ Models/Credentials.cs
Models/Credentials.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.ComponentModel.DataAnnotations; namespace grocery.Models { public class Credentials { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } } } |
สร้างไฟล์ Data/UserDbContext.cs
Data/UserDbContext.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace grocery.Data { public class UserDbContext : IdentityDbContext<IdentityUser> { public UserDbContext(DbContextOptions<UserDbContext> options) : base(options) { Database.EnsureCreated(); } } } |
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 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 |
using grocery.Data; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace grocery { 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.AddDbContext<UserDbContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<IdentityUser, IdentityRole>() .AddEntityFrameworkStores<UserDbContext>(); services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); services.AddDbContext<GroceryListContext>(options => options.UseSqlServer( Configuration.GetConnectionString("DefaultConnection"))); services.Configure<JWTSettings>(Configuration.GetSection("JWTSettings")); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } // 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(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); //app.UseIdentity(); app.UseAuthentication(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } } |
Perform migration
ใช้คำสั่งนี้จะ Eror แบบนี้ เป็นเพราะมี DbContext
มากกว่าหนึ่ง
[code]
> dotnet ef migrations add "Config JWT"
More than one DbContext was found. Specify which one to use. Use the ‘-Context’ parameter for PowerShell commands and th
e ‘–context’ parameter for dotnet commands.
[/code]
ดังนั้นให้ระบุ DbContext
ไปเลยว่าใช้ตัวไหน
[code]
> dotnet ef migrations add "Config JWT" –context UserDbContext
> dotnet ef database update –context UserDbContext
[/code]
รัน F5
[code]
> dotnet run
[/code]
Register ผู้ใช้งาน
ใช้ Postman ทำการ POST ไปที่
https://localhost:5001/api/account
พารามิเตอร์ส่งใน body เป็น
[code]
{
"email": "mr.phaisarn@gmail.com",
"password": "123456#User"
}
[/code]
จะได้
[code]
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtci5waGFpc2FybkBnbWFpbC5jb20iLCJlbWFpbCI6Im1yLnBoYWlzYXJuQGdtYWlsLmNvbSIsImlzcyI6ImRvdG5ldF9ncm9jZXJ5X2xpc3QiLCJhdWQiOiJHcm9jZXJ5TGlzdEFQSSIsIm5iZiI6MTUzOTc4Nzg4Mi4wLCJpYXQiOjE1Mzk3ODc4ODIuMCwiZXhwIjoxNTQwMzkyNjgyLjB9.Ssw96pKsqgUZB7Ay75mx60jYdFku_MvCDyha136gdgU",
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImFjZWE1NWRiLWE5MmQtNGM5Ni1iNGEwLTA0NDE2YWU2NGMxYyIsInN1YiI6Im1yLnBoYWlzYXJuQGdtYWlsLmNvbSIsImVtYWlsIjoibXIucGhhaXNhcm5AZ21haWwuY29tIiwiZW1haWxDb25maXJtZWQiOmZhbHNlLCJpc3MiOiJkb3RuZXRfZ3JvY2VyeV9saXN0IiwiYXVkIjoiR3JvY2VyeUxpc3RBUEkiLCJuYmYiOjE1Mzk3ODc4ODIuMCwiaWF0IjoxNTM5Nzg3ODgyLjAsImV4cCI6MTU0MDM5MjY4Mi4wfQ._fIqx3oDpHy6KKKk-CVez9vBzXO2TClaZK1sXEFA-rE"
}
[/code]