ASP.NET Core Web API Token-based Authentication with 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)

2018-04-06_16-41-03

โครงสร้าง Token จะคั่นด้วยจุด . (dot) ประกอบไปด้วย 3 ส่วนดังนี้

  1. Header ปกติแล้วจะเก็บ 2 ส่วนคือ Encyption Algorithm กับ Type ตัวอย่างเช่น
{
  "alg": "HS256",
  "typ": "JWT"
}
  1. Payload จะเก็บข้อมูลเช่น iss (issuer), exp (expiration time), sub (subject), aud (audience)
{
  "sub": "codebankok",
  "jti": "94b5b618-34d4-4270-bead-8fb3614d1e3e",
  "exp": 1525597010,
  "iss": "Surasuk Oakkharamonphong",
  "aud": "Bond"
}
  1. Signature คือส่วนที่เข้ารหัส Header กับ Payload เก็บไว้เป็น Signature นั่นเอง

สามารถเข้าไปทดสอบ Token ได้ด้วยว่า Token นั้น Valid หรือไม่ได้ที่ https://jwt.io

อ่านข้อมูลเพิ่มเติมเรื่อง JWT ได้ที่ https://jwt.io/introduction


บทความนี้ผมจะนำเสนอการนำ JWT มาทำ Authentication ให้กับ ASP.NET Core Web API เพื่อป้องกันผู้ไม่พึงประสงค์เข้ามา Request API โดยพละการ

  1. New ASP.NET Core Web Application Project

  1. เลือก API

  1. ติดตั้ง Nuget Package เพิ่ม 2 ตัว

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

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

{
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "JwtKey": "1234567890123456",
  "JwtIssuer": "Surasuk Oakkharamonphong",
  "JwtAudience": "Bond",
  "JwtExpireDays": 30
}

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

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "JwtKey": "1234567890123456",
  "JwtIssuer": "Surasuk Oakkharamonphong",
  "JwtAudience": "Bond",
  "JwtExpireDays": 30
}
  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 Method

// ===== 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 Method

app.UseAuthentication();

สรุปไฟล์ Startup.cs ทั้งหมดเป็นแบบนี้

using System; 
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace CBWebApiJwt
{
    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)
        {
            // ===== 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
                    };
                });

            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();
        }
    }
}
  1. เพิ่ม API Controller - Empty มา 1 ตัว ใช้ชื่อ JwtController

2018-04-06_17-22-47

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

เพิ่ม Using Namespace ดังนี้

using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;

แก้ Route เป็น

[Route("api/[controller]/[action]")]

เพิ่ม Contructor และฟิว _configuration ใช้สำหรับเรียกค่า Config มาจากไฟล์ appsettings.json

private readonly IConfiguration _configuration;

public JwtController(IConfiguration configuration)
{
    _configuration = configuration;
}

เพิ่ม GetToken Method ใช้สำหรับสร้าง Token และจะเก็บ email ไว้ที่ Payload ด้วย

[HttpGet]
public string GetToken(string email)
{
    var claims = new List<Claim>
    {
        new Claim(JwtRegisteredClaimNames.Sub, email),
        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
    };

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

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

    return new JwtSecurityTokenHandler().WriteToken(token);
}

เพิ่ม GetData Method ใข้สำหรับดึงข้อมูล โดยจะระบุ Authorize ไว้ด้วย (ถ้า Token ไม่มีหรือผิดจะไม่สามารถเรียกได้)

[Authorize]
[HttpGet]
public IEnumerable<string> GetData()
{
    return new string[] { "value1", "value2" };
}

สรุปไฟล์ JwtController.cs มีดังนี้

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;

namespace CBWebApiJwt.Controllers
{
    [Produces("application/json")]
    [Route("api/[controller]/[action]")]
    public class JwtController : Controller
    {
        private readonly IConfiguration _configuration;

        public JwtController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [HttpGet]
        public string GetToken(string email)
        {
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Sub, email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            };

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

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

            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        [Authorize]
        [HttpGet]
        public IEnumerable<string> GetData()
        {
            return new string[] { "value1", "value2" };
        }
    }
}
  1. ทดสอบรันโปรแกรมดู โดยให้เข้าไปที่ http://localhost:50095/api/jwt/GetToken?email=noreply@codebangkok.com (แก้ port กับ email กันเอาเองนะ)

2018-04-06_17-43-58

  1. ก๊อปปี้ Token ที่ได้ไปทดสอบที่เวป https://jwt.io ติ๊กตรง secret base64 encoded ด้วยนะ จะขึ้นว่า Signature Verified และจะเห็นข้อมูลใน Header กับ Payload ตามที่เราได้ Config เอาไว้

  1. ทดสอบเรียก GetData ด้วยโปรแกรม Advance Rest Client โหลดได้ที่ https://install.advancedrestclient.com (ใช้โปรแกรมอื่นที่ไกล้เคียงก็ได้นะ เช่น Postman) ตั้งค่าดังนี้ แล้วกดปุ่ม SEND
Method = Get
Request URL = http://localhost:50095/api/jwt/GetData 
Header name = Authorization
Header value = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJub3JlcGx5QGNvZGViYW5na29rLmNvbSIsImp0aSI6IjljM2ZiMGRmLWYwYTEtNGM0NC1iODEyLTAyNGExOGZjNTlkYSIsImV4cCI6MTUyNTYwMzEyNCwiaXNzIjoiU3VyYXN1ayBPYWtraGFyYW1vbnBob25nIiwiYXVkIjoiQm9uZCJ9.-NRMbf4UmRHjT799SjzslmLFVk1dFWuN23qJoFNOIVU

  1. ทดลอง ลบ Token ออกหรือ แก้ไข Token ก็จะไม่สามารถเรียก GetData ได้

Demo: https://github.com/CodeBangkok/CBWebApiJwt
Source: https://medium.com/@ozgurgul/asp-net-core-2-0-webapi-jwt-authentication-with-identity-mysql-3698eeba6ff8

ปล. ขอบคุณ @AkeDev ที่แนะนำให้ผมศึกษาเรื่อง JWT ที่พึ่งเคยได้สัมผัสเป็นครั้งแรก ก็ประทับใจเลยทีเดียว โปรดติดตามบทความต่อๆไปของ JWT ได้ที่นี่นะครับ

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

https://www.lazada.co.th/products/codebangkok-i219874509-s334754949.html

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

2 Likes