플랫폼 신원 — Blueprint = OIDC Provider
상태: Phase 1+2 LIVE (2026-06-04). Blueprint OP(discovery·jwks·authorize·token) + RS256 서명키(vault
blueprint/axe/oidc-signing-key) +axe loginloopback-PKCE + frame·hive·cortex·index·matrix + Blueprint 자체 MCP — 6개 서비스 전부 Blueprint 토큰 신뢰 (영속 설정, e2e 검증).axe login→axe <svc> toolsGREEN. 본 페이지는 D-axe-idp-1 설계 SSOT 이자 현행 구현 기준. ⚠️ 발행자 자신이 마지막 resource server였다 (2026-06-04 fix): Blueprint MCP 는 토큰을 발행하면서도 검증은 Microsoft Entra 경로만 알아, 자기 플랫폼 토큰을unknown_kid로 401 했다 (kid = Blueprint OIDC 서명키, Microsoft JWKS 에 부재). frame 의auth_blueprint.py+ iss-dispatch 를 미러해 Blueprint MCP 도 자기 issuer 의 resource server 로 배선 (BLUEPRINT_ISSUER=https://blueprint.axellc.com). 교훈: OP 를 세울 때 그 OP 의 자체 MCP 도 resource-server 목록에 포함해야 한다. 비파괴 cutover 유지 (BLUEPRINT_ISSUER미설정 시 서비스는 종전 Microsoft 경로). 인가 중앙화(entity grant 토큰 삽입)는 여전히 Phase 3 — 현재는 인증만 Blueprint, 인가는 각 서비스 customers.yaml.
본질
오늘 외부/멀티에이전트 접근의 마찰은 서비스마다 따로 인증한다는 데서 온다. 직원·에이전트가 frame·hive·index·cortex·matrix 를 쓰려면 서비스별 MCP OAuth 를 각각 통과하고, frame·cortex 는 각자 OAuth-RP 프록시 (D-ops-14/15) 를 따로 운영한다. 신원은 N 곳에 흩어져 있다.
목표 한 줄: 사람은 한 번 SSO 로그인하고, 그 결과로 받은 하나의 플랫폼 토큰으로 모든 서비스를 쓴다. 거버넌스(누가·어떤 스코프·취소·감사)는 Blueprint 한 곳에 모인다.
그 한 곳이 Blueprint 인 이유:
- Blueprint 는 이미 Entra 를 federate 한다 (NextAuth Azure AD provider,
src/lib/auth.ts). 신규 IdP 를 세우는 게 아니라 기존 세션 위에 토큰 발행만 얹는다. - Blueprint 는 이미 per-user 권한의 권위자다 —
entityScopes·EntityRole·getEntityRolesForUser를 들고 있고, frame/hive 가/api/internal/entity-roles로 이미 그걸 물어본다. 토큰 거버넌스의 자연스러운 자리. - Blueprint 는 control plane (구동 시스템). 서비스가 신뢰할 단일 발행자로 토폴로지상 맞다.
현재 상태 — 검증된 인증 표면 (2026-06-03 실측)
각 서비스가 incoming 토큰을 검증하는 방식. “Blueprint 신뢰” 추가 시 바꿀 지점과 난이도를 함께 표기.
| 서비스 | 스택 | validator (file) | iss-dispatch seam | HS256 경로 | ”Blueprint 신뢰” 난이도 |
|---|---|---|---|---|---|
| frame | Python FastMCP | src/frame/mcp/http_server.py:438 | ✅ _is_microsoft_iss → else HS256 | ✅ FRAME_JWT_SECRET | 낮음 — elif _is_blueprint_iss 한 가지 추가 |
| hive | Python FastMCP (frame fork) | src/hive/mcp/http_server.py:250 | ✅ _is_microsoft_iss → else HS256 | ✅ HIVE_JWT_SECRET | 낮음 — frame 과 1:1 동일 패턴 |
| cortex | Rust axum | src/auth.rs:214 | △ HS256 short-circuit → RS256 (MS hardcoded) | ✅ CORTEX_JWT_SECRET (배선됨) | 중간 — issuer 파라미터화 |
| index | Rust axum | src/auth.rs:214 | ✗ RS256(MS) hardcoded, fallback 없음 | △ INDEX_JWT_SECRET 정의만·미배선 | 중간 — issuer 파라미터화 + HS256 배선 |
| matrix | Rust axum (custom) | src/auth.rs:17 | ✗ HS256 단일, iss 미검증 | ✅ MATRIX_JWT_SECRET 만 | 높음 — async refactor + RS256 + JWKS |
| blueprint (자체 MCP) | Python FastMCP | mcp/src/blueprint_mcp/mcp/http_server.py | ✅ iss-dispatch 추가됨 (2026-06-04) — Blueprint vs Microsoft | ✗ HS256 경로 없음 (Microsoft + Blueprint 만) | 낮음 (완료) — frame auth_blueprint.py 미러. 발행자 본인이라 누락됐던 행 |
핵심 발견:
- frame·hive 는 이미 iss 로 verifier 를 고르는 dispatch seam 이 있다 — Blueprint 분기는 한 줄. Phase 1 첫 타깃 = frame (seam +
auth_oidc.pyJWKS 머신 재사용 + 이미 CLI GREEN 검증됨). - Blueprint 측 OP 머신(
SignJWT/JWKS endpoint/authorize/token)은 전무 (greenfield). 단jose는 이미 dependency 이고 검증측createRemoteJWKSet패턴이src/lib/mcp-microsoft-rp.ts에 존재. - customers.yaml 의 tenant 는 고객사별로 다름 (realchoice = 별도 Entra tenant). → OIDC-OP 는 per-deployment: 각 고객 Blueprint = 자기 Entra 를 federate 하는 자기 issuer. AXE 자체 issuer =
https://blueprint.axellc.com.
목표 아키텍처
┌─────────────────────── 사람 SSO 1회 ───────────────────────┐
│ ▼
axe login (CLI) ┌──────────────────────┐
loopback PKCE │ Microsoft Entra ID │
│ │ (customer tenant) │
│ 1. open browser /oauth/authorize └──────────┬───────────┘
▼ │ NextAuth (기존)
┌───────────────────────── Blueprint = OIDC Provider ─────────────┴──┐
│ GET /.well-known/openid-configuration (discovery) │
│ GET /.well-known/jwks.json (공개키 — 서비스가 fetch) │
│ GET /oauth/authorize getServerSession 재사용 → code (PKCE) │
│ POST /oauth/token code+verifier → 플랫폼 JWT(RS256)+refresh │
│ POST /oauth/register DCR (loopback + claude.ai allowlist) │
│ POST /oauth/revoke refresh 취소 │
│ 거버넌스: scope · EntityRole · 발행 audit · per-user/tenant │
└───────────────────────────────┬───────────────────────────────────┘
2. 플랫폼 토큰 (keychain) │ iss=blueprint, aud=platform, RS256
▼ ▼ (서비스는 Blueprint JWKS 로 검증)
┌──────────┐ Bearer ┌────────┬────────┬────────┬────────┬────────┐
│ axe CLI │ ───────► │ frame │ hive │ index │ cortex │ matrix │
└──────────┘ 하나의 └────────┴────────┴────────┴────────┴────────┘
토큰으로 전 서비스 iss=blueprint 분기 → Blueprint JWKS RS256 검증원칙:
- Entra federation 은 재사용 —
/oauth/authorize가getServerSession(authOptions)로 기존 NextAuth 세션을 확인. 세션 있으면(=이미 Entra SSO 됨) code 발행, 없으면/login으로 bounce 후 복귀. Blueprint 가 Entra 와 직접 서버-투-서버 code 교환을 다시 짤 필요 없음 (frameoauth.py의/oauth/callback단계가 Blueprint 에선 불필요 — 이게 frame 대비 단순화). - 하나의 토큰, 전 서비스 —
aud= 플랫폼 audience 단일값. 서비스별 audience juggling 없음. 서비스 권한은 scope 가 가른다. - 서비스는 서명만 신뢰 — 각 서비스는
iss=blueprint분기에서 Blueprint JWKS 로 RS256 검증. Phase 1 에선 인증만 Blueprint 로 (email vouch); 인가(email→entity) 는 각 서비스 customers.yaml 그대로 → 비파괴. (entity grant 를 토큰에 심는 인가 중앙화는 Phase 3.)
OIDC-OP 엔드포인트 (Blueprint Next.js 에 신설)
모두 https://blueprint.axellc.com (= BLUEPRINT_OIDC_ISSUER, 기본 NEXTAUTH_URL) origin 에 App Router route handler 로 추가. 전부 공개(인증 면제) — 단 authorize 는 세션을 요구.
| 엔드포인트 | 메서드 | 신규 파일 (제안) | 역할 |
|---|---|---|---|
/.well-known/openid-configuration | GET | src/app/.well-known/openid-configuration/route.ts | RFC 8414 metadata (issuer, endpoints, jwks_uri, S256, RS256) |
/.well-known/jwks.json | GET | src/app/.well-known/jwks.json/route.ts | 공개키 (서비스·CLI 가 RS256 검증) |
/oauth/authorize | GET | src/app/oauth/authorize/route.ts | 세션 확인(getServerSession) → 단발 code 발행 (PKCE S256 필수) |
/oauth/token | POST | src/app/oauth/token/route.ts | authorization_code + refresh_token grant → 플랫폼 JWT |
/oauth/register | POST | src/app/oauth/register/route.ts | RFC 7591 DCR (loopback + claude.ai redirect allowlist) |
/oauth/revoke | POST | src/app/oauth/revoke/route.ts | refresh token 취소 (governance) |
/oauth/device_authorization | POST | (Phase 3) | RFC 8628 device-code (headless) |
frame oauth.py 가 검증된 레퍼런스다 (PKCE 단발 code·atomic 단일소비·S256 constant-time·DCR allowlist 전부 구현). Blueprint OP 는 그것을 그대로 따르되 차이점 3 가지:
- HS256 대신 RS256 서명 + JWKS 공개 (서비스가 secret 공유 없이 검증).
- Microsoft 서버-투-서버 교환 단계 제거 → 기존 NextAuth 세션 재사용.
- refresh token 추가 (CLI UX “로그인 1회 후 한동안 유지” + 취소 지점).
플랫폼 JWT (access token) shape
RS256, kid 헤더. 클레임:
{
"iss": "https://blueprint.axellc.com", // BLUEPRINT_OIDC_ISSUER (per-deployment)
"sub": "<Entra oid>", // User.entraOid — 안정적 cross-app 식별자
"email": "[email protected]", // 소문자 정규화
"aud": "https://axe.axelabs.ai", // 플랫폼 audience (단일; 전 서비스 공통)
"scope": "openid profile email frame hive index cortex matrix",
"azp": "axe-cli", // 토큰을 받은 client_id (감사)
"jti": "<uuid>", // 감사 + (선택) 취소 denylist
"iat": 1730000000,
"exp": 1730003600, // 1h
"ent": { "axec": ["read","write"], "axev": ["read"] } // Phase 3 (선택) — 현재는 서비스가 무시
}- access token: 짧음 (1h). 만료 시 refresh 로 무중단 갱신.
- refresh token: opaque 랜덤 (JWT 아님), 서버측 저장(Prisma), 회전(rotating — 사용 시 새 발급+구 폐기), 30d. → 취소·도난탐지 지점.
- scope: Phase 1 = 서비스 단위 grant (존재 = 그 서비스 접근). frame 의 entity별 read/write 인가는 customers.yaml email→entity 가 그대로 담당. (
frame:read같은 fine scope +ent클레임 인가중앙화 = Phase 3.) - aud 단일값의 트레이드오프: 한 토큰이 전 서비스 → 토큰 유출 시 blast radius 가 플랫폼 전체. 완화 = 짧은 exp(1h) + scope + refresh 취소 + loopback-only public client + PKCE 필수. (per-service aud 격리보다 편의를 택한 D-axe-idp-1 의 의도적 선택.)
서비스별 신뢰 이전 (trust migration)
각 서비스는 feature flag 로 Blueprint 신뢰를 켠다 — BLUEPRINT_ISSUER + BLUEPRINT_JWKS_URL (+ BLUEPRINT_AUDIENCE) unset = 현행 동작 그대로 (비파괴·롤백 = env 제거).
frame · hive (Python — 낮음, Phase 1·2)
http_server.py 의 dispatch 에 분기 한 줄 + verifier 모듈 신설:
# http_server.py — 기존 if _is_microsoft_iss(iss): ... else: (HS256) 사이에 삽입
elif _is_blueprint_iss(iss): # iss == BLUEPRINT_ISSUER
payload = await verify_blueprint_token( # 신규 auth_blueprint.py
agent_token, jwks_url=BLUEPRINT_JWKS_URL,
issuer=BLUEPRINT_ISSUER, audience=BLUEPRINT_AUDIENCE,
)
email = extract_email(payload)
agent_claims = resolve_subject_to_claims(payload, email, customer_id=cid) # 기존 재사용verify_blueprint_token = auth_oidc.py 의 JWKS fetch/cache(createRemoteJWKSet 대응, 1h TTL·kid-miss 강제갱신)를 그대로 미러, issuer/aud/exp 검증 후 payload 반환. email→entity 는 기존 resolve_subject_to_claims 무수정 재사용 → 인가 모델 불변.
cortex · index (Rust — 중간, Phase 2)
auth.rs:verify_token 의 단일 MS issuer hardcode 를 issuer-dispatch 로 리팩터:
let iss = peek_iss(token)?; // unverified payload iss
match trusted_issuer(&iss, &state.settings) {
Issuer::Microsoft => verify_rs256(token, ms_jwks, ms_iss, ms_aud).await,
Issuer::Blueprint => verify_rs256(token, bp_jwks, bp_iss, bp_aud).await, // 신규
Issuer::Self_ => verify_hs256(token, secret), // cortex 기존 / index 는 여기서 배선
_ => Err(Unknown),
}verify_rs256(token, jwks, iss, aud) 헬퍼로 추출하면 MS·Blueprint 가 같은 코드 재사용. index 는 이참에 INDEX_JWT_SECRET HS256 도 배선(cortex oauth_as.rs 패턴 복사).
matrix (Rust custom — 높음, Phase 2 후)
HS256 단일·동기 미들웨어 → ① async 화 ② iss peek ③ Blueprint RS256 분기 ④ JWKS fetch(reqwest 이미 보유) ⑤ Claims 에 iss/aud 필드 추가. ~80–120 LOC. seam 없음 → 마지막.
| 서비스 | flag env | 작업량 | Phase |
|---|---|---|---|
| frame | BLUEPRINT_ISSUER,BLUEPRINT_JWKS_URL,BLUEPRINT_AUDIENCE | ~80 LOC + tests | 1 (모델 증명) |
| hive | 동일 | ~60 LOC (frame 복사) | 2 |
| cortex | BLUEPRINT_* (settings) | refactor + ~80 LOC | 2 |
| index | BLUEPRINT_* + INDEX_JWT_SECRET 배선 | refactor + ~100 LOC | 2 |
| matrix | MATRIX_BLUEPRINT_* | async refactor ~120 LOC | 2 |
| blueprint (자체 MCP) | BLUEPRINT_ISSUER,BLUEPRINT_AUDIENCE (JWKS 파생) | ~150 LOC + 10 tests (frame 미러) | 완료 (2026-06-04) |
위 표가 frame·hive·cortex·index·matrix 만 세고 Blueprint 자체 MCP 를 빠뜨린 것이 2026-06-04 의 401 버그 근원이었다 — issuer 가 곧 resource server 라는 사실이 자명해 보여 목록에서 누락됐다. 새 OP 를 세울 땐 그 OP 의 MCP 도 이 표의 한 행이다.
axe login — loopback PKCE (CLI)
AXE CLI 에 axe login (인자 없음) 추가. stdlib 만 (http.server·webbrowser·hashlib·secrets·urllib):
1. CLI: 랜덤 loopback 서버 기동 http://127.0.0.1:<port>/callback
2. CLI: POST /oauth/register {redirect_uris:[loopback]} → client_id (또는 정적 axe-cli)
3. CLI: code_verifier 생성, challenge=S256, state 생성 → 브라우저 open
/oauth/authorize?response_type=code&client_id=…&redirect_uri=loopback
&code_challenge=…&code_challenge_method=S256&scope=…&state=…
4. 브라우저: (NextAuth 세션 없으면) Entra SSO 1회 → Blueprint 가 loopback 으로 302 ?code=…&state=…
5. CLI(loopback): code 수신, state 검증 → POST /oauth/token
grant_type=authorization_code&code=…&code_verifier=…&redirect_uri=loopback
6. CLI: {access_token, refresh_token, expires_in} → keychain 저장. 브라우저엔 "닫아도 됨".- 헤드리스(Codex/CI):
axe login --token <T>/AXE_TOKENenv 공존 (gh 모델) — 변경 없음. 또는 Phase 3 device-code. - 검증은 사람 브라우저 로그인 필요 → 단독 e2e 불가, 운영자 SSO 1회로 마무리.
- 토큰 자동 refresh: access 만료 시 CLI 가 저장된 refresh 로
/oauth/token(grant_type=refresh_token) 조용히 갱신.
거버넌스 (Blueprint 중앙)
- scope:
openid profile email+ 서비스 grant. 발급 시 Blueprint 가 user 의entityScopes/EntityRole로 허용 scope 를 결정 (consent UI 는 first-party CLI 라 Phase 1 생략 가능, 외부 third-party client 도입 시 추가). - revoke: refresh token 서버측 저장 →
/oauth/revoke또는 admin UI 에서 폐기. access 는 짧아(1h) revoke latency = exp. 더 강한 즉시취소 필요 시jtidenylist (introspection 없이). - audit: 모든 발행(누가·언제·client·scope·jti)을 append-only 로그. Blueprint 의 기존 로깅/DB 재사용.
- per-tenant: issuer 가 per-deployment 이므로 고객사 토큰은 그 고객 Blueprint 가 발행·관리 (sovereignty 정합).
키 관리
- RS256 keypair. private key = vault (
blueprint/axe/oidc-signing-key, PKCS8 PEM 또는 JWK) →BLUEPRINT_OIDC_PRIVATE_KEY로 주입. 공개키만/.well-known/jwks.json에kid와 함께. - 회전: JWKS 에 2 키(old+new) 동시 게시 → 신규 서명은 new
kid, 검증은 둘 다 수용 → old 만료 후 제거. 서비스 JWKS 캐시 TTL(1h) 고려. jose(generateKeyPair/importPKCS8/SignJWT/exportJWK) 사용 — 이미 dependency.
데이터 모델 (Prisma 신설)
model OAuthAuthCode { // authorize→token 단발 code (frame oauth_authorization_codes 대응)
code String @id
clientId String
userId String
codeChallenge String
redirectUri String
scope String
expiresAt DateTime
consumedAt DateTime?
}
model OAuthRefreshToken { // refresh + revoke + 회전
id String @id @default(cuid())
tokenHash String @unique // 평문 저장 안 함
userId String
clientId String
scope String
expiresAt DateTime
revokedAt DateTime?
rotatedTo String? // 회전 추적 (도난 재사용 탐지)
createdAt DateTime @default(now())
}
model OAuthClient { // 클라이언트 레지스트리 (axe-cli 정적 seed + DCR)
clientId String @id
name String
redirectUris String // JSON 배열
type String @default("public") // PKCE-only
createdAt DateTime @default(now())
}비파괴 cutover (신·구 병행)
보안 핵심 → 점진·가역. 어느 단계도 기존 claude.ai MS-OAuth 흐름과 frame/cortex 프록시를 깨지 않는다.
- Blueprint OP 추가 = 전부 신규 route. 기존 동작 0 변경.
- 서비스별 Blueprint 신뢰 추가 = iss 분기 1개 ADD. MS·HS256 경로 불변. flag(
BLUEPRINT_ISSUER) unset 이면 무시 = 현행. → 서비스 단위로 독립 ship·롤백. - per-service 프록시 폐기는 나중 — frame/cortex 의 claude.ai DCR 프록시(D-ops-14/15)는 Blueprint OP 가 충분히 증명될 때까지 공존. claude.ai 커넥터 흐름 내내 유지. 통합(claude.ai 를 Blueprint OP 로 이전 + 프록시 제거)은 Phase 2 말~3.
롤백 = 해당 서비스 BLUEPRINT_* env 제거 후 recreate. 토큰 발행 중단 = Blueprint route 비활성.
Phase 계획
- Phase 1 (모델 증명) ✅ 2026-06-04: Blueprint OP(discovery·jwks·authorize·token·register·revoke) + RS256 키(vault) + Prisma 모델 +
axe-cli정적 client +axe loginloopback PKCE + frame Blueprint 신뢰. → 운영자 브라우저 SSO 1회로axe login→axe frame toolsGREEN 증명 완료. - Phase 2 ✅ 2026-06-04: hive·cortex·index·matrix trust 이전 — 5개 서비스 전부 LIVE + 영속(
BLUEPRINT_ISSUERcompose/.env.local 기본값) + e2e(axe <svc> tools). claude.ai → Blueprint OP 이전 + frame/cortex 프록시 폐기는 미평가(잔여). - Phase 3: 인가 중앙화(
ent클레임 + fine scope, 서비스가 customers.yaml 대신 토큰 grant 사용) + 감사 UI + headless device-code(RFC 8628,B-axe-cli-device-code— 미구현) + 키 회전 자동화.
미해결 / 결정 필요
- issuer 도메인: 현재
blueprint.axellc.com(NEXTAUTH_URL). axelabs.ai 도메인 이전 시 issuer 변경 = 발행된 토큰·서비스 신뢰설정 일괄 영향 → 이전 전에 확정하거나auth.axelabs.ai안정 alias 고정. - claude.ai 통합 시점: claude.ai 커넥터를 Blueprint OP 뒤로 옮기면 per-service 프록시 폐기 가능. 단 claude.ai DCR redirect allowlist·Mcp-Session-Id 흐름 재검증 필요 (B-axe-cli 의 public 멀티스텝 403 함정과 연동).
- 인가 중앙화 범위: entity grant 를 토큰에 심는 순간 customers.yaml 의 email→entity 가 Blueprint 로 이동 → 큰 마이그레이션. Phase 1 은 인증만, 인가는 그대로 두는 게 안전.
관련
- 결정: D-axe-idp-1 (본 설계) · D-axe-cli-1 (CLI·토큰모델) · D-ops-14/15 (per-service 프록시 — 통합 대상)
- 현행 인증: /architecture/auth (3 경로 — Blueprint = 4번째 trusted issuer 로 합류)
- 레퍼런스 구현: frame
src/frame/mcp/oauth.py(검증된 OAuth 2.1 AS + PKCE + DCR) - 백로그: B-axe-idp-1