본문으로 건너뛰기

FastAPI를 역할 기반 접근 제어 (RBAC) 및 JWT 검증으로 보호하세요

이 가이드는 Logto에서 발급한 역할 기반 접근 제어 (RBAC) 및 JSON Web Token (JWT)을 사용하여 FastAPI API를 안전하게 보호하기 위한 인가 (Authorization) 구현 방법을 안내합니다.

시작하기 전에​

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

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

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

학습 내용​

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

사전 준비 사항​

  • 최신 안정 버전의 Python 설치
  • FastAPI 및 웹 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 프로젝트 초기화하기​

새로운 FastAPI 프로젝트를 초기화하려면, 디렉터리를 생성하고 기본 구조를 설정하세요:

mkdir your-api-name
cd your-api-name

requirements 파일을 생성하세요:

requirements.txt
fastapi
uvicorn[standard]

의존성을 설치하세요:

pip install -r requirements.txt

기본 FastAPI 애플리케이션을 생성하세요:

main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
return {"message": "Hello from FastAPI"}

개발 서버를 시작하세요:

uvicorn main:app --reload
노트:

경로 연산, 의존성 주입 및 기타 기능 설정 방법에 대한 자세한 내용은 FastAPI 문서를 참조하세요.

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

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

auth_middleware.py
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'

class AuthInfo:
def __init__(self, sub: str, client_id: str = None, organization_id: str = None,
scopes: list = None, audience: list = None):
self.sub = sub
self.client_id = client_id
self.organization_id = organization_id
self.scopes = scopes or []
self.audience = audience or []

def to_dict(self):
return {
'sub': self.sub,
'client_id': self.client_id,
'organization_id': self.organization_id,
'scopes': self.scopes,
'audience': self.audience
}

class AuthorizationError(Exception):
def __init__(self, message: str, status: int = 403):
self.message = message
self.status = status
super().__init__(self.message)

def extract_bearer_token_from_headers(headers: dict) -> str:
"""
HTTP 헤더에서 bearer 토큰을 추출합니다.

참고: FastAPI 및 Django REST Framework에는 내장 토큰 추출 기능이 있으므로,
이 함수는 주로 Flask 및 기타 프레임워크용입니다.
"""
authorization = headers.get('authorization') or headers.get('Authorization')

if not authorization:
raise AuthorizationError('Authorization 헤더가 없습니다', 401)

if not authorization.startswith('Bearer '):
raise AuthorizationError('Authorization 헤더는 "Bearer "로 시작해야 합니다', 401)

return authorization[7:] # 'Bearer ' 접두사 제거

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를 위해 항상 권한(스코프)과 컨텍스트(대상, 조직)를 모두 검증하세요.

검증 로직 추가하기​

우리는 PyJWT를 사용하여 JWT를 검증합니다. 아직 설치하지 않았다면 설치하세요:

pip install pyjwt[crypto]

먼저, JWT 검증을 처리하기 위한 다음과 같은 공통 유틸리티를 추가하세요:

jwt_validator.py
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER

jwks_client = PyJWKClient(JWKS_URI)

def validate_jwt(token: str) -> Dict[str, Any]:
"""JWT를 검증하고 페이로드를 반환합니다"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)

payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer=ISSUER,
options={'verify_aud': False} # 대상 (Audience)는 수동으로 검증합니다
)

verify_payload(payload)
return payload

except jwt.InvalidTokenError as e:
raise AuthorizationError(f'유효하지 않은 토큰: {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'토큰 검증 실패: {str(e)}', 401)

def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""JWT 페이로드로부터 AuthInfo를 생성합니다"""
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
audience = payload.get('aud', [])
if isinstance(audience, str):
audience = [audience]

return AuthInfo(
sub=payload.get('sub'),
client_id=payload.get('client_id'),
organization_id=payload.get('organization_id'),
scopes=scopes,
audience=audience
)

def verify_payload(payload: Dict[str, Any]) -> None:
"""권한 모델에 따라 페이로드를 검증합니다"""
# 권한 모델에 따라 검증 로직을 여기에 구현하세요
# 아래 권한 모델 섹션에서 예시를 확인할 수 있습니다
pass

그 다음, 액세스 토큰을 검증하는 미들웨어를 구현하세요:

auth_middleware.py
from fastapi import HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jwt_validator import validate_jwt, create_auth_info

security = HTTPBearer()

async def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AuthInfo:
try:
token = credentials.credentials
payload = validate_jwt(token)
return create_auth_info(payload)

except AuthorizationError as e:
raise HTTPException(status_code=e.status, detail=str(e))

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

jwt_validator.py
def verify_payload(payload: Dict[str, Any]) -> None:
"""글로벌 API 리소스에 대한 페이로드를 검증합니다"""
# 대상 (Audience) 클레임이 API 리소스 지표와 일치하는지 확인
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]

if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('유효하지 않은 대상 (Audience)')

# 글로벌 API 리소스에 필요한 스코프 확인
required_scopes = ['api:read', 'api:write'] # 실제 필요한 스코프로 교체하세요
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('스코프가 부족합니다')

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

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

app.py
from fastapi import FastAPI, Depends
from auth_middleware import verify_access_token, AuthInfo

app = FastAPI()

@app.get("/api/protected")
async def protected_endpoint(auth: AuthInfo = Depends(verify_access_token)):
# auth 매개변수에서 인증 (Authentication) 정보를 직접 접근합니다
return {"auth": auth.to_dict()}

보호된 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 애플리케이션 구축: 설계부터 구현까지 완벽 가이드