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

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

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” 항목을 설정합니다.
마치며
설명은 길었지만, 기본 템플릿 코드에 추가한 것은 딱 세 가지입니다.
- Microsoft.AspNetCore.Authentication.JwtBearer 패키지 설치
- 인증 서비스 등록 (자동으로 인증 미들웨어 추가됨)
- appsettings.json 에 Jwt 핸들러 옵션 나열.
사실, 2번은 그마저도 user-jwts 도구가 한 것을 약간 수정한 것에 지나지 않습니다.
참고로, 이 예제는 인증 미들웨어만 사용하고, 인가 미들웨어를 사용하지는 않았습니다.
이는 모든 엔드 포인트 핸들러는 인가 미들웨어에 의해 보호 받지 않고 있어, 토큰이 요청에 포함되어 있지 않아도, 엔드 포인트 핸들러가 호출됨을 의미합니다.