[JWT] Bearer Token in Asp.Net Core

[JWT] Bearer Token in Asp.Net Core

JWT (Json Web Token),主要是為了保護網路安全傳輸的資訊,透過 JWT 可以產生一把權杖,透過這把權杖,來驗證使用者的資訊,細節可以參考附錄4。

JWT Token (權杖)

權杖主要由三個部分所組成(Header, Payload,以及Signature)。

{{"alg":"HS256","typ":"JWT"}.{"nameid":"ROBERT.CHEN@RFF.COM","http://schemas.microsoft.com/ws/2008/06/identity/claims/role":"Admin","exp":1532966362,"iss":"Robert Chen","aud":"Robert For Fun"}}
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjYzNjIsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.5wl6YqmVI9IHSDSgFw_i1bW5OpZuiKGJ9s38V0HiGog"

表頭主要定義演算法,與權杖的類別。

{"alg":"HS256","typ":"JWT"}

上面的內容是指HMACSHA256演算法,權證的類別為"JWT"

Payload

定義權杖的內容

{"nameid":"ROBERT.CHEN@RFF.COM","http://schemas.microsoft.com/ws/2008/06/identity/claims/role":"Admin","exp":1532966362,"iss":"Robert Chen","aud":"Robert For Fun"}

在此的 Payload 總共由五個部分所組成

  1. nameid : 主要是用來加密的金鑰。
  2. role : 表示此權杖的角色是什麼。
  3. exp : 此權杖預期超時的時間。
  4. iss : 表示 issuer,代表此憑證發行者。
  5. aud : 表示 audience,代表此憑證合法的接收者。

Signature

透過 Header 與 Payload 的資訊,產生一個簽名,且此簽名不可逆。

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjYzNjIsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.5wl6YqmVI9IHSDSgFw_i1bW5OpZuiKGJ9s38V0HiGog"

擴充服務

由於系統可能會加載註冊很多服務到容器中,透過擴充方法,將JWT服務的內容移至擴充方法。

Note : 擴充方法是靜態類別,且第一個參數必須是 this 開始。

public static class ServicesExtension
    {
        public static void ConfigureJWT(this IServiceCollection services, IConfiguration config)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
                options => {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = config["Token:ValidIssuer"],
                        ValidAudience = config["Token:ValidAudience"],
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["Token:ValidSignKey"])),
                        RequireExpirationTime = true,
                    };
                }
                );
        }
    }

註冊服務

將JWT註冊至服務容器中,並且啟用認證機制。

public void ConfigureServices(IServiceCollection services)
        {
        ...
            services.ConfigureJWT(Configuration);
        ...
        }

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            ...
            app.UseAuthentication();
            ...
            app.UseMvc();
        }

application.json

預設把權證的主要資訊,寫在 application.json 中,透過它來控制預設的設定檔。

{
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "ConnectionString": {
    "Default": "..."
  },
  "Token": {
    "ValidIssuer": "Robert Chen",
    "ValidAudience": "Robert For Fun",
    "ValidSignKey" :  "Robert.Chen@RFF.Com"
  }
}

認證

當要存取某個 API 時,希望該 API 需要經過授權認證,只需要對於該項控制器(Controller)的方法(Action),設定屬性即可。

// HTTPGet 屬性,無須授權。
[HttpGet]
// 只要有授權即可
[HttpGet, Authorize]
// 授權並驗證授權的角色
[HttpGet, Authorize(Roles = "Admin")]

當然,也可以對整個控制項做授權控制。

[Authorize]
public class XXXController : Controller
{
...
}

Note : 屬性可以針對控制項或是方法去做設定。

取得權證

經過屬性的設定,對於某個控制項或是方法限制後,需要取得授權,才可以呼叫該控制項或是該方法。 建立新的控制來存取權證。

    [Produces("application/json")]
    [Route("api/Auth")]
    public class AuthController : Controller
    {
        private bool IsValidUsername(string username)
        {
            return String.IsNullOrEmpty(username);
        }

        private bool IsValidPassword(string password)
        {
            return String.IsNullOrEmpty(password);
        }

        private IConfiguration _config;
        public AuthController(IConfiguration config)
        {
            _config = config;
        }

        [AllowAnonymous]
        [HttpPost]
        public IActionResult Login([FromBody]LoginModel user)
        {
            Claim[] claims = null;

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

            if (IsValidUsername(user.Username) || IsValidPassword(user.Password))
                return BadRequest();

            if (IsValidAdmin(user) && IsValidPassword(user))
            {
                claims = new[] {
                   new Claim(JwtRegisteredClaimNames.NameId, "ROBERT.CHEN@RFF.COM"),
                   new Claim(ClaimTypes.Role, "Admin")
                };
            }
            else if (user.Username == "John" && user.Password == "test1234")
            {
                claims = new[] {
                   new Claim(JwtRegisteredClaimNames.NameId, "John@RFF.COM"),
                   new Claim(ClaimTypes.Role, "others")
                };
            }
            else {
                return BadRequest();
            }

            var signKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Token:ValidSignKey"]));
            var signCredentials = new SigningCredentials(signKey, SecurityAlgorithms.HmacSha256);
            var tokenOptions = new JwtSecurityToken(
                issuer: "Robert Chen",
                audience: "Robert For Fun",
                claims: claims,
                expires: DateTime.Now.AddMinutes(5),
                signingCredentials: signCredentials
                );
            var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);

            return Ok(new { Token = tokenString });

        }

        private static bool IsValidPassword(LoginModel user)
        {
            return string.Compare(user.Password, "12345", true) == 0;
        }

        private static bool IsValidAdmin(LoginModel user)
        {
            return String.Compare(user.Username, "Robert", true) == 0;
        }
    }

Note : 如果沒有對控制項的方法設定路由條件,即直接呼叫該控制項即可。(http://xxx:ooo/api/Auth) 若是有設定路由屬性([Route("login")],則 URL 為 http://xxx:ooo/api/Auth/Login

憑證參數選項

The issuer is the actual server that created the token (ValidateIssuer=true)|實際的驗證伺服器

The receiver of the token is a valid recipient (ValidateAudience=true)|權證的接收者

The token has not expired (ValidateLifetime=true)|權證的有效期限

The signing key is valid and is trusted by the server (ValidateIssuerSigningKey=true)|權證是否有效以及此權證是否經過伺服器信任

Issuer: The first parameter is a simple string representing the name of the web server that issues the token|伺服器名稱

Audience: The second parameter is a string value representing valid recipients|合法的接受者

Claims: The third argument is a list of user roles, for example, the user can be an admin, manager or author (we are going to add roles in the next post)|使用者的角色資訊

Expires: The fifth argument is DateTime object that represents the date and time after which the token expires|憑證多久後會預期

取得憑證

將相關資訊填好後,送交給驗證API,API驗證完後,會發行驗證後的 Token。

--- Post ---
POST /api/auth HTTP/1.1
Host: localhost:55747
Content-Type: application/json
Cache-Control: no-cache
Postman-Token: 6e2feb5e-4e1b-4e7b-88f7-a1592efc2509

{
"UserName" : "Robert",
"Password" : "12345"
}
--- End of Post
--- Response ---
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjkyMTEsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.cugbDM-jf5R54FLUU6NDB9zNHfCOOgVfpzBv9lPzhXs"
}

填入權杖到表頭中

由於我們使用的是 JWT,表頭中需填入 Authorization : Bearer $token

GET /api/values HTTP/1.1
Host: localhost:55747
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJST0JFUlQuQ0hFTkBSRkYuQ09NIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE1MzI5NjU4NTgsImlzcyI6IlJvYmVydCBDaGVuIiwiYXVkIjoiUm9iZXJ0IEZvciBGdW4ifQ.R50uTvyjQWMIhge8nb2KgYK_Sh1W5qCHNIPKMZ4G6Fg
Cache-Control: no-cache
Postman-Token: 72baa000-f91c-457c-b635-9ec8d8db8df6

Reference

  1. ASP.NET Core Authentication with JWT and Angular - Part1
  2. JWT Validation and Authorization in ASP.NET Core
  3. JWT JSON Web Token 使用 ASP.NET Core 2.0 Web API 的逐步練習教學與各種情境測試
  4. JWT

留言

這個網誌中的熱門文章

[Tools] GCOV & LCOV 初探

Quilt Patch 管理操作方法

[C#]C# Coding 規則