ASP.NET Core Web API Authentication with Identity Framework and JWT

JWT (JSON Web Token) คือรูปแบบใหม่ในการยืนยันตัวตน (Authentication) ด้วย Token นั่นเอง

โดยปกติแล้วการ Authentication ในรูปแบบเดิมๆ จะเป็นในรูปแบบ Server Based Authentication จะต้องใช้ Session กับ Cookie เป็นตัวจัดการ ซึ่ง Session ID นั้นจะต้องถูกจัดเก็บอยู่ในรูปแบบของ Memory ทำให้ Scale ยาก หรือเก็บลง Database ทำให้ Performance ตก

JWT (JSON Web Token) เป็นมาตรฐานเปิด (RFC 7519) ที่เข้ามาแก้ปัญหาเหล่านี้ ด้วยการใช้ Token ที่เข้ารหัสแล้วส่งผ่านมาทาง Header แล้วถอดรหัสเช็คได้เลยว่าถูกต้องมั้ย ซึ่งถ้าทำแบบนี้ได้ก็ไม่ต้องพึ่ง Session อีกต่อไปแล้ว สามารถเอาไปใช้กับ Mobile App ได้ด้วย เพราะมันมีขนาดกระทัดรัด (Compact) และเก็บข้อมูลภายในตัวได้ (Self-contained)

ท่านสามารถอ่านบทความเก่าได้ที่นี่ ASP.NET Core Web API Token-based Authentication with JWT

บทความนี้ผมจะมานำเสนอการทำงานร่วมกันของ ASP.NET Web API + Identity Framework + JWT

ASP .NET Core Web API and Identity Framework

  1. สร้างโปรเจ็คด้วยคำสั่ง
dotnet new webapi -o CBAuthenJwt

เข้าไปใน Project Directory แล้วเปิดโปรแกรม Visual Studio Code ด้วยคำสั่ง

cd CBAuthenJwt
code .

  1. สร้างไฟล์ AppDbContext.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public class AppDbContext : IdentityDbContext<IdentityUser>
{
    public AppDbContext(DbContextOptions options) : base(options)
    {

    }
}

3, สร้าง SQL Database มา 1 ตัว (Cloud หรือ On-Premise ก็ได้)

  1. แก้ไขไฟล์ Startup.cs

ที่ Using Namespace

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;

ที่ ConfigureServices

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer("Server=codebangkok.database.windows.net; Database=CBAngular; User ID=xxx; Password=xxx");               
});

services
    .AddDefaultIdentity<IdentityUser>()
    .AddEntityFrameworkStores<AppDbContext>();
  1. เปิด Terminal หรือ Command Prompt เข้าไปที่ Project Directory แล้วพิมพ์คำสั่ง

Entity Framework คำสั่ง Migrations ตั้งชื่อว่า Init

dotnet ef migrations add Init

Entity Framework คำสั่ง Database Update สำหรับสร้าง Table ที่จำเป็น

dotnet ef database update

  1. เข้าไปดู Table ที่ Database ก็จะเห็นดังนี้

2018-06-08_00-54-18


JWT Authentication
ขั้นตอนนี้จะเหมือนกับ ASP.NET Core Web API Token-based Authentication with JWT

  1. ติดตั้ง JwtBearer Package ด้วยคำสั่งนี้
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

  1. แก้ไขไฟล์ appsettings.json และ appsettings.Development.json เพิ่มคำสั่ง (ไปแก้ไขค่ากันเองนะ)
"JwtKey": "1234567890123456",
"JwtIssuer": "Surasuk Oakkharamonphong",
"JwtAudience" :  "Bond",
"JwtExpireSeconds": 7200

ลงในไฟล์ appsettings.json ต่อจากคำสั่งเดิมดังนี้

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "JwtKey": "1234567890123456",
  "JwtIssuer": "Surasuk Oakkharamonphong",
  "JwtAudience" :  "Bond",
  "JwtExpireSeconds": 7200
}

และลงในไฟล์ appsettings.Development.json ต่อจากคำสั่งเดิมดังนี้

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "JwtKey": "1234567890123456",
  "JwtIssuer": "Surasuk Oakkharamonphong",
  "JwtAudience" :  "Bond",
  "JwtExpireSeconds": 7200
}
  1. แก้ไขไฟล์ Startup.cs

เพิ่ม Using Namespace

using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

เพิ่มส่วน Jwt Authentication เข้าไปใน ConfigureServices

// ===== Add Jwt Authentication ========
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
    .AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

    })
    .AddJwtBearer(cfg =>
    {
        cfg.RequireHttpsMetadata = false;
        cfg.SaveToken = true;
        cfg.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = Configuration["JwtIssuer"],
            ValidAudience = Configuration["JwtAudience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
            ClockSkew = TimeSpan.Zero // remove delay of token when expire
        };
    });

เพิ่มคำสั่งนี้เข้าไปใน Configure

app.UseAuthentication();
  1. สร้างไฟล์ AccountController.cs ที่ Controllers
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;

[Route("api/[controller]/[action]")]
public class AccountController : Controller
{
    IConfiguration config;
    UserManager<IdentityUser> userManager;

    public AccountController(IConfiguration config, UserManager<IdentityUser> userManager)
    {
        this.config = config;
        this.userManager = userManager;
    }

    [HttpPost]
    public async Task<IActionResult> Register(string email, string password)
    {
        var userIdentity = new IdentityUser(email);
        var result = await userManager.CreateAsync(userIdentity, password);

        if (result == IdentityResult.Success)
        {
            return await Login(email, password);
        }
        else return BadRequest();
    }

    [HttpPost]
    public async Task<IActionResult> Login(string email, string password)
    {
        // get the user to verifty
        var userToVerify = await userManager.FindByNameAsync(email);

        if (userToVerify == null) return BadRequest();

        // check the credentials
        if (await userManager.CheckPasswordAsync(userToVerify, password))
        {
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["JwtKey"]));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var expires = DateTime.Now.AddSeconds(Convert.ToDouble(config["JwtExpireSeconds"]));

            var token = new JwtSecurityToken(
                issuer: config["JwtIssuer"],
                audience: config["JwtAudience"],
                claims: claims,
                expires: expires,
                signingCredentials: creds
            );

            return Ok(new JwtSecurityTokenHandler().WriteToken(token));
        }
        else return BadRequest();
    }

    [Authorize]
    [HttpGet]
    public IActionResult GetData()
    {
        var products = new List<Product>();
        products.Add(new Product { Id = 1, Name = "iPhone"} );
        products.Add(new Product { Id = 2, Name = "Android"} );
        return Json(products);
    }

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}
  1. ทดสอบรันเวปด้วยคำสั่ง
dotnet watch run

แล้วเปิดโปรแกรมเพื่อทดสอบ ผมใช้ Advance Rest Client

ทดสอบ Register

ทดสอบ Login

ทดสอบ Login ไม่ผ่าน

ทดสอบเอา Token ที่ได้ไปเช็คที่ https://jwt.io จะขึ้นว่า Signature Verified

เรียก GetData ด้วย Token

เรียก GetData โดยไม่มี Token


บริจาค (Donate) ด้วย Crypto Currency ให้กับ CodeBangkok ได้ที่
BTC = 3GDxhb84ho2jmAV9seAgAFJ7dy1XR3GCyc
ETH = 0x119fa8A618A0283D1834853325A8FF4fe1101230
LTC = ME2abSdDeQYuTmzZSAnHL7LGGeF836d1ut
ZEC = t1Y4NkK3Dx3yBbwCVdpXzKYrqUSJSHgaFXa

https://www.lazada.co.th/products/c-net-core-i221844611-s338593893.html?spm=a2o6z.10453683.17.2.77ba30028gg3mg&mp=3

สอบถามครับ
ของผม login ได้ปกติครับ, แต่ตอน getdata มันแจ้งแบบในรูปอ่ะครับ