현대 분산 시스템 환경에서 서비스 간 리소스 접근을 제어하는 것은 보안 아키텍처의 핵심 과제입니다. 과거 '비밀번호 안티패턴(Password Anti-Pattern)'이라 불리는 방식, 즉 제3자 애플리케이션이 사용자의 자격 증명(ID/Password)을 직접 받아 저장하는 방식은 보안 경계(Security Perimeter)를 무너뜨리고 공격 표면(Attack Surface)을 불필요하게 확장하는 결과를 초래했습니다. OAuth 2.0은 이러한 문제를 해결하기 위해 인증(Authentication)과 권한 부여(Authorization)를 분리하고, 리소스 접근 권한을 안전하게 위임(Delegation)하는 표준 프로토콜로 자리 잡았습니다. 본 글에서는 OAuth 2.0의 기술적 설계 원칙과 주요 권한 부여 흐름, 그리고 보안 강화를 위한 최신 구현 패턴을 분석합니다.
1. 프로토콜 구성 요소 및 역할 정의
OAuth 2.0 프레임워크는 역할(Roles)의 명확한 분리를 통해 시스템 간 결합도를 낮춥니다. 이 구조는 단일 실패 지점(SPOF)을 관리 가능한 단위로 격리하고, 각 컴포넌트의 책임을 명확히 합니다. 프로토콜은 크게 네 가지 주체로 구성되며, 이들의 상호작용을 이해하는 것이 설계의 기초가 됩니다.
- 리소스 소유자 (Resource Owner): 데이터에 대한 접근 권한을 통제하는 최종 주체(End-User)입니다.
- 클라이언트 (Client): 리소스 소유자의 권한을 위임받아 리소스 서버에 요청을 보내는 애플리케이션입니다.
- 권한 서버 (Authorization Server): 클라이언트를 인증하고, 리소스 소유자의 승인을 확인하여 Access Token을 발급하는 서버입니다.
- 리소스 서버 (Resource Server): Access Token을 검증하고, 요청된 범위(Scope) 내에서 리소스를 제공하는 서버입니다.
2. 권한 부여 흐름(Grant Types)의 선택과 설계
OAuth 2.0의 핵심은 클라이언트의 유형과 신뢰 수준에 따라 적절한 권한 부여 흐름(Grant Type)을 선택하는 것입니다. 잘못된 흐름의 선택은 치명적인 보안 취약점으로 이어집니다.
2.1 Authorization Code Grant (With PKCE)
가장 표준적이고 강력한 보안을 제공하는 흐름입니다. 초기에는 서버 사이드 웹 애플리케이션을 위해 설계되었으나, 현재는 PKCE (Proof Key for Code Exchange) 확장을 통해 SPA(Single Page Application)나 모바일 앱과 같은 Public Client에서도 표준으로 사용됩니다.
PKCE는 `client_secret`을 안전하게 저장할 수 없는 환경에서 권한 코드 탈취 공격(Authorization Code Interception Attack)을 방지합니다. 동작 원리는 다음과 같습니다.
# PKCE 흐름의 핵심: Code Verifier 및 Challenge 생성 (Python 예시)
import secrets
import hashlib
import base64
def generate_pkce_pair():
# 1. 고해상도 난수로 Code Verifier 생성 (43~128자)
code_verifier = secrets.token_urlsafe(64)
# 2. SHA-256 해싱 후 Base64 URL-safe 인코딩으로 Challenge 생성
hashed = hashlib.sha256(code_verifier.encode('ascii')).digest()
code_challenge = base64.urlsafe_b64encode(hashed).decode('ascii').rstrip('=')
return code_verifier, code_challenge
# 클라이언트는 /authorize 요청 시 code_challenge를 전송하고
# /token 요청 시 원본 code_verifier를 전송하여 정당성을 증명함
2.2 Client Credentials Grant
사용자(User)의 개입 없이 머신 투 머신(M2M) 통신이나 데몬 서비스가 자신의 자격 증명으로 리소스에 접근할 때 사용됩니다. 이 경우 클라이언트는 리소스 소유자이자 요청자가 됩니다. 인증 절차가 간소하지만, `client_secret` 관리가 보안의 핵심이 됩니다.
| Grant Type | 대상 클라이언트 | 보안 특징 | 비고 |
|---|---|---|---|
| Auth Code + PKCE | SPA, Mobile, Web App | 코드 탈취 방지, Access Token 노출 최소화 | 표준 권장 사항 |
| Client Credentials | M2M, Backend Services | 사용자 컨텍스트 없음, 서버 간 인증 | 배치 작업, 내부 통신 |
| Refresh Token | 모든 클라이언트 | Access Token 만료 시 재발급 | 로그인 유지 경험 제공 |
3. OIDC(OpenID Connect)와 Identity Layer
OAuth 2.0은 본질적으로 권한 부여(Authorization) 프로토콜입니다. "누구인가(Authentication)"와 "무엇을 할 수 있는가(Authorization)"를 혼동해서는 안 됩니다. OAuth 2.0 만으로는 표준화된 방식으로 사용자 신원 정보를 획득하기 어렵습니다. 이를 보완하기 위해 등장한 계층이 OIDC입니다.
OIDC는 OAuth 2.0 흐름 상에서 scope=openid를 추가함으로써 동작하며, Access Token과 함께 ID Token을 발급합니다. ID Token은 JWT(JSON Web Token) 포맷으로, 서명된 사용자 정보를 포함하여 클라이언트가 위변조 여부를 검증할 수 있게 합니다.
# OIDC 토큰 교환 응답 예시 (JSON)
{
"access_token": "eyJraW...", // 리소스 접근용 (Opaque or JWT)
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGci...", // 신원 확인용 (JWT 필수)
"refresh_token": "tGzv3J..." // 토큰 갱신용
}
결론 및 엔지니어링 제언
OAuth 2.0과 OIDC는 복잡한 보안 요구사항을 추상화하여 안전한 데이터 연동 환경을 제공합니다. 그러나 프로토콜 자체가 보안을 보장하는 것은 아닙니다. 개발자는 토큰의 생명주기(Lifecycle) 관리, 적절한 Grant Type의 선정, 그리고 PKCE와 같은 보안 확장의 필수적 적용을 통해 구현상의 허점을 제거해야 합니다. 특히 OAuth 2.1 스펙이 구체화됨에 따라, 레거시 패턴(Implicit Grant 등)을 제거하고 제로 트러스트(Zero Trust) 원칙에 부합하는 인증/인가 아키텍처를 설계하는 것이 필수적입니다.
Post a Comment