[Glossary] OBO 인증 완벽 정리
한 줄 요약: OBO(On-Behalf-Of) 인증이란, 마이크로서비스 환경에서 중간 API가 사용자의 신원을 유지한 채 하위 API를 연속으로 호출할 수 있도록 토큰을 체인 방식으로 교환하는 OAuth 2.0 기반 인증 흐름이다.
1. OBO 인증이란 무엇인가?
택배 배송 대행을 떠올려 보세요. 고객(사용자)이 쇼핑몰(프론트엔드)에 주문하면, 쇼핑몰은 물류 센터(API A)에 처리를 맡깁니다. 물류 센터는 배송 기사(API B)에게 다시 지시를 내릴 때, “쇼핑몰이 시켜서”가 아니라 “고객 홍길동이 주문한 건”이라는 사실을 그대로 전달해야 합니다. OBO는 바로 이 “고객 이름표”가 서비스 체인 전체에 유지되도록 보장하는 인증 흐름입니다.
공식 정의로는, RFC 8693 Token Exchange 스펙과 Microsoft Identity Platform의 On-Behalf-Of Flow로 표준화되어 있으며, 사용자가 동의한 액세스 토큰을 중간 서비스가 인증 서버에 제시하여 다른 대상(Audience)의 토큰으로 교환하는 방식입니다.
쉽게 말해, “A가 B에게 요청을 보냈는데, B가 일처리를 위해 A의 이름(권한)으로 C에게 다시 요청하는 구조” 에서 사용됩니다.
2. 핵심 개념 이해하기
OBO가 필요한 이유: 일반 OAuth 2.0의 한계
일반적인 OAuth 2.0에서는 프론트엔드가 발급받은 액세스 토큰으로 API A를 호출하면 끝납니다. 하지만 마이크로서비스 아키텍처(MSA) 환경에서는 두 가지 문제가 발생합니다.
| 문제 | 설명 |
|---|---|
| 토큰 전달의 한계 | 프론트엔드가 API A용으로 발급받은 토큰은 Audience가 API A로 제한되어 있어, API A가 이를 그대로 API B에 전달할 수 없습니다 |
| 익명성 방지 실패 | API A가 자신의 서비스 권한(Client Credentials)으로 API B를 호출하면, API B 입장에서 실제 사용자가 누구인지 알 수 없어 감사(Audit) 추적이 불가능합니다 |
OBO는 이 두 문제를 모두 해결하여 사용자의 신원 정보와 권한(Scope)을 하위 서비스까지 안전하게 체인(Chain) 형태로 전달합니다.
OBO 인증 기본 흐름 (Flow)
사용자
│ ① 로그인 → 토큰 1 발급 (Audience: API A)
▼
프론트엔드 앱
│ ② HTTP 헤더에 토큰 1 담아 호출
▼
API A (중간 서비스)
│ ③ IDP에 토큰 1 제시 → "사용자 대신 API B용 토큰 2 발급 요청"
▼
인증 서버 (IDP / Entra ID)
│ ④ 토큰 1 검증 → 토큰 2 발급 (Audience: API B)
▼
API A
│ ⑤ 토큰 2로 API B 호출
▼
API B (하위 서비스)
토큰 교환 요청 파라미터
OBO 흐름의 핵심인 Step ③ 토큰 교환 요청은 다음 HTTP POST 형태로 인증 서버에 전달됩니다.
POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&client_id={API A의 클라이언트 ID}
&client_secret={API A의 시크릿}
&assertion={토큰 1 (사용자가 API A에 보낸 액세스 토큰)}
&requested_token_use=on_behalf_of
&scope={API B의 스코프} (예: api://api-b/.default)
핵심 파라미터 설명
| 파라미터 | 역할 |
|---|---|
grant_type |
OBO 전용 grant type 지정 |
assertion |
사용자가 원래 발급받은 토큰 1 (사용자 신원 증명) |
requested_token_use |
on_behalf_of 고정값 |
scope |
새로 발급받을 토큰 2의 대상(API B) 및 권한 |
3. 실무 적용 예시
Python + MSAL을 활용한 OBO 흐름 구현
import msal
import requests
# API A 서버에서 실행되는 코드
TENANT_ID = "your-tenant-id"
CLIENT_ID = "api-a-client-id"
CLIENT_SECRET = "api-a-client-secret"
API_B_SCOPE = ["api://api-b-client-id/.default"]
def call_api_b_on_behalf_of_user(user_access_token: str) -> dict:
"""
사용자로부터 받은 토큰(assertion)으로 API B 호출용 토큰을 교환하고,
API B를 사용자 신원으로 호출합니다.
"""
authority = f"https://login.microsoftonline.com/{TENANT_ID}"
# MSAL ConfidentialClientApplication 초기화
app = msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=authority,
)
# OBO 토큰 교환 요청
result = app.acquire_token_on_behalf_of(
user_assertion=user_access_token, # 토큰 1
scopes=API_B_SCOPE, # API B 대상 스코프
)
if "access_token" not in result:
error = result.get("error_description", "Unknown error")
raise Exception(f"OBO 토큰 교환 실패: {error}")
token_2 = result["access_token"] # 토큰 2 (Audience: API B)
# 토큰 2로 API B 호출
response = requests.get(
"https://api-b.example.com/data",
headers={"Authorization": f"Bearer {token_2}"}
)
response.raise_for_status()
return response.json()
Azure Function에서의 OBO 패턴
import azure.functions as func
import msal
import json
app = func.FunctionApp()
@app.route(route="process-data", auth_level=func.AuthLevel.ANONYMOUS)
def process_data(req: func.HttpRequest) -> func.HttpResponse:
# 프론트엔드가 보낸 Bearer 토큰 추출
auth_header = req.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return func.HttpResponse("Unauthorized", status_code=401)
user_token = auth_header.split(" ")[1] # 토큰 1
try:
# OBO로 하위 서비스(Microsoft Graph) 호출
graph_data = call_graph_on_behalf_of(user_token)
return func.HttpResponse(
json.dumps(graph_data),
mimetype="application/json"
)
except Exception as e:
return func.HttpResponse(str(e), status_code=500)
def call_graph_on_behalf_of(user_token: str) -> dict:
app_client = msal.ConfidentialClientApplication(
client_id=FUNC_CLIENT_ID,
client_credential=FUNC_CLIENT_SECRET,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
)
result = app_client.acquire_token_on_behalf_of(
user_assertion=user_token,
scopes=["https://graph.microsoft.com/User.Read"],
)
token_2 = result["access_token"]
response = requests.get(
"https://graph.microsoft.com/v1.0/me",
headers={"Authorization": f"Bearer {token_2}"}
)
return response.json()
3-Tier 체인 (A → B → C) 구조
사용자 → API A: 토큰1(aud=A)
API A → IDP: 토큰1 교환 → 토큰2(aud=B)
API A → API B: 토큰2
API B → IDP: 토큰2 교환 → 토큰3(aud=C)
API B → API C: 토큰3
각 단계에서 원본 사용자 클레임(sub, oid, upn)이 새 토큰에 유지되므로, API C도 요청을 처음 시작한 사용자가 누구인지 검증할 수 있습니다.
4. OBO vs 유사 인증 흐름 비교
| 구분 | OBO Flow | Client Credentials | Authorization Code | Token Forwarding |
|---|---|---|---|---|
| 사용자 신원 유지 | ✅ 유지됨 | ❌ 서비스 신원만 | ✅ 유지됨 | ⚠️ 부분적 |
| 사용자 개입 | 최초 1회만 | 없음 | 매번 필요 | 없음 |
| Audience 변환 | ✅ 가능 | ✅ 가능 | ❌ | ❌ |
| 감사(Audit) 추적 | ✅ 가능 | ❌ 불가 | ✅ 가능 | ⚠️ 위험 |
| 주요 사용 환경 | MSA 서비스 체인 | 서비스 간 M2M | 최초 사용자 로그인 | 단순 프록시 |
| 토큰 보안 | ✅ 재발급 | ✅ 재발급 | ✅ 재발급 | ❌ 원본 전달 |
Token Forwarding(토큰 그대로 전달)은 안티패턴입니다. 토큰의 Audience 제한을 우회하고, 어떤 서비스가 어떤 목적으로 사용했는지 추적이 불가능해집니다.
5. 마치며
OBO 인증은 마이크로서비스 아키텍처에서 “사용자 신원을 잃지 않으면서 안전한 서비스 체인 호출” 을 가능하게 하는 핵심 패턴입니다. 세 가지 핵심만 기억하세요.
- 보안 전파(Identity Propagation): 서비스가 아무리 깊게 중첩되어도(A → B → C) 최초 사용자 신원이 유지됩니다.
- 최소 권한 원칙: 각 서비스는 자신에게 맞는 Audience/Scope 토큰만 수신하여 무단 재사용을 방지합니다.
- 사용자 개입 최소화: 사용자는 최초 로그인 한 번만 하면 되고, 이후 토큰 교환은 백엔드 간 자동으로 처리됩니다.
다음으로는 IAM, JWT, Zero Trust 개념을 함께 학습하면 MSA 보안 설계 역량이 한층 깊어집니다.
참고 자료
- Microsoft Identity Platform - On-Behalf-Of Flow 공식 문서 — Azure AD OBO 흐름 전체 사양 및 예제
- RFC 8693 - OAuth 2.0 Token Exchange — OBO의 기반이 되는 IETF 토큰 교환 표준 스펙
함께 읽으면 좋은 용어
이 개념과 함께 알아두면 이해가 깊어지는 관련 용어들입니다.
댓글 남기기