JWT 토큰 커스텀 처리

Json Web Token의 커스텀 처리를 위해 닷넷은 아래 두 개의 네임스페이스를 제공합니다.

System.IdentityModel.Tokens.Jwt Namespace - Microsoft Authentication Library for .NET | Microsoft Learn
image

Microsoft.IdentityModel.JsonWebTokens Namespace - Microsoft Authentication Library for .NET | Microsoft Learn

image

인터넷이나 ChatGpt 가 보여주는 JWT 예제들은 주로 전자에 기반한 것들이 많을 것입니다. 그러나, 전자는 레거시로 후자를 사용하도록 권고하고 있습니다.

후자가 나중에 나온 것이라 그런지, 뭔가 좀 더 정리된 느낌이고, 속도도 빠르다고 합니다.

후자를 바탕으로 JWT 토큰을 처리하는 방법을 알아 봅니다.
토큰의 처리는 크게 생성(Creation)과 검증(Validation)으로 나뉩니다.

생성

생성은 토큰이 가져야 하는 데이터를 제공해서 토큰 문자열을 생성하는 과정으로, 이 도구를 사용할 때는 아래의 두 단계를 따른다고 보시면 됩니다.

  1. 보안 토큰(SecurityToken)을 기술(Describe)
  2. 기술을 바탕으로 실물 토큰 생성

토큰을 기술하기 위해 SecurityTokenDescriptor 객체를 사용합니다.
이 객체를 실물 토큰 핸들러에게 전달하는 방식으로 실물 토큰을 생성합니다.

이 예제는 JWT 토큰을 다루므로, 핸들러는 JsonWebTokenHandler 입니다.

코드

var userId = // ...
var firstName = //...
var lastName = // ....

// Microsoft.IdentityModel.Tokens
var secret = "123456179-123456789-123456789-12";
var keyCode = Encoding.UTF8.GetBytes(secret); 
var symkey = new SymmetricSecurityKey(keyCode);

var signingCredentials = new SigningCredentials(symkey, SecurityAlgorithms.HmacSha256Signature);

var tokenDescriptor =  new SecurityTokenDescriptor
{
   Issuer = "http://YourCompany.Com",
   Expires = DateTime.UtcNow.AddMinutes(120),
   SigningCredentials = signingCredentials,
   Claims = new Dictionary<string, object>()
   {
      ["sub"] = userId,
      ["jti"] = Guid.NewGuid(),
      ["given_name"] = firstName,
      ["family_name"] = lastName,
   }
};

// Microsoft.IdentityModel.JsonWebTokens
tokenDescriptor.Claims = new Dictionary<string, object>()
{
   [JwtRegisteredClaimNames.Sub] = userId,
   [JwtRegisteredClaimNames.Jti] = Guid.NewGuid(),
   [JwtRegisteredClaimNames.GivenName] = firstName,
   [JwtRegisteredClaimNames.FamilyName] = lastName,
};

var jwtHandler = new JsonWebTokenHandler{
   SetDefaultTimesOnTokenCreation = false
};

var tokenString = jwtHandler.CreateToken(tokenDescriptor);

코드에 나타난 주석의 의미는 아래와 같습니다.

Microsoft.IdentityModel.Tokens

보안 토큰과 관련한 도구들을 제공합니다.
여기에는 암호화와 관련한 객체들과, 보안 토큰을 기술하는 SecurityTokenDescriptor 객체가 포함되어 있습니다.

Microsoft.IdentityModel.JsonWebTokens

Json Web Token 과 관련된 도구들을 제공하는데, 대표적인 것이JsonWebTokenHandler 입니다.

이 객체의 CreateToken 메서드에 앞서 설정한 SecurityTokenDescriptor 객체를 제공하면 토큰 문자열을 얻을 수 있습니다.

주의

var secret = "123456179-123456789-123456789-12";
var keyCode = Encoding.UTF8.GetBytes(secret); 
var symkey = new SymmetricSecurityKey(keyCode);

시크릿 값을 ASCII 코드 범위의 문자 32자로 이뤄진 문자열을 설정했는데, 이 문자열을 바탕으로 생성된 대칭키(symKey)는 문자열의 전체 바이트의 합산과 같아, 256 비트가 됩니다.

256 비트 길이의 값을 사용하는 이유는 아래의 코드 때문입니다.

var signingCredentials = new SigningCredentials(
   symkey, 
   SecurityAlgorithms.HmacSha256Signature);

암호화 알고리즘 Sha256을 설정하기 위해 SecurityAlgorithms.HmacSha256Signature 상수를 사용했는데, 이와 유사한 값으로 SecurityAlgorithms.HmacSha256 도 있습니다.

전자는 서명을 위한 Sha256 해시 알고리즘을 의미한다는 차이점이 있습니다.
이 의미에 따라 달라지는 부분은 암호화를 위한 키의 길이(Key size)가 SHA256 의 해시 코드 길이(256)보다 길거나 같아야 함을 강제하는데, 이와 반대로, 키 길이가 256 비트보다 짧으면 에러가 납니다.

Claim, ClaimsIdentity, JsonWebToken

많은 예제들이 토큰을 생성할 때, 이 객체들을 사용하는 것을 봐 왔을 것입니다.
그러나, 이 예제에는 전혀 나타나지 않습니다.

보안 토큰 기술자가 Claims 라는 속성을 가지고 있지만, 이는 사전 객체입니다.

tokenDescriptor.Claims = new Dictionary<string, object>()
// ...

이러한 방식은 코드 입장에서는 매우 효율적입니다.

첫째로, 토큰을 생성하기 위해 아래와 같은 번잡한 생성 과정이 필요 없다는 점이죠.

claim data => Claim => ClaimsIdentity => JsonWebToken => string (JWT)

물론 토큰의 검증에는 위 객체들이 등장합니다.

string (JWT) => JsonWebToken => ClaimsIdentity => Claim

둘째로, 사전의 Value가 object 형식이라, ToString() 메서드를 사용하기 때문에, 값을 코딩하기 위한 번잡함도 줄어 듭니다.

JwtRegisteredClaimNames

Json Web Token 표준에는 몇 가지 예약된 key name 이 있는데, 그 값들을 문자열 상수로 제공합니다.

참고로, IANA 에 등록된 등록된 클래임들은 아래의 사이트에서 확인 가능합니다.
JSON Web Token (JWT) (iana.org)

마지막으로, 예제 코드는 예약된 key name 을 문자열로 한번, 상수 값으로 또 한번 설정하고 있는데, 이는 상수에 매칭되는 문자열 값을 보여주기 위한 것이지, 예제처럼 중복적으로 설정할 필요는 없습니다.

[토큰의 검증으로 이어집니다]

20 Likes

@BigSquare 좋은 설명 고맙습니다, 잘 읽어보겠습니다.

3 Likes

발급된 토큰의 사용

JWT 토큰은 토큰 발행 대상자(Audience)와 토큰 보유자(Bearer)에 따라 의미가 달라집니다.
토큰 보유자의 다른 말은 토큰 제출자이고, 토큰 발행 대상자의 다른 말은 토큰 검증자입니다.

보통, 토큰 제출자는 API 서버로부터 정보를 제공 받는 클라이언트 웹 프론트엔드 앱, 데스크탑 앱, 모바일 앱, 또는 다른 서버 앱이고, 토큰의 발행 대상자는 제출자에게 정보를 제공하는 API 서버입니다.

토큰 보유자(Bearer)의 사용

Json Web Token 은 자체적으로 페이로드검증 방식을 포함하고 있기 때문에 JWT의 보유자는 제출 전에 페이로드 사용과 토큰의 검증이 가능은 합니다.

JWT 와 달리, 토큰이 비 표준 형식이라면, 보유자가 토큰에 대해 할 수 있는 일은 제출 밖에 없습니다.
Asp.Net Core Identity가 발급한 Cookie 값이 비표준 형식의 토큰에 해당됩니다.

개인적으로는 비표준 토큰이 더 높은 보안성을 제공하기에, 토큰의 사용 목적에 더 부합한다고 생각합니다.

페이로드의 사용

JWT 의 페이로드에서 원하는 값을 추출하는 것입니다.

var tokenFromResponse = //...;

var jwt = new JsonWebToken(tokenFromReponse);
        
var userIdString = jwt.Subject;
var validity = jwt.ValidTo;
var isExpired = DateTime.UtcNow > validity; 
        
var firstName = jwt.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.GivenName)?.Value;
var lastName = jwt.Claims.FirstOrDefault(c => c.Type == JwtRegisteredClaimNames.FamilyName)?.Value;

tokenFromResponse는 토큰 발행자의 응답(Http Response) 바디에 포함된 JWT 토큰을 나타냅니다.

토큰 보유자는 토큰에 포함된 다양한 정보를 추출하고 있는데, 이 것이 가능한 이유는 JWT 토큰은 페이로드가 공개된 표준 형식이기 때문입니다.

그러나, 토큰이 표준 형식을 지키지 않거나 오염된 경우(Base64 디코딩 실패 등), JsonWebToken 생성자에서 예외가 발생하기 때문에 try-catch로 처리하는 편이 좋습니다.

토큰의 검증

토큰의 검증 - 다른 말라고, 위변조 여부를 확인하려면 토큰의 서명키가 필요합니다.

그러나, 토큰 보유자는 서명키를 알고 있을 확률보다는 모를 확률이 매우 높습니다.
이는 우리가 지폐를 쓰지만, 지폐의 위변조 방지 수단을 모르는 것과 같습니다.

따라서, 보유자가 토큰의 위변조를 검증하는 경우는 거의 없다고 할 수 있습니다.

토큰 보유자가 시스템의 서브 서버인 경우와 토큰의 서명 알고리즘이 공개키 암호화 방식인 경우에는 토큰을 검증할 수도 있습니다.
그러나, 이 경우에도, 페이로드가 오픈된 JWT로 데이터를 주고 받기 보다는, AES(대칭 키, 속도 빠름) 혹은 HMAC 으로 둘 사이에 암호화된 메시지를 주고 받는 것이 더 나은 방법입니다.

제출 (Negotiation)

토큰 보유자가 토큰을 사용하는 주요 방식은 발급 대상자에게 제출하고, 원하는 정보를 얻는 것(Negotiation)입니다.

토큰 제출은 보통 Http 요청을 통해 이뤄지는데, 제출 목적이 인증을 통과하기 위한 것이라면, Authorization 해더에 포함시키고, 다른 목적이라면, 요청 바디나, 커스텀 해더에 포함시킵니다.

토큰 발행 대상자(Audience)의 사용

토큰의 검증

JWT는 발행된 이후 인터넷을 돌고 돌다가 어느 날 문득, 토큰의 발행 대상자에게 제출됩니다.

토큰 발행 대상자 입장에서는 제출된 토큰이 유효한 토큰인지 검증(Verification)하는 것이 매우 중요합니다.

검증은 JWT 토큰에 표시된 서명 값(1)과 토큰을 서명할 때 사용했던, 서명 알고리즘과 서명키(Signing Key)로 JWT의 해더와 페이로드를 서명한 값(2)을 비교하는 행위(1 == 2)를 가리킵니다.

검증에 필요한 서명 알고리즘은 JWT 토큰 해더에 표함되어 있지만, 서명 키는 토큰 발행자로부터 미리 획득해야 합니다.

서명 키는 안전한 루트를 통해 발행자(Isser)로부터 발행 대상자(Audience)에게 전달되어야 합니다. 다만, 발행자와 발행 대상자가 같다면, 발행 대상자는 이미 서명 키를 알고 있습니다.
발행자와 발행 대상자가 같은 대표적인 경우가, 인증을 위해 JWT를 자체적으로 발행한 경우입니다.

검증을 수행할 때도 JsonWebTokenHandler 를 사용하면 편리합니다.

// secret = "123456179-123456789-123456789-12";
ClaimsIdentity ValidateToken(string token, string secret)
{   
    var symkey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
    
    var validationParameters = new TokenValidationParameters{
        // ValidateIssuerSigningKey = true,
        IssuerSigningKey = symkey,
        ValidateAudience = false,
        ValidIssuer = "http://YourCompany.Com",
        
        // resets clock sckew from 5 minutes(default) to one.
        ClockSkew = TimeSpan.FromMinutes(1),
    };

    var handler = new JsonWebTokenHandler();

    var validationResult = handler
      .ValidateTokenAsync(token, validationParameters)
      .Result;

    return validationResult.ClaimsIdentity;
}
JsonWebTokenHandler.ValidateTokenAsync

ValidateToken 메서드 내부에서 JsonWebTokenHandler.ValidateTokenAsync를 사용하고 있는데, 이 메서드는 TokenValidationResult 객체를 반환합니다.

이 결과 객체는 ClaimsIdentity 속성을 보유하는데, 검증 결과에 따라 ClaimsIdentity.IsAuthenticated가 true 또는 false 로 설정됩니다.

위 코드에서 주목할 점은, 토큰을 발행할 때 보였던, 서명 알고리즘을 설정하는 코드가 없다는 점입니다.

var signingCredentials = 
   new SigningCredentials(symkey, SecurityAlgorithms.HmacSha256Signature);

이는 JsonWebTokenHandler.ValidateTokenAsync 메서드는 파라미터로 제공된 JWT 토큰의 해더에 포함된 서명 알고리즘을 추출하기 때문입니다.

    var validationResult = handler
      .ValidateTokenAsync(token, validationParameters)
      .Result;

커스텀 JWT 인증 미들웨어

ValidateToken 메서드의 로직으로, 커스텀 JWT 인증 미들웨어를 정의할 수 있습니다.

public class JwtAuthMiddleware 
{
    private readonly RequestDelegate _next;
    private readonly string _authSecret;

    public JwtAuthMiddleware(RequestDelegate next, IConfiguration config)
    {
       _next = next;,
       _authSecret = config["AuthSecret"] ?? "";
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var token = context.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last();
        if (token is not null)
        {
            var claimsIdentity = ValidateToken(token, _authSecret);

            var sub = claimsIdentity.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
            
            if (Guid.TryParse(sub, out var userId))
            {
                // if (_userService.GetById(userId) is not null) 
                context.User = new ClaimsPrincipal(claimsIdentity);
            }  
        }
        await _next(context);
    }

   private ClaimsIdentity ValidateToken(string token, string secret )  {  // ... }
program.cs
// ...
app.UseMiddleware<JwtAuthMiddleware>();
// ...

이 미들웨어는 토큰의 위변조 여부를 검증한 후에, 토큰에 담긴 정보를 바탕으로 ClaimsPrincipal 객체를 생성하고, 이를 HttpContext.User 속성에 할당하는데, 이는 아래에 설명할 프레임워크 제공 인증 미들웨어가 하는 역할과 완전히 동일합니다.

이 미들웨어가 설정한 HttpContext.User 는 후속 미들웨어(주로 인가 미들웨어)나 요청 핸들러가 사용하게 됩니다.

프레임워크 인증 미들웨어

프레임워크가 제공하는 인증 미들웨어를 ValidateToken 메서드의 로직을 구현하도록 설정하는 방법은 아래와 같습니다.

program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer(options => 
   {
      var secret = builder.Configuration["AuthSecret"] ?? ""; 
      // secret = "123456179-123456789-123456789-12";

      var symkey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
    
      options.TokenValidationParameters = new TokenValidationParameters
      {
         // ...
      };
   });
// ...
// app.UseMiddleware<JwtAuthMiddleware>();
app.UserAuthentication();
// ...
4 Likes

WebApplication 에서 JWT 인증

Host 를 파생하는 WebApplication 은 최신 Asp Net Core 의 웹 호스트 객체로, Web Api, 미니멀 Api, 블레이저 웹 앱의 근간 객체입니다.

이 객체는 인증과 관련한 제반 환경이 매우 잘 구비돼 있어서, 이전 글처럼 번잡한 코드가 없이 간편하게 인증을 설정할 수 있습니다.

미니멀 API 프로젝트 템플릿을 통해 JWT 인증을 설정하는 방법에 대해 알아 봅니다.

가정

API는 제 3 자(토큰 발행자)가 발행한 JWT 토큰을 기준으로 인증을 합니다.
이는 토큰 발행자로부터 토큰의 서명 키를 사전에 제공 받았다는 가정이 전제됩니다.

즉, 토큰을 서명한 서명키가 없으면, 아래의 내용은 아무런 쓸모가 없다는 점 기억해야 합니다.

예제 프로젝트 생성

커맨드 라인 환경에서 웹 API 프로젝트 템플릿을 통해 프로젝트를 생성합니다.
(닷넷 cli 도구가 설치되어 있어야 합니다.)

dotnet new webapi -n TestApi

위와 같이 호출하면 컨트롤러 베이스 API가 아닌, 미니멀 API 가 생성됩니다.

Program.cs 의 파일을 아래와 같이 수정합니다.

using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/hello", (ClaimsPrincipal user)=>
{
    // user.Identity 은 언제나 not null
    if (!user.Identity!.IsAuthenticated) 
        return Results.Text("토큰이 유효하지 않거나, 검증에 실패했습니다.");

    var userName = user.Identity.Name ?? "임시 방문자";
    var hello = $"안녕하세요, {userName}님. {user.Identity.AuthenticationType} 토큰이 확인됐습니다.";

    return Results.Text(hello);
});

app.Run();

Get “/hello” 엔드 포인트 헨들러는 ClaimsPrincipal 를 주입받는데, 아래처럼 할 수도 있습니다.

app.MapGet("/hello", (HttpContext context)=>
{
   ClaimsPrincipal user = context.User;
    // user.Identity 은 언제나 not null
   // ...

Webapplication 은 Http 요청으로부터, HttpContext 객체를 생성할 때, 인증 여부과 상관없이 언제나 (ClaimsPrincipal) User 속성을 할당합니다.
또한, (ClaimsIdentity?) User.Identity 속성도 언제나 not null 입니다.
(bool) Identity.IsAuthenticated 속성은 Identity.AuthenticationType != null 입니다.

이제 앱을 실행한 후, 아래의 요청을 보내면,

GET http://localhost:5285/hello HTTP/1.1

아래의 응답을 받습니다.

HTTP/1.1 200 OK
Content-Length: 26
Connection: close
Content-Type: text/plain; charset=utf-8
Date: Sun, 01 Sep 2024 08:00:48 GMT
Server: Kestrel

토큰이 유효하지 않거나, 검증에 실패했습니다.

테스트 용 토큰과 서명키

우리는 제 3자 발행 토큰으로 인증하는 경우를 가정하기에, 누군가 발행한 jwt 토큰과 그 토큰을 검증할 서명키가 있어야 합니다.

닷넷은 이러한 경우에 사용할 수 있도록, user-jwts 라는 도구를 제공합니다.

Generate tokens with dotnet user-jwts | Microsoft Learn

이 도구는 기본 JWT 토큰을 생성하는데, 토큰 생성 시 사용한 토큰 서명키를 제공합니다. 뿐만 아니라, 필요한 설정 코드도 함께 생성합니다.

위 링크의 설명을 따라서, 기본 토큰을 하나 발행해 봅니다.

cd TestApi
dotnet user-jwts create

위 명령어를 실행하면, 생성된 jwt 토큰이 화면에 출력됩니다.

이 값을 복사해서 따로 저장해 놔도 되고, 아래에 설명할 다른 파일에 들어 가 있으니 그것을 사용해도 됩니다.(파일을 찾는 법과 사용 법은 아래 다시 설명)

user-jwts 는 닷넷의 다른 개발 도구인 시크릿 매니저(user-secrets) 에 기반합니다.
이는 user-jwts 도구가 생성한 결과물은 user-secrets 가 저장하는 위치와 같다는 의미입니다.

참고 : 윈도우에 MS 계정을 연결해 놓았다면, 시크릿의 저장 위치는 아래와 같습니다.
C:\Users\{계정이름}\AppData\Roaming\Microsoft\UserSecrets

시크릿 저장 위치에 찾아 가면, 몇 개의 폴더가 보이는데, 그 중에서 하나를 찾아야 합니다. (저는 폴저의 생성 날짜를 기준으로 찾았습니다)

image

적절한 폴더를 찾으면, 아래와 같이 두 개의 파일이 생성되어 있을 것입니다.

image

secrets.json 에는 jwt 를 검증할 수 있는 서명키(Signing Key)가 들어 가 있습니다.

{
    "Authentication:Schemes:Bearer:SigningKeys": [
        {
            "Id": "48f9b8f0",
            "Issuer": "dotnet-user-jwts",
            "Value": "6v3dko2cGWi8h38DbxHQwvypZwdb6BhdX\u002B3bkQ/TRIQ=",
            "Length": 32
        }
    ]
}

user-jwts.json 에는 관련 인증 서비스가 사용하는 인증 핸들러의 옵션 값들과, 토큰이 들어가 있습니다.

이 값들의 사용 방법은 아래에서 다시 설명합니다.

이제 테스트를 위한 토큰과 서명키가 마련됐습니다.

인증과 관련한 객체들

이제 Asp Net core의 인증 관련 객체들에 대해 이해하는 시간을 잠시 가져 보겠습니다.

인증 서비스, 인증 핸들러

인증 서비스(IAuthenticationService)는 인증 미들웨어가 인증을 수행할 때 사용하는 서비스입니다.

Asp Net Core 에서는 아래와 같이 정의해 놨습니다.

IAuthenticationService Interface (Microsoft.AspNetCore.Authentication) | Microsoft Learn

Asp Net Core 는 인증 서비스 뿐만 아니라, 인증 핸들러(IAuthenicationHandler)도 정의해 놓았습니다.

IAuthenticationHandler Interface (Microsoft.AspNetCore.Authentication) | Microsoft Learn

이 둘은 아래와 같은 의존 관계가 있습니다.

IAuthenicationService → IAuthenticationHandler

인증과 관련해서 프레임워크가 제공하는 객체들의 의존 관계를 좀 더 확장해 보면,

인증 미들웨어 → IAuthenicationService → IAuthenticationHandler → AuthenticationSchemeOptions

인증 스킴

인증 스킴은 인증 핸들러와 그 핸들러를 위한 설정의 조합을 가리킵니다.

IAuthenticationHandler → AuthenticationSchemeOptions

JWT 인증에 있어서 인증 핸들러는 당연히 JWT 토큰을 처리하는데, 이 핸들러는 JWT의 항목 - 발행자, 수신자, 소유자, 유효 기간 등등 - 중 어떤 것을 검증할 것인지에 관한 설정(옵션)이 필요합니다.

이 핸들러와 핸들링 옵션을 함께 묶어 인증 스킴이라고 부릅니다.

  • 인증 스킴 1
    이름 : “Bearer”
    인증 핸들러 : ~~~
    검증 설정 : 발행자

  • 인증 스킴 2
    이름 : “MyApp”
    인증 핸들러 : ~~~
    검증 설정 : 발행자, 수신자, 유효기간

프레임워크 제공 스킴

Microsoft.AspNetCore.Authentication.JwtBearer 패키지는 사전에 정의된 인증 스킴을 포함하고 있습니다.

이 스킴을 사용하는 방법은 이 패키지가 제공하는 확장 메서드 중 하나를 호출하면 됩니다.

JwtBearerExtensions.AddJwtBearer Method (Microsoft.Extensions.DependencyInjection) | Microsoft Learn

WebApplication

마지막으로, WebApplication의 특징에 관해 알아 둬야 할 점이 있는데, 이 호스트는 :

인증(Authentication) 서비스를 등록하면, 자동으로 인증 미들웨어를 등록
인가(Authorization) 서비스를 등록하면, 자동으로 인가 미들웨어를 등록

합니다.

예전에 사용하던, WebHost 는 서비스와 미들웨어를 명시적으로 등록해야만 했던 것에서 (편리한 방향으로) 달라진 부분입니다.

사정 등록된 스킴을 사용한 Jwt 인증 설정.

이제 Jwt 인증을 설정해 봅니다.

Microsoft.AspNetCore.Authentication.JwtBearer 패키지를 프로젝트에 추가하고, Program.cs 에 아래와 같이 코드를 추가합니다.

var builder = WebApplication.CreateBuilder(args);

// 추가
builder.Services.AddAuthentication()
    .AddJwtBearer();

var app = builder.Build();

앞서 길게 설명한 것들이 허무할 정도로 실제 코드는 간단합니다.

참고로, WebApplication은 위 코드만으로도, 미들웨어를 등록하기에 명시적으로 다시 등록할 필요는 없습니다.

// app.UseAuthentication();

인증 서비스를 등록할 때, AddJwtBearer() 확장 메서드를 호출했는데. 이는 아래와 같이 "Bearer"라는 사전 정의 인증 스킴(Authentication scheme)을 설정하는 것과 같습니다.

// 추가
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer");

즉, 기본 스킴의 이름을 지정하지 않으면, 기본 값인 "Bearer"로 설정되는 것이죠.

appsettings.json 을 통한 설정.

이전 글에서는 인증 서비스 등록할 때, 아래와 같이 설정 옵션을 코드로 지정했었습니다.

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
   .AddJwtBearer(options => 
   {
      var secret = builder.Configuration["AuthSecret"] ?? ""; 
      // secret = "123456179-123456789-123456789-12";

      var symkey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
    
      options.TokenValidationParameters = new TokenValidationParameters
      {
         // ...
      };
   });

“Authentication:Schemes”

WebApplication 은 Configuration 항목에 키를 예약하여, 위와 같은 부산한 코드를 적지 않아도 됩니다.

이는 “ConntectionStrings” 키를 데이터 베이스 연결 문자열을 위한 키로 예약한 것과 같은 방식입니다.

인증 핸들러 설정을 위해 예약된 키는 “Authentication:Schemes” 입니다.

이 키를 사용하여 설정 값을 환경 변수에 넣든, 사용자 시크릿에 넣든, json 파일에 넣든, 인증 핸들러는 Configuration 을 통해 값을 제공 받습니다.

이를 우리가 직접 적을 수도 있지만, 앞서 실행한 user-jwts 도구가, 이미 그 형식에 맞는 값을 appsettings.development.json 파일에 설정해 놓았습니다.

appsettings.development.json 의 마지막 부분.

  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "http://localhost:16457",
          "https://localhost:44386",
          "http://localhost:5285",
          "https://localhost:7278"
        ],
        "ValidIssuer": "dotnet-user-jwts"
...

“Bearer” 라는 이름의 인증 스킴의 인증 옵션이 나열되어 있는데, 수취자(ValidAudiences) 와 발행자(ValidIssuer)를 검증하도록 설정하고 있습니다.

이중 ValidIssuer 의 값으로 지정된 "dotnet-user-jwts"는 앞서 user-jwts 를 통해 키를 생성할 때, 지정된 것입니다.

그런데, 이 파일에는 가장 중요한 항목인 서명 키(SigningKey) 값이 없습니다.

앞서 user-jwts 도구는 user-secrets 도구에 기반한다는 점을 언급했습니다.
이는 서명키 값은 user-secrets 도구에 의해 Configuration 항목으로 제공됨을 의미합니다.

다시 말하면, 사전 등록된 JWT 핸들러는 서명키 값은 secrets.json에서, 검증 옵션은 appsettings.json 에서 공급 받는 것입니다.

설정을 직관적으로 만들기 위해, 아래 secrets.json 파일에 있는 서명 키 값만 appsettings.development.json 으로 옮깁니다.

  "Authentication": {
    ...
        "ValidIssuer": "dotnet-user-jwts",
        "SigningKeys": [
          {
             "Issuer": "dotnet-user-jwts",
             "Value": "6v3dko2cGWi8h38DbxHQwvypZwdb6BhdX\u002B3bkQ/TRIQ=",
             "Length": 32
          }
        ],

   ...

토큰 보내기

이제, 앞서 복사해두었거나, user-jwts.json 에 있는 토큰 값을 요청의 해더에 포함시켜 테스트를 해봅니다.

GET http://localhost:5285/hello HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ(생략)

HTTP/1.1 200 OK
Content-Length: 87
Connection: close
Content-Type: text/plain; charset=utf-8
Date: Sun, 01 Sep 2024 09:21:06 GMT
Server: Kestrel

안녕하세요, admin님. AuthenticationTypes.Federation 토큰이 확인됐습니다.

참고로, admin 은 제 컴퓨터의 사용자 계정 이름인데, user-jwts 은 실행시킨 로컬 계정 이름으로 JWT의 “sub” 항목을 설정합니다.

마치며

설명은 길었지만, 기본 템플릿 코드에 추가한 것은 딱 세 가지입니다.

  1. Microsoft.AspNetCore.Authentication.JwtBearer 패키지 설치
  2. 인증 서비스 등록 (자동으로 인증 미들웨어 추가됨)
  3. appsettings.json 에 Jwt 핸들러 옵션 나열.

사실, 2번은 그마저도 user-jwts 도구가 한 것을 약간 수정한 것에 지나지 않습니다.

참고로, 이 예제는 인증 미들웨어만 사용하고, 인가 미들웨어를 사용하지는 않았습니다.
이는 모든 엔드 포인트 핸들러는 인가 미들웨어에 의해 보호 받지 않고 있어, 토큰이 요청에 포함되어 있지 않아도, 엔드 포인트 핸들러가 호출됨을 의미합니다.

3 Likes

좋은 아티클 감사합니다 @BigSquare 님은 26일날 워크샵.오시나요?

1 Like

@파란매 안타깝게도 다른 일정이 있어, 참석이 어렵습니다.