ASP.NET Core Bearer Token 인증 적용 가이드

  • 35 minutes to read

이 문서는 ASP.NET Core Razor Pages 프로젝트에 ASP.NET Core Identity 를 기반으로 Bearer Token 인증을 적용하는 전 과정을 안내합니다.
최종적으로 토큰 기반으로 보호된 API 를 호출하고, Visual Studio 환경에서 .http 파일을 사용하여 발급된 토큰을 단계별로 테스트합니다.

이 가이드는 프로젝트 생성 단계부터 시작하여 API 보호, 토큰 발급, 토큰 만료, Refresh Token을 활용한 토큰 갱신까지 모든 흐름을 실습하며 익힐 수 있도록 구성되어 있습니다.
처음부터 끝까지 따라 하면 토큰 인증 시스템이 완성됩니다.


1단계: 새로운 ASP.NET Core 프로젝트 생성

먼저, ASP.NET Core Razor Pages 프로젝트를 새로 생성합니다.
이 프로젝트는 Razor Pages 기반이며, 인증 방식으로 개별 계정(Individual Accounts) 을 사용합니다.

Visual Studio 를 사용한 프로젝트 생성

  1. Visual Studio 를 실행합니다.
  2. '새 프로젝트 만들기'를 선택합니다.
  3. 템플릿 목록에서 ASP.NET Core 웹 앱 을 선택한 뒤 '다음'을 클릭합니다.
  4. 프로젝트 이름에 Azunt.Web 을 입력하고 저장 위치를 지정한 후 '다음'을 클릭합니다.
  5. .NET 8.0 을 선택합니다. .NET 9.0 이상을 권장합니다.
  6. 인증 옵션에서 개별 계정 을 선택하여 기본 인증 기능이 포함되도록 합니다.
  7. '만들기'를 클릭하여 프로젝트를 생성합니다.

이제 Visual Studio 를 사용하여 기본 인증 기능이 포함된 Razor Pages 프로젝트가 준비되었습니다.
다음 단계로 넘어가 데이터베이스를 준비합니다.

CLI 를 사용한 프로젝트 생성 (보조)

만약 명령줄 도구를 사용하는 경우, 다음 명령어로 동일한 프로젝트를 생성할 수 있습니다.

dotnet new webapp -n Azunt.Web --auth Individual

이 명령어를 사용하면 Visual Studio 없이도 Razor Pages 기반의 인증 포함 프로젝트가 생성됩니다.
이후 단계부터는 Visual Studio 를 기반으로 설명을 진행하지만, CLI 로 생성한 프로젝트도 동일하게 따라 할 수 있습니다.


2단계: 데이터베이스 생성 및 마이그레이션

프로젝트에 포함된 ASP.NET Core Identity 기능이 정상적으로 동작하려면, 데이터베이스가 필요합니다.
이 단계에서는 데이터베이스를 생성하고, 필요한 테이블 및 스키마를 준비합니다. 참고로, 프로젝트를 실행 후 웹페이지의 Register 경로에서 직접 마이그레이션 방식으로 데이터베이스와 테이블들을 생성할 수 있습니다.

Visual Studio 의 패키지 관리자 콘솔 사용

Visual Studio 내 패키지 관리자 콘솔 을 통해 손쉽게 데이터베이스를 생성할 수 있습니다.

  1. 상단 메뉴에서 도구 > NuGet 패키지 관리자 > 패키지 관리자 콘솔 을 엽니다.
  2. 패키지 관리자 콘솔 창에 다음 명령어를 입력하고 실행합니다.
Update-Database

명령어가 성공적으로 실행되면 LocalDB 데이터베이스가 생성되고, 사용자 계정 및 인증 정보 관리를 위한 Identity 테이블들이 자동으로 준비됩니다.
이제 프로젝트에서 실제로 사용자 인증 및 관리가 가능해졌습니다.

CLI 를 사용한 데이터베이스 생성 (보조)

만약 명령줄 도구를 사용하는 경우, 동일한 작업을 다음 명령어로 수행할 수 있습니다.

dotnet ef database update

이 명령어를 실행하면 마찬가지로 데이터베이스가 생성되고, 모든 마이그레이션이 적용됩니다.
CLI 환경에서 개발하거나 자동화된 배포 파이프라인을 구성할 때 유용합니다.


3단계: 기본 인증 기능 정상 동작 확인

생성된 프로젝트가 정상적으로 작동하는지 검증합니다.
이 과정은 Bearer Token 구현 전에 기본 인증 플로우를 확인하는 중요한 단계입니다.

  1. Visual Studio 메뉴에서 디버그 > 디버깅하지 않고 시작 을 클릭합니다.
  2. 웹 브라우저가 열리면 Home 페이지와 Privacy 페이지가 정상적으로 표시되는지 확인합니다.
  3. 'Register' 메뉴를 클릭하여 새 사용자로 회원 가입을 진행합니다.
  4. 회원 가입 후 'Login' 메뉴로 이동하여 방금 생성한 계정으로 로그인합니다.
  5. 로그인 후 'Manage' 메뉴를 클릭하여 사용자 관리 페이지가 정상적으로 표시되는지 확인합니다.

이 과정을 통해 프로젝트의 기본 인증 기능이 문제없이 작동하는 것을 확인할 수 있습니다.
이제 API 보호 기능을 추가해 보겠습니다.


4단계: Minimal API 추가

이제 실제 API 를 추가하고 테스트할 준비를 합니다.
Minimal API 는 간결하고 가볍게 API 를 정의할 수 있는 방식으로, 토큰 인증과 연계하기에 적합합니다.

  1. Program.cs 파일을 열고 다음 코드를 추가합니다.
app.MapGet("api/foo", () =>
{
    return new[]
    {
        new { Id = 1, Name = "Foo" },
        new { Id = 2, Name = "Bar" }
    };
});
  1. 인증 및 라우팅 미들웨어가 활성화되도록 Program.cs 에 다음 코드를 추가합니다.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorPages();

app.MapGet("api/foo", () =>
{
    return new[]
    {
        new { Id = 1, Name = "Foo" },
        new { Id = 2, Name = "Bar" }
    };
});

app.Run();
  1. 프로젝트를 실행하고 브라우저에서 다음 URL 을 호출합니다.
https://localhost:5001/api/foo

다음과 같은 응답을 확인할 수 있습니다.

[
  { "id": 1, "name": "Foo" },
  { "id": 2, "name": "Bar" }
]

API 가 성공적으로 동작하는 것을 확인했습니다.
다음 단계에서는 API 를 자동화된 방식으로 테스트하는 .http 파일을 준비합니다.


5단계: .http 파일로 API 테스트 자동화 준비

개발하면서 매번 브라우저를 열지 않고도 API 를 테스트할 수 있도록 .http 파일을 사용합니다.
Visual Studio 에서 .http 파일은 직접 실행할 수 있으므로 API 테스트가 매우 편리해집니다.

  1. 프로젝트 루트 폴더에 HttpRequests 폴더를 생성합니다.
  2. .http 파일을 생성하고 이름을 api-foo.http 로 지정합니다.
  3. 다음 내용을 파일에 추가합니다.
### Test api/foo without auth
GET https://localhost:5001/api/foo

파일을 실행하면, Visual Studio 에서 직접 요청을 보내고 응답을 받을 수 있습니다.

[
  { "id": 1, "name": "Foo" },
  { "id": 2, "name": "Bar" }
]

이제 .http 파일을 사용하여 개발 중 언제든지 API 테스트를 쉽게 할 수 있습니다.


6단계: API 보호 - RequireAuthorization 적용

API 가 누구에게나 열려 있는 상태입니다.
이제 이 API 에 대해 인증이 필요한 보호를 적용합니다.

RequireAuthorization() 메서드는 해당 엔드포인트에 인증을 요구하도록 지정하는 역할을 합니다.
이 메서드를 사용하면 미인증 사용자의 접근을 차단하고, 반드시 유효한 인증 토큰이 필요하도록 만들 수 있습니다.
즉, 인증이 완료되지 않은 요청은 모두 401 Unauthorized 응답으로 처리됩니다.

  1. Program.cs 파일에서 /api/foo 엔드포인트를 다음과 같이 수정합니다.
app.MapGet("api/foo", () =>
{
    return new[]
    {
        new { Id = 1, Name = "Foo" },
        new { Id = 2, Name = "Bar" }
    };
}).RequireAuthorization();
  1. .http 파일을 다시 실행해 보면 다음과 같은 결과가 나타납니다.
Status: 401 Unauthorized

API 보호가 성공적으로 적용되었습니다.
이제 인증이 없는 요청은 더 이상 API 에 접근할 수 없습니다.
정상적으로 동작하는 인증 토큰이 필요하며, 이후 단계에서 토큰을 발급하고 사용하는 과정을 실습하게 됩니다.


7단계: Bearer Token 인증 서비스 추가

이제 API 가 단순히 "인증 필요" 수준을 넘어서, Bearer Token 방식의 인증을 요구하도록 설정합니다.
이 단계에서는 실제로 토큰을 발급하고 검증하는 시스템을 프로젝트에 구성하는 과정입니다.

ASP.NET Core 는 다양한 인증 방식을 지원하지만, 여기서는 Bearer Token 방식을 사용합니다.
Bearer Token 은 사용자가 로그인할 때 발급받은 토큰을 HTTP 요청의 Authorization 헤더에 포함하여 API 에 접근하는 방식입니다.

인증 및 권한 서비스 구성

  1. Program.cs 파일의 builder.Services 부분에 다음 코드를 추가합니다.
builder.Services.AddAuthentication()
    .AddBearerToken(IdentityConstants.BearerScheme);
  • AddAuthentication() 을 호출하면 인증 서비스가 활성화됩니다.
  • AddBearerToken() 메서드는 Bearer Token 인증 방식을 추가합니다.
  • IdentityConstants.BearerScheme 은 내부적으로 사용할 인증 스킴 이름입니다.
  1. 이어서, 권한 부여(Authorization) 정책을 추가합니다.
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("api", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.AddAuthenticationSchemes(IdentityConstants.BearerScheme);
    });
  • 이 정책의 이름은 "api" 입니다.
  • RequireAuthenticatedUser() 는 사용자가 반드시 인증되어야 한다는 것을 의미합니다.
  • AddAuthenticationSchemes() 를 통해 인증 방식으로 Bearer Token 만 허용하도록 지정합니다.
  • 이로써 API 가 단순히 로그인된 사용자가 아니라, 올바른 토큰을 소지한 사용자만 접근할 수 있도록 설계되었습니다.

정책 적용: API 엔드포인트 보호

이제 /api/foo 엔드포인트에 방금 정의한 정책을 적용합니다.

app.MapGet("api/foo", () =>
{
    return new[]
    {
        new { Id = 1, Name = "Foo" },
        new { Id = 2, Name = "Bar" }
    };
}).RequireAuthorization("api");
  • RequireAuthorization("api") 를 지정함으로써, 이 엔드포인트는 반드시 "api" 정책을 만족해야 접근이 허용됩니다.
  • 즉, 유효한 Bearer Token 을 가진 사용자만 /api/foo 에 접근할 수 있게 됩니다.

요약

이제 API 가 Bearer Token 기반의 인증을 강제하도록 성공적으로 구성되었습니다.
다음 단계에서는 실제로 토큰을 발급하고 사용하는 과정으로 넘어가며, API 호출 시 토큰이 어떻게 사용되는지 확인하게 됩니다.


8단계: Identity API Endpoint 추가

지금까지 API 가 Bearer Token 으로 보호되도록 설정은 마쳤지만, 사용자가 직접 로그인하여 토큰을 발급받을 수 있는 방법은 아직 없습니다.
이제 실제로 사용자 인증 정보를 기반으로 Access Token 과 Refresh Token 을 발급할 수 있도록 Identity API 엔드포인트를 추가하는 단계입니다.

ASP.NET Core Identity 는 기본적으로 Razor Pages UI 기반의 회원가입, 로그인 기능을 제공하지만,
이번 프로젝트에서는 API 형태로 로그인과 토큰 발급이 이루어지도록 설정합니다.

Identity 서비스에 API 엔드포인트 추가

  1. Program.cs 파일의 서비스 구성 부분에 다음 코드를 추가합니다.
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
        options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddApiEndpoints();

각 메서드의 역할을 이해하면 흐름이 더 명확합니다.

  • AddDefaultIdentity<IdentityUser>()
    기본 Identity 기능을 등록합니다. 여기서 IdentityUser 는 사용자 정보를 나타내는 클래스입니다.

  • options.SignIn.RequireConfirmedAccount = true
    이메일 또는 기타 수단을 통해 계정 확인이 완료되어야만 로그인할 수 있도록 요구합니다.
    (테스트 시에는 false 로 설정하면 계정 확인 없이 빠르게 테스트할 수 있습니다.)

  • AddEntityFrameworkStores<ApplicationDbContext>()
    Identity 데이터 저장소로 EF Core 를 사용하도록 지정합니다.
    이미 앞 단계에서 마이그레이션을 통해 데이터베이스가 준비되었으므로 연결됩니다.

  • AddApiEndpoints()
    핵심 포인트입니다! Identity 관련 기능을 API 형태로 사용할 수 있도록 확장합니다.
    이 메서드를 호출해야 /api/identity/login, /api/identity/register 같은 API 엔드포인트가 자동으로 생성됩니다.

Identity API 엔드포인트 그룹으로 매핑

  1. 이제 API 라우팅을 구성하여 Identity API 엔드포인트들을 그룹으로 매핑합니다.
app.MapGroup("api/identity")
   .MapIdentityApi<IdentityUser>();
  • app.MapGroup("api/identity")
    /api/identity 경로를 기본으로 하는 엔드포인트 그룹을 만듭니다.

  • .MapIdentityApi<IdentityUser>()
    앞서 Identity 서비스에 등록한 API 엔드포인트들을 해당 그룹에 매핑합니다.

결과적으로 다음과 같은 API 가 자동으로 생성됩니다.

HTTP Method Endpoint 설명
POST /api/identity/login 사용자 로그인 및 토큰 발급
POST /api/identity/register 사용자 회원가입
POST /api/identity/refresh Refresh Token 으로 Access Token 재발급
POST /api/identity/logout 사용자 로그아웃 및 토큰 무효화

이제 /api/identity/login 엔드포인트를 통해 사용자는 이메일과 비밀번호를 입력하여 Access Token 과 Refresh Token 을 발급받을 수 있습니다.
다음 단계에서는 실제로 .http 파일을 사용하여 로그인 요청을 보내고, 토큰을 발급받는 실습을 진행합니다.


9단계: .http 파일로 로그인 및 토큰 발급 테스트

이제 우리가 설정한 Identity API 가 정상적으로 동작하는지 확인할 차례입니다.
이 단계에서는 .http 파일을 이용하여 실제로 로그인 요청을 보내고, Access Token 과 Refresh Token 을 발급받습니다.
앞서 설정한 Bearer Token 인증이 실제로 작동하는지 확인하는 중요한 과정입니다.

.http 파일 생성

  1. 프로젝트의 HttpRequests 폴더 안에 새로운 .http 파일을 생성합니다.
    이름은 다음과 같이 지정합니다.
api-identity-login.http

.http 파일은 Visual Studio 에서 직접 실행할 수 있는 API 요청 스크립트입니다.
요청과 응답을 즉시 확인할 수 있어 개발 및 테스트에 매우 유용합니다.

로그인 요청 작성

  1. 파일에 다음 내용을 추가합니다.
### Login to get JWT token
POST https://localhost:5001/api/identity/login
Content-Type: application/json

{
  "email": "a@a.com",
  "password": "P455w0rd"
}
  • POST /api/identity/login : 로그인 API 를 호출합니다.
  • Content-Type: application/json : 요청 본문이 JSON 형식임을 나타냅니다.
  • emailpassword 는 앞서 회원 가입한 계정 정보로 입력합니다.
    • 참고: 테스트용으로 a@a.com / P455w0rd 으로 가입했으면 그대로 사용하면 됩니다.

Visual Studio 에서 바로 실행하면 요청이 서버로 전송됩니다.

응답 확인

  1. 실행 후, 다음과 같은 JSON 응답이 반환됩니다.
{
  "tokenType": "Bearer",
  "accessToken": "...",
  "expiresIn": 3600,
  "refreshToken": "..."
}

응답 필드 설명:

  • tokenType : 토큰 유형, "Bearer" 로 명시됩니다.
  • accessToken : API 호출 시 사용할 인증 토큰입니다.
  • expiresIn : Access Token 의 유효 시간 (초 단위) 입니다. 기본적으로 3600초 (1시간) 입니다.
  • refreshToken : Access Token 이 만료되었을 때 새로운 토큰을 발급받기 위한 토큰입니다.

Access Token 과 Refresh Token 이 모두 정상적으로 발급된 것을 확인할 수 있습니다.

IMPORTANT

Access Token 은 다음 단계에서 API 를 호출할 때 사용할 것이므로, 복사해 두세요. Refresh Token 은 Access Token 이 만료되었을 때 사용할 예정입니다. 이후 단계에서 함께 활용하게 됩니다.


10단계: 발급받은 Token 으로 보호된 API 호출

앞 단계에서 Access Token 을 성공적으로 발급받았습니다.
이번 단계에서는 발급받은 Access Token 을 실제 API 요청에 사용하여 토큰 기반 인증이 어떻게 동작하는지 직접 확인합니다.

특히 먼저 토큰 없이 요청해서 실패하는 상황을 실습한 후, 정상 토큰을 사용하여 성공하는 과정을 진행합니다.
이렇게 하면 API 보호가 실제로 어떻게 동작하는지 체감할 수 있습니다.


1. .http 파일 생성

프로젝트의 HttpRequests 폴더에 새로운 .http 파일을 생성합니다.

파일 이름:

api-foo-auth.http

이 파일 안에서 토큰 없이 요청하는 시도와, 토큰을 추가하여 성공하는 시도를 모두 정의합니다.


2. 토큰 없이 API 호출 (실패 시도)

먼저, 인증 토큰 없이 보호된 API 를 호출합니다.

### Test api/foo without Token (Expected to fail)
GET https://localhost:5001/api/foo

Authorization 헤더 없이 요청합니다.
Visual Studio 에서 요청을 실행하면, 다음과 같은 응답이 반환됩니다.

Status: 401 Unauthorized

예상대로 실패했습니다.
API 가 보호되고 있으며, 인증되지 않은 요청은 거부된다는 것을 확인했습니다.


3. 발급받은 Access Token 으로 API 호출 (성공 시도)

이번에는 발급받은 Access Token 을 Authorization 헤더에 포함하여 보호된 API 를 호출합니다.

### Test api/foo with Bearer Token (Expected to succeed)
GET https://localhost:5001/api/foo
Authorization: Bearer {accessToken}

{accessToken} 부분에 앞서 로그인 시 발급받은 Access Token 을 입력합니다.
Visual Studio 에서 요청을 실행하면, 정상적인 응답을 받을 수 있습니다.

[
  { "id": 1, "name": "Foo" },
  { "id": 2, "name": "Bar" }
]

정상적으로 데이터가 반환되었습니다.


11단계: Access Token 만료 시나리오 이해 및 테스트 준비

Access Token 은 보안을 위해 짧은 유효 기간이 설정되어 있습니다.
기본적으로 발급된 Access Token 의 유효 시간은 약 1시간(expiresIn: 3600) 으로, 시간이 지나면 만료됩니다.

하지만 실습을 위해 굳이 1시간을 기다릴 필요는 없습니다.
Access Token 의 유효 시간을 1분(60초) 으로 변경하여 더 빠르게 테스트할 수 있습니다.

Access Token 유효 기간을 1분으로 설정하는 방법

  1. Program.cs 파일을 열고, 서비스 구성 부분에 다음 코드를 추가합니다.
builder.Services.Configure<BearerTokenOptions>(options =>
{
    options.AccessTokenLifetime = TimeSpan.FromMinutes(1);
});
  • AccessTokenLifetime 을 1분으로 지정하면 Access Token 이 발급된 후 60초가 지나면 자동으로 만료됩니다.
  • Refresh Token 은 더 긴 기간을 유지하므로 Access Token 이 만료되었을 때 새 토큰 발급 테스트가 원활하게 진행됩니다.

서버를 재시작하면 이 설정이 적용됩니다.

Access Token 이 만료된 상태에서 보호된 API 에 접근하면 서버는 다음과 같은 응답을 반환합니다.

Status: 401 Unauthorized

서버는 Access Token 의 유효성을 검사하고, 만료된 경우 인증이 필요하다는 응답을 반환합니다.
이렇게 토큰이 만료되면 매번 로그인할 필요 없이 Refresh Token 을 사용하여 새로운 Access Token 을 발급받을 수 있습니다.

이제 Access Token 이 만료된 상황을 준비했으므로, 이후 단계에서 Refresh Token 을 이용하여 토큰을 재발급하는 과정을 실습합니다.


12단계: Access Token 만료된 상태에서 API 요청 테스트

Access Token 의 유효 시간이 만료된 상황을 실습합니다.
앞 단계에서 Access Token 유효 기간을 1분으로 설정했으므로, 발급받은 후 1분이 지난 뒤 보호된 API 에 접근합니다.

파일명: api-foo-auth.http

### Test api/foo with expired Access Token
GET https://localhost:5001/api/foo
Authorization: Bearer {accessToken}

{accessToken} 부분에는 기존에 발급받았던 Access Token 을 사용합니다.

Visual Studio 에서 요청을 실행하면 서버는 Access Token 이 만료되었음을 인식하고 다음과 같은 응답을 반환합니다.

Status: 401 Unauthorized

서버가 Access Token 의 유효 시간을 검사하여 만료된 것으로 판단했음을 확인할 수 있습니다.

이로써 Access Token 이 정상적으로 만료되는 시나리오가 완성되었습니다.

다음 단계에서는 만료된 Access Token 을 새로 발급받기 위해 Refresh Token 을 사용하는 방법을 실습합니다.


13단계: Refresh Token 으로 새로운 Access Token 요청

Access Token 이 만료된 상황에서 재로그인 없이 API 를 계속 사용하려면 Refresh Token 을 사용해야 합니다.
Refresh Token 은 Access Token 보다 더 긴 유효 기간을 가지며, 새로운 Access Token 을 발급받을 수 있는 권한을 제공합니다.

  1. 프로젝트의 HttpRequests 폴더에 새로운 .http 파일을 생성합니다.
  2. 파일 이름은 다음과 같이 지정합니다.
api-identity-refresh.http
  1. 파일에 다음 내용을 추가합니다.
### Refresh Access Token using Refresh Token
POST https://localhost:5001/api/identity/refresh
Content-Type: application/json

{
  "refreshToken": "{refreshToken}"
}

{refreshToken} 부분에는 Access Token 발급 시 함께 받은 Refresh Token 을 입력합니다.

Visual Studio 에서 요청을 실행하면 서버는 Refresh Token 의 유효성을 검사하고, 새로운 Access Token 과 Refresh Token 을 함께 발급합니다.

{
  "tokenType": "Bearer",
  "accessToken": "...",
  "expiresIn": 60,
  "refreshToken": "..."
}

응답 설명:

  • accessToken: 새롭게 발급된 Access Token 입니다. 보호된 API 에 사용할 수 있습니다.
  • refreshToken: 새로운 Refresh Token 입니다. 이후 토큰 만료 시 다시 사용할 수 있습니다.
  • expiresIn: Access Token 의 유효 시간(초) 입니다. 지금은 60초로 설정했습니다.

서버가 정상적으로 토큰을 재발급했음을 확인했습니다.
새 Access Token 을 복사해 다음 단계에서 사용합니다.


14단계: 새 Access Token 으로 API 호출 테스트

새롭게 발급받은 Access Token 을 사용하여 보호된 API 를 다시 호출합니다.
이 과정을 통해 Access Token 재발급 흐름이 정상적으로 동작하는지 검증합니다.

  1. 프로젝트의 HttpRequests 폴더에 새로운 .http 파일을 생성합니다.
  2. 파일 이름은 다음과 같이 지정합니다.
api-foo-auth-refreshed.http
  1. 파일에 다음 내용을 추가합니다.
### Test api/foo with new Access Token
GET https://localhost:5001/api/foo
Authorization: Bearer {newAccessToken}

{newAccessToken} 부분에 방금 발급받은 Access Token 을 입력합니다.

Visual Studio 에서 요청을 실행하면 정상적으로 API 응답을 받을 수 있습니다.

[
  { "id": 1, "name": "Foo" },
  { "id": 2, "name": "Bar" }
]

서버가 새로운 Access Token 을 유효한 것으로 판단하고, 보호된 API 요청을 허용했습니다.

이로써 Access Token 이 만료된 상황에서도 Refresh Token 을 사용하여 토큰을 재발급받고, 다시 API 에 정상적으로 접근할 수 있음을 검증했습니다.


15단계: Refresh Token 재사용 방지

지금까지 우리는 Refresh Token 을 사용하여 만료된 Access Token 을 새로 발급받는 과정을 실습했습니다.
하지만 Refresh Token 은 기본적으로 한 번 사용 후에도 유효합니다.
이 상태로 두면 보안상 취약점이 생길 수 있습니다.

Refresh Token 재사용 방지 기능을 활성화하면, 이미 사용한 Refresh Token 으로는 더 이상 새로운 Access Token 을 발급받을 수 없게 됩니다.
이를 통해 도난당한 Refresh Token 이 재사용되어 인증 우회를 시도하는 것을 차단할 수 있습니다.

Refresh Token 재사용 방지 설정

  1. Program.cs 파일을 열고, Bearer Token 옵션을 구성하는 부분에 아래 설정을 추가합니다.
builder.Services.Configure<BearerTokenOptions>(options =>
{
    options.AccessTokenLifetime = TimeSpan.FromMinutes(1);
    options.RefreshTokenReuseLimit = 0;
});
  • AccessTokenLifetime : 앞에서 설정한 Access Token 유효 시간 (1분).
  • RefreshTokenReuseLimit = 0 : Refresh Token 재사용을 허용하지 않도록 설정합니다.
    • 기본값은 5입니다. 0으로 설정하면 재사용 시 바로 차단됩니다.
    • 1 이상으로 설정하면 해당 횟수만큼 재사용이 허용됩니다.

서버 재시작

설정을 적용한 후 서버를 반드시 재시작합니다.
이제부터 Refresh Token 은 단 1회만 사용 가능한 일회용 토큰이 됩니다.


Refresh Token 재사용 시도 및 테스트

  1. 이미 사용한 Refresh Token 으로 재발급 요청을 보내 테스트합니다.

파일명: api-identity-refresh.http

### Reuse same Refresh Token (Expected to fail)
POST https://localhost:5001/api/identity/refresh
Content-Type: application/json

{
  "refreshToken": "{previouslyUsedRefreshToken}"
}
  • {previouslyUsedRefreshToken} 부분에는 앞 단계에서 사용했던 Refresh Token 을 그대로 입력합니다.
  1. 요청을 실행하면 다음과 같은 실패 응답이 반환됩니다.
Status: 400 Bad Request

응답 본문 예시:

{
  "error": "invalid_grant"
}

서버가 해당 Refresh Token 이 이미 사용된 것으로 판단하고 재발급을 거부했습니다.
재사용 방지 기능이 성공적으로 적용되었음을 확인할 수 있습니다.


새 Refresh Token 사용 시도 (성공 시나리오)

  1. 이전 단계에서 Refresh Token 을 사용하여 새로 발급받은 최신 Refresh Token 으로 요청을 다시 보냅니다.

파일명: api-identity-refresh.http

### Use latest Refresh Token (Expected to succeed)
POST https://localhost:5001/api/identity/refresh
Content-Type: application/json

{
  "refreshToken": "{newValidRefreshToken}"
}
  • {newValidRefreshToken} 부분에는 가장 최근에 발급받은 새 Refresh Token 을 입력합니다.
  1. 요청을 실행하면 새로운 Access Token 과 Refresh Token 이 정상적으로 발급됩니다.
{
  "tokenType": "Bearer",
  "accessToken": "...",
  "expiresIn": 60,
  "refreshToken": "..."
}

정상적인 동작입니다.
Refresh Token 재사용 방지 기능이 동작하면서, 새로 발급된 Refresh Token 만이 유효하게 사용됩니다.


요약

테스트 시나리오 예상 결과 실제 결과
이전에 사용한 Refresh Token 재사용 400 Bad Request 예상대로 실패
새로 발급받은 Refresh Token 사용 200 OK, 토큰 재발급 예상대로 성공

이번 단계에서는 Refresh Token 의 재사용 방지 기능을 적용하고 테스트하여 보안을 강화했습니다.
이 기능을 통해 Refresh Token 탈취와 같은 위협으로부터 시스템을 보호할 수 있습니다.


16단계: 로그아웃 API 및 토큰 무효화 구현

지금까지 Access Token 과 Refresh Token 발급, 만료, 재발급, 재사용 방지까지 적용했습니다.
하지만 사용자가 직접 로그아웃 할 수 있는 기능이 없다면, 토큰은 만료되거나 재사용 방지 로직에 의존할 수밖에 없습니다.

로그아웃 API 를 구현하면, 사용자가 직접 Refresh Token 을 무효화할 수 있습니다.
특히 보안을 강화하거나, 사용자가 직접 세션을 종료하고 싶을 때 반드시 필요한 기능입니다.

로그아웃 API 활성화

사실 이미 우리가 사용하고 있는 Identity API 엔드포인트 안에는 로그아웃 API 가 포함되어 있습니다.
다음 경로를 통해 로그아웃 요청을 보낼 수 있습니다.

POST /api/identity/logout

Refresh Token 과 함께 요청하면 서버가 해당 토큰을 무효화합니다.


.http 파일로 로그아웃 API 테스트

  1. 프로젝트의 HttpRequests 폴더에 새로운 .http 파일을 생성합니다.
  2. 파일 이름을 다음과 같이 지정합니다.
api-identity-logout.http
  1. 파일에 다음 내용을 추가합니다.
### Logout and invalidate Refresh Token
POST https://localhost:5001/api/identity/logout
Content-Type: application/json

{
  "refreshToken": "{validRefreshToken}"
}
  • {validRefreshToken} 부분에는 사용 중인 Refresh Token 을 입력합니다.
  1. 요청을 실행하면 다음과 같은 응답을 받을 수 있습니다.
Status: 200 OK

서버는 전달받은 Refresh Token 을 데이터베이스 또는 내부 저장소에서 삭제하거나 무효화 처리합니다.
이로써 해당 Refresh Token 은 더 이상 사용할 수 없게 됩니다.


로그아웃 이후 Refresh Token 사용 시도 (실패 시나리오)

이제 로그아웃 후, 무효화된 Refresh Token 으로 재발급을 시도합니다.

파일명: api-identity-refresh.http

### Try to refresh token after logout (Expected to fail)
POST https://localhost:5001/api/identity/refresh
Content-Type: application/json

{
  "refreshToken": "{loggedOutRefreshToken}"
}
  • {loggedOutRefreshToken} 부분에는 로그아웃 시 사용한 Refresh Token 을 그대로 사용합니다.

요청을 실행하면 서버는 해당 Refresh Token 이 이미 무효화된 것으로 판단하고 다음과 같은 응답을 반환합니다.

Status: 400 Bad Request

응답 본문 예시:

{
  "error": "invalid_grant"
}

정상적인 보안 동작이 확인되었습니다.
Refresh Token 은 로그아웃 이후 더 이상 사용할 수 없습니다.

VisualAcademy Docs의 모든 콘텐츠, 이미지, 동영상의 저작권은 박용준에게 있습니다. 저작권법에 의해 보호를 받는 저작물이므로 무단 전재와 복제를 금합니다. 사이트의 콘텐츠를 복제하여 블로그, 웹사이트 등에 게시할 수 없습니다. 단, 링크와 SNS 공유, Youtube 동영상 공유는 허용합니다. www.VisualAcademy.com
박용준 강사의 모든 동영상 강의는 데브렉에서 독점으로 제공됩니다. www.devlec.com