인증 · 권한
3 가지 인증 경로
| 경로 | 사용 주체 | 토큰 | 검증 |
|---|---|---|---|
| A. Microsoft Entra ID OAuth | 직원 (Claude Code / claude.ai) | access_token (RS256, Microsoft 서명) | frame middleware: JWKS RS256 + aud match |
| B. frame HS256 JWT | 운영자 / 서비스 토큰 (cron job 등) | HS256 (FRAME_JWT_SECRET 으로 서명) | frame middleware: audience=client_id |
| C. dual-token | Blueprint agent + per-user 위임 | (agent_jwt, user_jwt) | intersection of permissions |
iss claim 으로 자동 dispatch — https://login.microsoftonline.com/... 시작이면 A, 아니면 B.
경로 D — Blueprint 플랫폼 토큰 (D-axe-idp-1, Phase 1+2 LIVE 2026-06-04): Blueprint = 플랫폼 OIDC Provider 가 4번째 신뢰 발행자로 합류 — 로그인 1회로 전 서비스. Blueprint 가 Entra 를 federate(기존 NextAuth 세션 재사용) + RS256 플랫폼 토큰 발행(
/oauth/*+/.well-known/{openid-configuration,jwks.json}onblueprint.axellc.com). 각 서비스는iss=blueprint분기에서 Blueprint JWKS 로 RS256 검증(aud=https://axe.axelabs.ai), email→entity 는 기존customers.yaml그대로. frame·hive·cortex·index·matrix + Blueprint 자체 MCP — 6개 서비스 전부 Blueprint 토큰 신뢰 (Python:auth_blueprint.py+is_blueprint_issdispatch / Rust:peek_iss→verify_rs256vs Blueprint JWKS). 영속 설정(compose/.env.local의BLUEPRINT_ISSUER기본값)·e2e 검증(axe login→axe <svc> toolsGREEN). 비파괴 롤백 = 서비스BLUEPRINT_*env 제거(unset=종전 Microsoft 경로). 설계·현행 SSOT: /architecture/platform-identity.
경로 A — Microsoft Entra ID (직원)
각 customer 의 Microsoft Entra ID tenant 에 5 개의 app 이 등록되어 있어야 합니다 (axe customer 2026-05-21 현재):
| App 이름 | 용도 | 토큰 종류 | 비고 |
|---|---|---|---|
| Frame MCP | 회계 backend 접근 | access_token (aud=client_id, v2) | public client + PKCE (b7ead15d-... Hive 패턴) |
| Hive MCP | HR backend 접근 | access_token (aud=client_id, v2) | public client + PKCE, client_id=b7ead15d-2fea-4864-a5a8-b4b07d1629d4 |
| Blueprint MCP | platform MCP at apex | access_token (aud=URI, v1) | public client + PKCE, /api/mcp apex |
| Vaultwarden | password vault SSO | OIDC id_token | confidential, secret 보관 |
| Blueprint Web | web UI 로그인 | OIDC id_token | NextAuth |
자동 등록: az ad app create + az rest --method PATCH /applications/APP_ID (identifierUris + requestedAccessTokenVersion=2 + oauth2PermissionScopes.value=“mcp.access”). 운영자 손작업 = axe.axelabs.ai 도메인 검증 1회만.
Frame MCP app 설정 (현재 axe live)
client_id: 137fc0ef-eb9f-4903-acbc-1a748add349c
application_id_uri: https://axe.axelabs.ai/frame/mcp # 도메인 검증 필요
tenant_id: 122fb574-7efa-476a-95b6-bee81bce2cce
platform: Web
redirect_uris:
- https://claude.ai/api/mcp/auth_callback
- https://claude.com/api/mcp/auth_callback
scopes:
- openid
- profile
- email
- https://axe.axelabs.ai/frame/mcp/mcp.access
allow_public_client_flows: true # PKCE 활성 (manifest `isFallbackPublicClient: true`)
# client_secret 발급 여부:
# - 직접-Microsoft path (현재 live): 발급 X (PKCE-only, claude.ai 가 secret 안 보냄)
# - OAuth proxy path (D-ops-15 dormant): 발급 + frame .env 에 보관
# access_token_version (manifest `requestedAccessTokenVersion`):
# - null/1 → access_token aud = Application ID URI (https://...)
# - 2 → access_token aud = client_id GUID
# frame middleware 는 양쪽 모두 수용 (audiences=[client_id, app_id_uri]).Azure App ID URI
Microsoft v2 endpoint 의 resource indicator 와 scope URI prefix 가 정확히 일치해야 Microsoft 가 token 발급. 발견된 함정:
- AADSTS9010010: scope=
api://137fc0ef-.../mcp.access+ resource=https://axe.axelabs.ai/frame/mcp→ mismatch. - 해결: domain verify (
axe.axelabs.ai에 TXTMS=ms...등록) → Application ID URI 를https://axe.axelabs.ai/frame/mcp(URL 형식) 로 변경 → scope 도 같은 prefix.
frame OAuth-RP middleware
frame 서버의 auth_oidc.py 가 다음을 수행:
Authorization: Bearer <access_token>헤더 추출issclaim peek (서명 검증 전 unverified decode) → Microsoft 시작이면 A 경로- Microsoft JWKS fetch (1h TTL, kid miss 시 강제 갱신)
- RS256 서명 검증
- audience 검증: token
aud가{client_id GUID, Application ID URI}중 하나와 일치 (v1/v2 호환) - issuer 검증:
https://login.microsoftonline.com/{tenant_id}/v2.0 email/preferred_username/upn중 첫 비어 있지 않은 값 → emailcustomers.yaml > user_entity_map[email]→ entity 권한 dict 매핑TokenClaims생성 → ContextVar 주입 → tool 호출에 사용
RFC 9728 protected-resource metadata path
claude.ai Connector 와 다른 MCP client 가 OAuth challenge 시작 시 fetch 하는 metadata endpoint 는 두 path 에서 동일한 응답:
| Path | 용도 |
|---|---|
/frame/.well-known/oauth-protected-resource | server-level (RFC 8615 base URL convention) |
/frame/mcp/.well-known/oauth-protected-resource | resource-level (RFC 9728 — application_id_uri 의 sub-path). claude.ai Connector / Anthropic MCP client 가 이 path 를 fetch |
양쪽 모두 _PUBLIC_PATHS 로 unauthenticated 면제 + inner.router 에 같은 handler 마운트. 401 응답의 WWW-Authenticate: Bearer realm="frame", resource_metadata="..." 가 가리키는 URL 도 양쪽 모두 valid (D-ops-23, 2026-05-22 — 이전엔 resource-level path 401 → claude.ai Reconnect silently 실패).
Schema discovery — auth-required
/frame/schemas (그리고 hive 의 /hive/schemas) 는 RFC 9728 metadata 와 달리 auth-required — _PUBLIC_PATHS 미포함. JWTAuthMiddleware 가 일반 MCP 호출과 동일 검증 적용. Blueprint artifact + PARA 지식 레이어 (D-bp-artifact-1) 가 fetch 시 자기 MCP 토큰 재사용. 자세한 endpoint contract = services/frame § Schema discovery.
권한 모델 — Scope
read < write < close < adminread— query_balance, list_journalswrite— post_journal, flag_uncertaintyclose— soft_close period, hard_close (운영자)reopen— 폐쇄된 period 재오픈 (드물게)admin— chart of accounts 변경, policy 마이그레이션
권한은 entity 별로 부여 (예: {"axec": ["read", "write"], "axev": ["read"]}).
dual-token (CFO + accountant 패턴)
CFO 에이전트가 회계사 A 를 대신해 활동할 때:
Authorization: Bearer <CFO agent JWT> ← 광범위한 권한
X-User-Token: <accountant A JWT> ← axec 만, read+writeframe 의 dual_authorize(agent, user, entity, scope) 가 두 토큰의 권한 교집합으로 게이팅:
effective_scopes = agent.permissions["axec"] ∩ user.permissions["axec"]최소 권한 원칙 강제. agent 가 admin 이어도 user 가 read-only 면 read 만 가능.
운영자 자체 토큰
운영자 ([email protected]) 가 CLI 에서 직접 호출 시:
docker exec frame-mcp-blue python -m frame.cli mcp-token \
--sub [email protected] \
--customer axe \
--entity axec:read,write,close \
--entity axev:read,write,close \
--ttl 2592000 # 30 days→ HS256 frame JWT 발급. 경로 B 로 검증.
운영자 토큰은 Vaultwarden 의 axe-frame-jwt collection 에 보관 (entity 별 vault).
함정
| 함정 | 결과 | 회피 |
|---|---|---|
| Application ID URI 와 scope prefix 불일치 | AADSTS9010010 | 두 값 같은 URL prefix |
| Allow public client flows: No + secret 없음 | AADSTS7000218 | Yes 토글 OR secret 입력 |
| accessTokenAcceptedVersion=2 + aud check on URL | mcp_client_invalid | v2 강제 + middleware multi-issuer 양쪽 모두: (1) Azure app 등록 시 requestedAccessTokenVersion=2 강제 (bootstrap.sh manifest PATCH 시점), (2) middleware 가 v1+v2 issuer 양쪽 수용 (defense in depth — v1 token 이 어떤 사유로 들어와도 graceful 처리). 단독 (1) = bootstrap PATCH 누락 시 사고. 단독 (2) = client 측 v1 token 발급 자유 잔존. |
| App ID URI 삭제 (cleanup 잘못) | AADSTS500011 resource principal not found | 복원 후 propagation 5-10분 대기 |
| 잘못된 client_id 입력 (vaultwarden vs frame_mcp) | AADSTS700016 application not found | customers.yaml 확인 |
az ad app create 후 sign-in 시도 → 즉시 실패 | Entra trace ID + “Authorization with the MCP server failed”. az ad app create 는 application object 만 생성, Service Principal 자동 생성 X | az ad sp create --id <appId> 별도 호출 |
| SP 있는데 sign-in 시 같은 메시지 | Service Principal 존재해도 requiredResourceAccess 비어 있고 admin consent 안 받음 | manifest 에 mcp.access (self) + Graph User.Read (e1fe6dd8-ba31-4d61-89e7-88639da4683d) requiredResourceAccess 추가 후 admin consent grant |
az ad app permission admin-consent → 403 Forbidden | 명령 실행 user 가 tenant admin role 없음 (“This operation can only be performed by an administrator”) | (a) Portal UI: App registrations → 해당 app → API permissions → “Grant admin consent for {tenant}” 버튼, (b) admin user 로 az login 후 명령 재실행 |
az ad app credential reset --append parsing 시 stderr 섞임 | JSON 출력에 progress 메시지 혼합 → python3 -c "json.load" 실패 | --query 'password' -o tsv 로 단일 필드 추출 |
| Vaultwarden SSO with Microsoft Entra ID → “Your provider does not send email verification status” | Entra ID id_token 에 email_verified claim 없음, Timshel fork 가 거부 | SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION: "true" 추가 (D-ops-24, 2026-05-22) |
Blueprint OAuth scope 추가 (SCOPES in src/lib/graph.ts) ship 후 admin consent grant 누락 | (1) 모든 user 의 GraphToken refresh 침묵 실패 — UI 상 “Connected” 표시 유지하나 실 호출 시 401/403, (2) 새 user OAuth flow 차단 AADSTS65001 Consent_VersionMismatch, (3) acquireTokenByClientCredential 캐시된 옛 토큰 (insufficient-scope) 계속 반환 | (1) ship 직후 az ad app permission admin-consent --id <client_id> 실행, (2) docker restart blueprint-app-green (또는 -blue) — MSAL in-memory 캐시 폐기, (3) 자동화 후보: axe ship blueprint post-deploy hook + Blueprint daily token health check (B-blueprint-scope-change-admin-consent-runbook 참조). 절차 상세 = 아래 OAuth scope 추가 시 운영자 절차 |
Frame/Hive MCP middleware 의 _MICROSOFT_ISS_PREFIX 가 v2 issuer prefix (https://login.microsoftonline.com/) 만 매치 (trap #33, Truvia 2026-05-25 보고) | Microsoft v1 access_token (iss=https://sts.windows.net/<tenant>/) 보내면 middleware 가 Microsoft 경로 미인식 → HS256 fallback path 진입 → algorithms=["RS256"] allowlist 가 RS256 token 을 잘못된 path 에서 reject → The specified alg value is not allowed | (1) Azure app 등록 시 requestedAccessTokenVersion=2 강제 (bootstrap.sh + 등록 후 manifest 검증 — Truvia 우회 = 3 MCP app 모두 manifest PATCH), (2) middleware 가 v1+v2 issuer 양쪽 수용 (Blueprint MCP 의 config.py:76,81 + auth_oidc.py:153 패턴 mirror). 위치: /Users/axe/frame/src/frame/mcp/http_server.py:141 + /Users/axe/hive/src/hive/mcp/http_server.py:67. 영구 fix = B-trap-33-frame-hive-multi-issuer (code portion 잔존, docs portion ✅ 2026-05-28) |
상세 troubleshooting: /onboard/troubleshooting.
OAuth scope 추가 시 운영자 절차
Blueprint Next.js app (2b222356-1c36-48e0-96a3-2c5e0ecbf937) 의 SCOPES array (src/lib/graph.ts) 변경 ship 시 다음 순서를 그대로 따를 것. 누락 시 위 함정 표 마지막 row 의 3 가지 증상 발현 (2026-05-26 D-bp-mcp-calendar-1 ship 후 발현 사례 있음).
# 1. SCOPES array 변경 commit + ship
cd /Users/axe/blueprint && axe ship
# 2. admin consent grant (운영자 = Global Admin 만 가능, soohun.kang)
az login --allow-no-subscriptions
az ad app permission admin-consent --id <client_id>
# (client_id = Blueprint Next.js app, 예: 2b222356-1c36-48e0-96a3-2c5e0ecbf937)
# 3. blueprint-app 재시작 (MSAL in-memory 캐시 폐기)
ssh axe-macmini "cd /Users/axe/blueprint && docker compose restart blueprint-app-blue blueprint-app-green"
# 4. 검증 (token health)
# - /api/admin/graph-tokens (또는 /settings) 에서 각 user 의 expiresAt 갱신 확인
# - 본인 계정으로 Graph API 호출 1회 (e.g. /api/admin/broadcast-dm test)
# 5. user 측 reconnect 안내 (Teams DM)
# - admin consent 후에도 옛 refresh token 의 grant 가 새 scope 안 가짐
# - 각 user 가 /api/graph/auth 재방문 → 새 scope 포함 consent → 새 refresh token 발급본 절차 가운데 step 3 (MSAL in-memory cache 폐기) 의 근본 원인은 /ops/known-gaps#msal-acquiretokenbyclientcredential-토큰-캐시 참조. Blueprint 의 Azure App 2 개 (Next.js app 2b222356-... vs MCP custom connector 482598f7-540c-462c-9dfd-b957651eb804) 구분도 같은 페이지에 있음 — 본 SCOPES 변경 및 admin consent 는 Next.js app 측에만 적용.
app-only Application permission (send-as)
위 OAuth scope 추가 시 운영자 절차 는 delegated scope (SCOPES in src/lib/graph.ts, caller 본인 토큰) 변경용이다. 그와 별개로, admin send-as 기능 (caller role=admin 이 타 사용자 리소스에 write) 은 app-only token (getAppOnlyClient() = acquireTokenByClientCredential) 을 쓰며, Application permission + tenant admin consent 를 요구한다. delegated 와 달리 사용자별 re-consent 가 아니라 테넌트 1회 admin consent 다.
대상 App = Blueprint Next.js app (2b222356-1c36-48e0-96a3-2c5e0ecbf937) — MCP connector app (482598f7-...) 이 아니다 (함정 /ops/known-gaps#blueprint-azure-app-id-혼동).
| 기능 | Application permission | delegated 대응 (이미 SCOPES) | 결정 |
|---|---|---|---|
calendar send-as (create_event 등 as_user_email) | Calendars.ReadWrite | Calendars.ReadWrite | D-bp-mcp-calendar-2 |
send_mail send-as (as_user_email) | Mail.Send | Mail.Send (→ self 발송은 추가 consent 불필요) | D-bp-mcp-mail-1 |
# 1. permission 추가: Azure Portal → App registrations → Blueprint (Next.js)
# → API permissions → Add → Microsoft Graph → Application permissions
# → Mail.Send (well-known roleId b633e1c5-b582-4048-a93e-9f11b44c7e96)
# 2. tenant admin consent (Global Admin 계정 — AXE 테넌트는 soohun.kang 만)
az ad app permission admin-consent --id 2b222356-1c36-48e0-96a3-2c5e0ecbf937
# 3. MSAL client-credential in-memory 캐시 폐기 (옛 insufficient-scope 토큰 재사용 방지)
docker restart blueprint-app-blue # 또는 -greenconsent 누락 시 send-as 호출은 Graph 403 → 코드가 mail_send_not_consented (mail) / app_only_token_unavailable (calendar) 로 surface. self (delegated) 경로는 본 permission 없이도 동작하므로 send-as 만 영향.