본문으로 건너뛰기

RBAC 및 JWT 검증으로 ASP.NET Core API 보호하기

이 가이드는 역할 기반 접근 제어 (RBAC)와 Logto에서 발급된 JSON Web Token (JWT)을 사용하여 ASP.NET Core API에 인가 (Authorization)를 구현하고 보안을 강화하는 방법을 안내합니다.

시작하기 전에​

클라이언트 애플리케이션은 Logto에서 액세스 토큰 (Access token)을 받아야 합니다. 아직 클라이언트 통합을 설정하지 않았다면, React, Vue, Angular 또는 기타 클라이언트 프레임워크를 위한 빠른 시작이나 서버 간 접근을 위한 기계 간 (M2M) 가이드를 확인하세요.

이 가이드는 ASP.NET Core 애플리케이션에서 이러한 토큰의 서버 측 검증에 중점을 둡니다.

이 가이드의 중점을 보여주는 그림

학습 내용​

  • JWT 검증: 액세스 토큰 (Access token)을 검증하고 인증 (Authentication) 정보를 추출하는 방법 학습
  • 미들웨어 구현: API 보호를 위한 재사용 가능한 미들웨어 생성
  • 권한 모델: 다양한 인가 (Authorization) 패턴 이해 및 구현:
    • 애플리케이션 전체 엔드포인트를 위한 글로벌 API 리소스
    • 테넌트별 기능 제어를 위한 조직 권한
    • 다중 테넌트 데이터 접근을 위한 조직 수준 API 리소스
  • RBAC 통합: API 엔드포인트에서 역할 기반 권한 (Role) 및 스코프 (Scope) 적용

사전 준비 사항​

  • 최신 안정 버전의 .NET 설치
  • ASP.NET Core 및 웹 API 개발에 대한 기본 이해
  • Logto 애플리케이션 구성 완료 (빠른 시작 참고)

권한 모델 개요​

보호를 구현하기 전에, 애플리케이션 아키텍처에 맞는 권한 모델을 선택하세요. 이는 Logto의 세 가지 주요 인가 (Authorization) 시나리오와 일치합니다:

글로벌 API 리소스 RBAC
  • 사용 사례: 애플리케이션 전체에서 공유되는 API 리소스를 보호 (조직별이 아님)
  • 토큰 유형: 글로벌 대상이 포함된 액세스 토큰 (Access token)
  • 예시: 공개 API, 핵심 제품 서비스, 관리자 엔드포인트
  • 적합 대상: 모든 고객이 사용하는 API가 있는 SaaS 제품, 테넌트 분리가 없는 마이크로서비스
  • 자세히 알아보기: 글로벌 API 리소스 보호하기

💡 진행하기 전에 모델을 선택하세요 - 이 가이드 전반에서 선택한 접근 방식을 참고하게 됩니다.

빠른 준비 단계​

Logto 리소스 및 권한 구성​

  1. API 리소스 생성: 콘솔 → API 리소스로 이동하여 API를 등록하세요 (예: https://api.yourapp.com)
  2. 권한 정의: read:products, write:orders와 같은 스코프를 추가하세요 – 권한과 함께 API 리소스 정의하기 참고
  3. 글로벌 역할 생성: 콘솔 → 역할로 이동하여 API 권한이 포함된 역할을 생성하세요 – 글로벌 역할 구성 참고
  4. 역할 할당: API 접근이 필요한 사용자 또는 M2M 애플리케이션에 역할을 할당하세요
RBAC가 처음이신가요?:

역할 기반 접근 제어 가이드에서 단계별 설정 방법을 시작하세요.

클라이언트 애플리케이션 업데이트​

클라이언트에서 적절한 스코프를 요청하세요:

이 과정에는 일반적으로 클라이언트 설정을 다음 중 하나 이상을 포함하도록 업데이트하는 것이 포함됩니다:

  • OAuth 플로우의 scope 파라미터
  • API 리소스 접근을 위한 resource 파라미터
  • 조직 컨텍스트를 위한 organization_id
코딩 전에:

테스트 중인 사용자 또는 M2M 앱에 API에 필요한 권한이 포함된 적절한 역할 또는 조직 역할이 할당되어 있는지 확인하세요.

API 프로젝트 초기화하기​

새로운 .NET Web API 프로젝트를 초기화하려면 .NET CLI를 사용할 수 있습니다:

dotnet new webapi -n YourApiName
cd YourApiName

JWT 인증 (Authentication)을 위한 필수 NuGet 패키지를 추가하세요:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

기본 API 컨트롤러를 생성하세요:

Controllers/ApiController.cs
using Microsoft.AspNetCore.Mvc;

namespace YourApiName.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ApiController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new { message = "Hello from .NET API" });
}
}
}

개발 서버를 시작하세요:

dotnet run
노트:

컨트롤러, 미들웨어 및 기타 기능 설정에 대한 자세한 내용은 ASP.NET Core 문서를 참조하세요.

상수 및 유틸리티 초기화하기​

코드에서 필요한 상수와 유틸리티를 정의하여 토큰 추출 및 유효성 검사를 처리하세요. 유효한 요청에는 Authorization 헤더가 반드시 Bearer <액세스 토큰 (Access token)> 형식으로 포함되어야 합니다.

AuthConstants.cs
namespace YourApiNamespace
{
public static class AuthConstants
{
public const string Issuer = "https://your-tenant.logto.app/oidc";
}
}
AuthenticationExceptions.cs
namespace YourApiNamespace.Exceptions
{
public class AuthorizationException : Exception
{
public int StatusCode { get; }

public AuthorizationException(string message, int statusCode = 403) : base(message)
{
StatusCode = statusCode;
}
}
}

Logto 테넌트 정보 가져오기​

Logto에서 발급한 토큰을 검증하려면 다음 값들이 필요합니다:

  • JSON Web Key Set (JWKS) URI: JWT 서명을 검증하는 데 사용되는 Logto의 공개 키 URL입니다.
  • 발급자 (Issuer): 예상되는 발급자 값 (Logto의 OIDC URL).

먼저, Logto 테넌트의 엔드포인트를 찾아야 합니다. 다음 위치에서 확인할 수 있습니다:

  • Logto 콘솔에서 설정 → 도메인에서 확인하세요.
  • Logto에서 구성한 애플리케이션 설정의 설정 → 엔드포인트 & 자격 증명에서 확인하세요.

OpenID Connect 디스커버리 엔드포인트에서 가져오기​

이 값들은 Logto의 OpenID Connect 디스커버리 엔드포인트에서 가져올 수 있습니다:

https://<your-logto-endpoint>/oidc/.well-known/openid-configuration

예시 응답입니다 (다른 필드는 생략):

{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}

Logto는 JWKS URI 또는 발급자 (Issuer)를 커스터마이즈할 수 없으므로, 이 값들을 코드에 하드코딩할 수 있습니다. 하지만, 향후 설정이 변경될 경우 유지보수 부담이 커질 수 있으므로 프로덕션 애플리케이션에서는 권장하지 않습니다.

  • JWKS URI: https://<your-logto-endpoint>/oidc/jwks
  • 발급자 (Issuer): https://<your-logto-endpoint>/oidc

토큰 및 권한 검증하기​

토큰을 추출하고 OIDC 구성을 가져온 후, 다음을 검증하세요:

  • 서명: JWT는 유효해야 하며 Logto(JWKS를 통해)에서 서명되어야 합니다.
  • 발급자 (Issuer): Logto 테넌트의 발급자와 일치해야 합니다.
  • 대상 (Audience): Logto에 등록된 API의 리소스 지표 또는 해당되는 경우 조직 컨텍스트와 일치해야 합니다.
  • 만료: 토큰이 만료되지 않아야 합니다.
  • 권한 (스코프, Permissions): 토큰에는 API/동작에 필요한 스코프가 포함되어야 합니다. 스코프는 scope 클레임에 공백으로 구분된 문자열입니다.
  • 조직 컨텍스트: 조직 수준의 API 리소스를 보호하는 경우, organization_id 클레임을 검증하세요.

JWT 구조와 클레임에 대해 더 알아보려면 JSON Web Token 을 참고하세요.

각 권한 모델별로 확인해야 할 사항​

클레임과 검증 규칙은 권한 모델에 따라 다릅니다:

  • Audience 클레임 (aud): API 리소스 지표
  • Organization 클레임 (organization_id): 없음
  • 확인할 스코프(권한) (scope): API 리소스 권한

비-API 조직 권한의 경우, 조직 컨텍스트는 aud 클레임(예: urn:logto:organization:abc123)으로 표현됩니다. organization_id 클레임은 조직 수준 API 리소스 토큰에만 존재합니다.

팁:

안전한 멀티 테넌트 API를 위해 항상 권한(스코프)과 컨텍스트(대상, 조직)를 모두 검증하세요.

검증 로직 추가하기​

JWT 인증 (Authentication)을 위해 필요한 NuGet 패키지를 추가하세요:

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />

토큰 검증을 처리할 검증 서비스를 생성하세요:

JwtValidationService.cs
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using YourApiNamespace.Exceptions;

namespace YourApiNamespace.Services
{
public interface IJwtValidationService
{
Task ValidateTokenAsync(TokenValidatedContext context);
}

public class JwtValidationService : IJwtValidationService
{
public async Task ValidateTokenAsync(TokenValidatedContext context)
{
var principal = context.Principal!;

try
{
// 권한 모델에 따라 검증 로직을 여기에 추가하세요
ValidatePayload(principal);
}
catch (AuthorizationException)
{
throw; // 인가 (Authorization) 예외는 다시 throw
}
catch (Exception ex)
{
throw new AuthorizationException($"토큰 검증 실패: {ex.Message}", 401);
}
}

private void ValidatePayload(ClaimsPrincipal principal)
{
// 권한 모델에 따라 검증 로직을 여기에 구현하세요
// 아래 권한 모델 섹션에서 예시를 확인할 수 있습니다
}
}
}

Program.cs에서 JWT 인증 (Authentication)을 구성하세요:

Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using YourApiNamespace.Services;
using YourApiNamespace.Exceptions;

var builder = WebApplication.CreateBuilder(args);

// 서비스 등록
builder.Services.AddControllers();
builder.Services.AddScoped<IJwtValidationService, JwtValidationService>();

// JWT 인증 (Authentication) 구성
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = AuthConstants.Issuer;
options.MetadataAddress = $"{AuthConstants.Issuer}/.well-known/openid_configuration";
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = AuthConstants.Issuer,
ValidateAudience = false, // 권한 모델에 따라 audience는 직접 검증
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(5)
};

options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var validationService = context.HttpContext.RequestServices
.GetRequiredService<IJwtValidationService>();

await validationService.ValidateTokenAsync(context);
},
OnAuthenticationFailed = context =>
{
// JWT 라이브러리 오류를 401로 처리
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
context.Response.WriteAsync($"{{\"error\": \"Invalid token\"}}");
context.HandleResponse();
return Task.CompletedTask;
}
};
});

builder.Services.AddAuthorization();

var app = builder.Build();

// 인증 (Authentication)/인가 (Authorization) 실패에 대한 전역 에러 처리
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (AuthorizationException ex)
{
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync($"{{\"error\": \"{ex.Message}\"}}");
}
});

// HTTP 요청 파이프라인 구성
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

권한 모델에 따라 JwtValidationService에서 적절한 검증 로직을 구현하세요:

JwtValidationService.cs
private void ValidatePayload(ClaimsPrincipal principal)
{
// audience 클레임이 API 리소스 지표와 일치하는지 확인
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("유효하지 않은 대상 (audience)");
}

// 글로벌 API 리소스에 필요한 스코프 확인
var requiredScopes = new[] { "api:read", "api:write" }; // 실제 필요한 스코프로 교체
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();

if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("스코프 (scope)가 부족합니다");
}
}

미들웨어를 API에 적용하기​

이제, 보호된 API 라우트에 미들웨어를 적용하세요.

이전 섹션에서 이미 인증 (Authentication) 및 인가 (Authorization) 미들웨어를 설정했습니다. 이제 액세스 토큰 (Access token)을 검증하고 인증된 요청에서 클레임 (Claim)을 추출하는 보호된 컨트롤러를 생성할 수 있습니다.

ProtectedController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace YourApiNamespace.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize] // 이 컨트롤러의 모든 액션에 인증 (Authentication) 필요
public class ProtectedController : ControllerBase
{
[HttpGet]
public IActionResult GetProtectedData()
{
// User 클레임 (Claim)에서 액세스 토큰 (Access token) 정보 직접 접근
var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value;
var clientId = User.FindFirst("client_id")?.Value;
var organizationId = User.FindFirst("organization_id")?.Value;
var scopes = User.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
var audience = User.FindAll("aud").Select(c => c.Value).ToArray();

return Ok(new {
sub,
client_id = clientId,
organization_id = organizationId,
scopes,
audience
});
}

[HttpGet("claims")]
public IActionResult GetAllClaims()
{
// 디버깅/점검을 위해 모든 클레임 (Claim) 반환
var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList();
return Ok(new { claims });
}
}
}

보호된 API 테스트하기​

액세스 토큰 (Access token) 받기​

클라이언트 애플리케이션에서: 클라이언트 통합을 설정했다면, 앱이 토큰을 자동으로 획득할 수 있습니다. 액세스 토큰 (Access token)을 추출하여 API 요청에 사용하세요.

curl / Postman으로 테스트할 때:

  1. 사용자 토큰: 클라이언트 앱의 개발자 도구에서 localStorage 또는 네트워크 탭에서 액세스 토큰 (Access token)을 복사하세요.

  2. 기계 간 (Machine-to-machine) 토큰: 클라이언트 자격 증명 플로우를 사용하세요. 다음은 curl을 사용한 비공식 예시입니다:

    curl -X POST https://your-tenant.logto.app/oidc/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=client_credentials" \
    -d "client_id=your-m2m-client-id" \
    -d "client_secret=your-m2m-client-secret" \
    -d "resource=https://your-api-resource-indicator" \
    -d "scope=api:read api:write"

    API 리소스 (API resource)와 권한 (Permission)에 따라 resource 및 scope 파라미터를 조정해야 할 수 있습니다. API가 조직 범위라면 organization_id 파라미터도 필요할 수 있습니다.

팁:

토큰 내용을 확인해야 하나요? 우리의 JWT 디코더를 사용하여 JWT를 디코드하고 검증하세요.

보호된 엔드포인트 테스트하기​

유효한 토큰 요청
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected

예상 응답:

{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
토큰 없음
curl http://localhost:3000/api/protected

예상 응답 (401):

{
"error": "Authorization header is missing"
}
잘못된 토큰
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected

예상 응답 (401):

{
"error": "Invalid token"
}

권한 (Permission) 모델별 테스트​

글로벌 스코프로 보호된 API 테스트 시나리오:

  • 유효한 스코프: 필요한 API 스코프(예: api:read, api:write)가 포함된 토큰으로 테스트하세요.
  • 스코프 누락: 토큰에 필요한 스코프가 없으면 403 Forbidden을 예상하세요.
  • 잘못된 대상 (Audience): 대상이 API 리소스와 일치하지 않으면 403 Forbidden을 예상하세요.
# 스코프가 누락된 토큰 - 403 예상
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected

추가 자료​

실전 RBAC: 애플리케이션을 위한 안전한 인가 (Authorization) 구현하기

멀티 테넌트 SaaS 애플리케이션 구축: 설계부터 구현까지 완벽 가이드