Adding JWT Authentication & Authorization #9

출처: https://youtu.be/mgeuh8k3I4g

그동안 TIM Corey 선생님의 강좌를 clone 하다가 다른분도 해봅니다.
Nick Chapsas 의 인증 ,허가의 관한 강좌입니다.
제가 앞으로 권한 ,인증의 관련 업무(?)를 할것 같아 공부중이기도 하고요

chater 1

jwt 설명
출처: https://youtu.be/qDJYgGzmalQ

보통 사용자가 웹사이트의 접근할때 사용허가를 받아야 합니다.
과거 모노롤틱 개발에서 인증을 통과한 사용자는 식별값은 보통 서바가 세션에 저장하는것이 보통이었습니다.

이때 사용자는 서버에게 요청을 보낼때 자식을 구분하는 Key가 있느데 보통
cookie 값을 사용합니다. 사용자의 cookie를 서버 세션 키값과 매칭해서
사용자를 식별합니다. 전통적인 인증 방식입니다.

하지만 웹사이트가 확장을 한다고 했을때 이 세션은 공유되기 어렵습니다.
(성능도 안좋고요)

이 문제를해결하기 위해 stateless 로 서버에서 사용자 상태값을 따로 저장하지 않고
해쉬값으로 사용자를 체크하는 방식을 개발합니다.
이건 해당 서버에서만 사용가능한 열쇠를 사용자가 가지고 다닌다는 의미죠

이런 시스템은 주로 API로 묶인 MSA에 많이 사용합니다.

사용자는 사이트가 하나같이 보이지만 실제로는 물리적으로 트래픽을 분산시킵니다.
이때 API는 사용자를 식별하기 위해 요청 Header 에 jwt token을 보내줍니다.


JWT 토큰의 구조

Header

typ: 토큰의 타입을 지정합니다. 바로 JWT 이죠.
alg: 해싱 알고리즘을 지정합니다. 해싱 알고리즘으로는 보통 HMAC SHA256 혹은 RSA 가 사용되며, 이 알고리즘은, 토큰을 검증 할 때 사용되는 signature 부분에서 사용됩니다.

정보(payload)

token에 담을 정보

{
“iss”: “velopert.com”,
“exp”: “1485270000000”,
https://velopert.com/jwt_claims/is_admin”: true,
“userId”: “11028373727102”,
“username”: “velopert”
}

서명

JSON Web Token 의 마지막 부분은 바로 서명(signature) 입니다. 이 서명은 헤더의 인코딩값과, 정보의 인코딩값을 합친후 주어진 비밀키로 해쉬를 하여 생성합니다


구현


API 프로젝트를 생성합니다.

귀찮으니 기본 와꾸는 만들자고요 ^^
그 다음 nuget package

Microsoft.AspNetCore.Authentication.JwtBearer

를 설치합니다.

그리고 program.cs 에서

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
});

서비스를 추가합니다.
그리고 jwt 기본 옵션 설정값을 appsettings.json을 저장합니다.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "JwtSettings": {
    "Issuer": "https://dotnetdeve.com",
    "Audience": "https://dotnetdeve.com/bluebird",
    "Key": "dotnetdevforeverdotnetdevforeverdotnetdevforeverdotnetdevforeverdotnetdevforever"
  }
}

추가로 아까 등록한 jwt 인증에

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x=> {
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = config["JwtSettings:Issuer"],
        ValidAudience = config["JwtSettings:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["JwtSettings:Key"]!)),
        ValidateIssuer = true,
        ValidateAudience= true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey =true

    };
});

Token 에 대한 인증 옵션을 삽입합니다.
ValidIssuer,ValidAudience,IssuerSigningKey 는 이값이 여기서 사용되는 값이라는 의미이고 ValidateIssuer,ValidateAudience,ValidateLifetime,ValidateIssuerSigningKey
필수 여부입니다.


여담으로 이 아저씨 말 진짜 빠르네요 10분짜리 강의인데 분량은 한 30분 되는것 같네요


그리고 builder.Services.AddAuthentication(); 인증서비스를 추가합니다.

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
미들웨어도 추가합니다. (순서 중요!)

그리고 controller 에 권한 속성을 추가합니다.

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase

이번에는 token 을 생성하는 conroller 를 추가하겠습니다.

[HttpGet]
    [Route("Token")]
    public IActionResult GenerateToken()
    {
        var tokenhandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(TokenSecret);
        var timespan = TimeSpan.FromHours(8);
        var claims = new List<Claim>
        {
            new (JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),
            new (JwtRegisteredClaimNames.Sub,"dotnetdev"),
            new (JwtRegisteredClaimNames.Email,"dot@korea.com"),
            new ("Userid","blueBird"),
        };

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Expires = DateTime.UtcNow.Add(timespan),
            Issuer = "https://dotnetdeve.com",
            Audience = "https://dotnetdeve.com/bluebird",
            SigningCredentials =new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
        };

        var token = tokenhandler.CreateToken(tokenDescriptor);
        var jwt = tokenhandler.WriteToken(token);
        return Ok(jwt);
    }

원래 이렇게 간단하게 하면 안되지만 구현에만 초점을 맞춰서 심플하게 했습니다.

swagger로 확인해보니 잘나오는군요 나온 토큰을 https://jwt.ms/
여기에서 확인해봅니다.

이제 이 token 이 이 API 에서도 사용이 가능한지 사용해보겠습니다.

Insomnia(https://insomnia.rest/) 로 확인해봅니다.


header 에 Authorization를 추가하고 Bearer [token] 포함합니다.

역시 잘나오네요


정책 설정

roll 명을 저장할 IdentityData.cs 생성한후

public class IdentityData
{
    public const string AdminUserClaimName = "admin";
    public const string AdminUserPolicyName = "Admin";
}

Program.cs에서 정책을 등록합니다.

builder.Services.AddAuthorization(option =>
{
    option.AddPolicy(IdentityData.AdminUserClaimName, p => p.RequireClaim(IdentityData.AdminUserClaimName,"true"));
});

token 에 IdentityData.AdminUserClaimName 이 claim value가 true 여야 통과한다는 의미입니다.

이제 이 정책을 쓰는 API를 만들어보겠습니다.

   [HttpGet("Test1")]
        [Authorize(Policy =IdentityData.AdminUserPolicyName)]
        public string GetAutor()
        {
            return "안녕";
        }

[Authorize(Policy =IdentityData.AdminUserPolicyName)]

이 Claim이 포함되야 통과 된다 의미입니다.
그리고 token 생성에서

 var claims = new List<Claim>
       {
           new (JwtRegisteredClaimNames.Jti,Guid.NewGuid().ToString()),
           new (JwtRegisteredClaimNames.Sub,"dotnetdev"),
           new (JwtRegisteredClaimNames.Email,"dot@korea.com"),
           new ("Userid","blueBird"),
           new ("admin","true"),
       };

이렇게 new (“admin”,“true”) claim을 추가합니다.

이렇게 잘 작동되는 모습을 볼수 있습니다.

Git Source : GitHub - BlueBirdJun/JwtAuth_Tutorial

이렇게 API 의 인증 권한을 강좌를 최대한 Simple 하게 작성해보았습니다.
그동안 정리가 안되서 힘들었는데 저도 참 뜻깊은 시간이었습니다.
앞으로는 nick 아저씨 강좌를 위주로 볼생각입니다.

10개의 좋아요

업무를 하실 예정이라고 하셔서 조금 보태자면 보안적인 측면에서 8시간의 accessToken은 조금 길다고 볼수 있습니다.
아마 일반적인 토큰 발급 서비스들을 살펴 보시면, 기본적으로 1시간 정도의 만료시간을 갖도록 설계 하는데요.

accessToken 발급시 refreshToken을 함께 발급하고, refreshToken은 accessToken에 비해 상대적으로 긴 시간을 갖고 보통의 경우라면 유저별로 관리되며, 브라우저 스토리지에 저장하여 api 호출시 토큰 만료에 따른 예외 발생(403) 에 대한 대응으로 refreshToken으로 accessToken을 재발급하여 보안은 지키면서 서비스의 불편함을 최소화하는 방향으로 개발하는 예제들을 많이 보실 수 있으실꺼에요.

6개의 좋아요

조언 감사합니다. 혹시 몇가지 질문 드려도 될까요??
예제를 보면 token 을 db 에 많이 저장하는데 이건 관리에 이유일까요?(그렇다해도 딱히 로그
외에는 쓸일이 없어보이네요)실무에서는
db에 저장하는것이 보통일까요??

그리고 저는 예제에서 Microsoft.AspNetCore.Authentication.JwtBearer 로 구현을 했지만
상당수 예제들이 identyserver4 를 쓰다가 유료화로 넘어간것 같은데
굳이 identyserver4 를 안써도 될까요 아니면 있다면
혹시 오픈 소스중에 추천하는 라이브러리같은것이 있을까요?
그리고 이게 가능한지 모르겠지만 Claim은 policy 생성의 경우
애초에 처음부터 code상의 하드 코딩 하는것으로 되있는데
이걸 db에 넣어놓고 런타임에서 변경하는것 불가능하겠죠??

1개의 좋아요

조언 감사합니다. 혹시 몇가지 질문 드려도 될까요??
예제를 보면 token 을 db 에 많이 저장하는데 이건 관리에 이유일까요?
(그렇다해도 딱히 로그외에는 쓸일이 없어보이네요)실무에서는 db에 저장하는것이 보통일까요??

어떤 예제인지는 모르겠습니다만, accessToken을 db에 저장하지는 않았습니다.
유효한 토큰 검사에 빠르게 대응하기 위해 redis를 이용하기는 합니다.
refreshToken은 유저당 한개만 유지하기 위해서 user 테이블에 매핑해서 저장해서 사용하긴 합니다.

그리고 저는 예제에서 Microsoft.AspNetCore.Authentication.JwtBearer 로 구현을 했지만
상당수 예제들이 identyserver4 를 쓰다가 유료화로 넘어간것 같은데
굳이 identyserver4 를 안써도 될까요 아니면 있다면
혹시 오픈 소스중에 추천하는 라이브러리같은것이 있을까요?

써보지는 않았습니다만, OpenIddict가 있습니다. 아래 글을 참조해주세요.
asp.net core - IdentityServer4 vs AspNet.Security.OpenIdConnect.Server vs OpenIddict - Stack Overflow
참고로, 저는 azure-ad에 권한 토큰을 별도 발급하는 방식으로 연동해본 경험이 있습니다.
Protecting an ASP.NET Core Web API using Microsoft identity platform - Code Samples | Microsoft Learn

그리고 이게 가능한지 모르겠지만 Claim은 policy 생성의 경우
애초에 처음부터 code상의 하드 코딩 하는것으로 되있는데
이걸 db에 넣어놓고 런타임에서 변경하는것 불가능하겠죠??

아마도 policy 자체를 코드상에서 정의하지 않도록 하는 것은 불가능할지 모르지만, policy에 연결되는 role들을 json config 파일이나 db에 넣고 불러 오도록 구성하는 것은 AuthorizationPolicyBuilder를 활용해서 얼마든지 구성할 수 있을 것 같습니다.
다만 저는 db까지는 필요하지 않아 json으로 설정하도록 했었습니다.

도움이 되셨길 바래요~

3개의 좋아요

아 감사합니다. 안 그래도 ?OIDC 는 봤는데 Sample 구현을 했는데
뭔가 ?? custom 하려면 힘들어서 그냥 따로 구현 해봤습니다.

?AuthorizationPolicyBuilder 이런 것이 있군요 한번 파보겠습니다 ^^

그리고 관리 측면에서 생각해 봤는데 토큰을 저장한다고 하기보다
발행요청정보, 발행자 IP같은 것 Log로 저장해 두는 편이 나을 것 같네요
임의로 계정이 탈취당해서 key 가 발행 요청을 추적한다거나 할 때 필요할 것 같습니다.

2개의 좋아요