Skip to Content
플랫폼 아키텍처인증 · 권한

인증 · 권한

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-tokenBlueprint 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} on blueprint.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_iss dispatch / Rust: peek_issverify_rs256 vs Blueprint JWKS). 영속 설정(compose/.env.localBLUEPRINT_ISSUER 기본값)·e2e 검증(axe loginaxe <svc> tools GREEN). 비파괴 롤백 = 서비스 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 MCPHR backend 접근access_token (aud=client_id, v2)public client + PKCE, client_id=b7ead15d-2fea-4864-a5a8-b4b07d1629d4
Blueprint MCPplatform MCP at apexaccess_token (aud=URI, v1)public client + PKCE, /api/mcp apex
Vaultwardenpassword vault SSOOIDC id_tokenconfidential, secret 보관
Blueprint Webweb UI 로그인OIDC id_tokenNextAuth

자동 등록: 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 indicatorscope URI prefix 가 정확히 일치해야 Microsoft 가 token 발급. 발견된 함정:

  • AADSTS9010010: scope=api://137fc0ef-.../mcp.access + resource=https://axe.axelabs.ai/frame/mcp → mismatch.
  • 해결: domain verify (axe.axelabs.ai 에 TXT MS=ms... 등록) → Application ID URI 를 https://axe.axelabs.ai/frame/mcp (URL 형식) 로 변경 → scope 도 같은 prefix.

frame OAuth-RP middleware

frame 서버의 auth_oidc.py 가 다음을 수행:

  1. Authorization: Bearer &lt;access_token&gt; 헤더 추출
  2. iss claim peek (서명 검증 전 unverified decode) → Microsoft 시작이면 A 경로
  3. Microsoft JWKS fetch (1h TTL, kid miss 시 강제 갱신)
  4. RS256 서명 검증
  5. audience 검증: token aud{client_id GUID, Application ID URI} 중 하나와 일치 (v1/v2 호환)
  6. issuer 검증: https://login.microsoftonline.com/{tenant_id}/v2.0
  7. email / preferred_username / upn 중 첫 비어 있지 않은 값 → email
  8. customers.yaml > user_entity_map[email] → entity 권한 dict 매핑
  9. 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-resourceserver-level (RFC 8615 base URL convention)
/frame/mcp/.well-known/oauth-protected-resourceresource-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 < admin
  • read — query_balance, list_journals
  • write — post_journal, flag_uncertainty
  • close — 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 &lt;CFO agent JWT&gt; ← 광범위한 권한 X-User-Token: &lt;accountant A JWT&gt; ← axec 만, read+write

frame 의 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 없음AADSTS7000218Yes 토글 OR secret 입력
accessTokenAcceptedVersion=2 + aud check on URLmcp_client_invalidv2 강제 + 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 foundcustomers.yaml 확인
az ad app create 후 sign-in 시도 → 즉시 실패Entra trace ID + “Authorization with the MCP server failed”. az ad app createapplication object 만 생성, Service Principal 자동 생성 Xaz 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 permissiondelegated 대응 (이미 SCOPES)결정
calendar send-as (create_eventas_user_email)Calendars.ReadWriteCalendars.ReadWriteD-bp-mcp-calendar-2
send_mail send-as (as_user_email)Mail.SendMail.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 # 또는 -green

consent 누락 시 send-as 호출은 Graph 403 → 코드가 mail_send_not_consented (mail) / app_only_token_unavailable (calendar) 로 surface. self (delegated) 경로는 본 permission 없이도 동작하므로 send-as 만 영향.

Last updated on