892 lines
51 KiB
JSON
892 lines
51 KiB
JSON
{
|
|
"sources": [
|
|
{
|
|
"id": "AP1_BOUNDARY",
|
|
"title": "AP1 SPA direct의 OAuth·token 책임 경계",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/docs/internal-spa-direct-no-google.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"브라우저의 vanilla JavaScript SPA가 public client spa-public로 Authorization Code + PKCE S256을 수행한다.",
|
|
"브라우저가 Keycloak access token을 Bearer header에 넣어 Spring Resource Server를 직접 호출하며 server session은 없다.",
|
|
"이 패턴의 명시된 선택 이유는 브라우저에서 OAuth와 token 수명주기를 직접 학습하는 데 있다.",
|
|
"대안은 refresh token만 server가 보관하는 AP2, 모든 OAuth token을 server가 보관하는 AP3, 인증을 edge로 옮기는 AP4다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Current branch state reviewed read-only; rationale is scoped to the repository's learning purpose.",
|
|
"source_type": "branch-note",
|
|
"status": "reviewed",
|
|
"path": "docs/internal-spa-direct-no-google.md",
|
|
"heading": "AP1 internal SPA direct: local identity profile",
|
|
"line_start": 3,
|
|
"line_end": 10,
|
|
"claim_ids": [
|
|
"AP1-C1"
|
|
],
|
|
"decision_ids": [
|
|
"AP1-D1"
|
|
],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP1_STORAGE",
|
|
"title": "AP1 token 저장 선택과 수용 비용",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/docs/ap1-token-storage.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"access, refresh, ID token은 JavaScript memory에만 두고 redirect transaction state와 PKCE verifier만 sessionStorage에 둔다.",
|
|
"persistent token 복사본을 reload 뒤 남기지 않는 대신 reload 생존을 포기한다.",
|
|
"memory-only 저장은 실행 중 XSS나 fetch hook이 현재 token 또는 API 권한을 악용하는 것을 막지 못한다.",
|
|
"대안인 localStorage·sessionStorage는 reload 편의 대신 persistent script-readable token surface를 늘리고, HttpOnly cookie는 BFF 또는 edge 패턴으로 책임 경계를 바꾼다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca.",
|
|
"source_type": "branch-note",
|
|
"status": "reviewed",
|
|
"path": "docs/ap1-token-storage.md",
|
|
"heading": "AP1 token storage trade-off",
|
|
"line_start": 3,
|
|
"line_end": 29,
|
|
"claim_ids": [
|
|
"AP1-C2"
|
|
],
|
|
"decision_ids": [
|
|
"AP1-D2"
|
|
],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP1_LOGIN_RUNTIME",
|
|
"title": "AP1 SPA authorization, callback와 browser token data flow",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/frontend/src/app.js",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"SPA UserManager는 spa-public, response_type code, openid profile email scope, callback /callback.html과 in-memory user store를 구성한다.",
|
|
"Login click은 signinRedirect를 호출하고 effective authorization request에는 state, PKCE challenge와 S256 method가 포함된다.",
|
|
"Callback path에 code 또는 error query가 있으면 signinRedirectCallback이 transaction state와 verifier를 사용해 browser에서 token endpoint로 code를 교환한다.",
|
|
"Token response의 access, refresh, ID token은 oidc-client-ts User와 currentUser를 통해 JavaScript memory에 있고 redirect transaction state와 verifier만 sessionStorage를 건넌다.",
|
|
"SPA code의 callback은 /callback.html이지만 local realm은 localhost와 127.0.0.1의 port 8088 wildcard redirect를 허용하며 invalid redirect negative test는 없다.",
|
|
"Callback 완료 뒤 URL query를 root로 지우고 subject, username, expiry와 token owner를 파생한 metadata만 UI에 표시한다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile app.js, token storage and PKCE docs, realm configuration, and E2E. Library-internal serialized schema is not claimed.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "frontend/src/app.js",
|
|
"heading": "UserManager configuration, callback, and renderSession",
|
|
"line_start": 9,
|
|
"line_end": 81,
|
|
"claim_ids": [
|
|
"AP1-C4"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP1_PKCE_DEMO_GAP",
|
|
"title": "AP1 manual PKCE helper와 실제 signin path의 구분",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/frontend/src/pkce.js",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"createPkcePair helper는 32 random bytes를 Base64URL verifier로 만들고 SHA-256 challenge와 S256 method를 반환한다.",
|
|
"이 helper는 UI의 PKCE demo button에서 길이를 보여 주는 수동 예시이고 actual signinRedirect path가 호출하지 않는다.",
|
|
"실제 login PKCE는 pinned oidc-client-ts library가 수행하므로 demo helper의 verifier 길이를 actual token request의 정확한 library output이라고 주장할 수 없다.",
|
|
"E2E는 authorization request의 response_type code, S256 method와 nonempty challenge를 검사하지만 token request verifier 값 자체는 직접 assert하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. This source records an implementation/test evidence boundary.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed-gap",
|
|
"path": "frontend/src/pkce.js",
|
|
"heading": "createPkcePair",
|
|
"line_start": 1,
|
|
"line_end": 25,
|
|
"claim_ids": [
|
|
"AP1-C5"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 85
|
|
},
|
|
{
|
|
"id": "AP1_API_RUNTIME",
|
|
"title": "AP1 browser Bearer input에서 /api/me JSON까지",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/frontend/src/app.js",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Call API click은 currentUser가 없거나 expired이면 network call 없이 login-required UI error를 만들고, 유효하면 absolute http://localhost:8081/api/me에 Bearer access token을 보낸다.",
|
|
"실제 SPA happy path는 frontend nginx의 /api proxy가 아니라 browser에서 Resource Server host port를 직접 호출한다.",
|
|
"Resource Server는 stateless filter chain에서 Bearer JWT를 Nimbus decoder, issuer and timestamp validator, keycloak-pattern-api audience validator와 realm-role converter로 처리한다.",
|
|
"ApiController.currentUser는 verified Jwt를 subject, username, issuer, audience 네 필드 JSON으로 변환한다.",
|
|
"SPA는 HTTP status, Resource Server JSON과 browser-memory token metadata를 한 화면용 wrapper JSON으로 다시 조립한다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile frontend app.js/nginx, backend security/decoder/converter/controller, and E2E.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "frontend/src/app.js",
|
|
"heading": "callProtectedApi",
|
|
"line_start": 83,
|
|
"line_end": 109,
|
|
"claim_ids": [
|
|
"AP1-C6"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP1_ROLE_FAILURE_RUNTIME",
|
|
"title": "AP1 JWT failure와 realm role authorization 경계",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/backend/src/main/java/com/example/keycloakpattern/KeycloakRealmRoleConverter.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"KeycloakRealmRoleConverter는 realm_access.roles의 string values를 ROLE_ prefixed Spring authorities로 바꾸고 claim이 없으면 empty authority list를 반환한다.",
|
|
"/api/me는 authenticated만 요구하므로 valid JWT에 role이 없어도 role converter 결과만으로 거부되지 않으며 admin-role은 /api/admin에서 요구된다.",
|
|
"Committed contracts define missing Bearer, wrong audience와 wrong issuer as 401 and regular-user access to /api/admin as 403.",
|
|
"Invalid signature와 expired JWT는 전용 E2E negative case가 없고 injected MockMvc jwt success는 Nimbus decoder path를 증명하지 않는다.",
|
|
"SPA는 non-2xx 응답에서도 먼저 response.json을 시도하므로 empty or non-JSON 401의 exact failure UX는 고정되지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile converter/security/controller, unit and E2E contracts, and frontend error handling.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed-gap",
|
|
"path": "backend/src/main/java/com/example/keycloakpattern/KeycloakRealmRoleConverter.java",
|
|
"heading": "convert",
|
|
"line_start": 12,
|
|
"line_end": 27,
|
|
"claim_ids": [
|
|
"AP1-C7"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 90
|
|
},
|
|
{
|
|
"id": "AP1_GUARDRAILS",
|
|
"title": "AP1 public client와 Resource Server 가드레일",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/keycloak/import/keycloak-patterns-realm.json",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"spa-public client는 public이고 standard flow만 사용하며 implicit와 direct grant를 끄고 PKCE S256을 강제한다.",
|
|
"access token에는 keycloak-pattern-api audience가 추가된다.",
|
|
"Spring Resource Server는 issuer, timestamp, signature와 audience를 검증하고 realm role을 ROLE_ authority로 변환한다.",
|
|
"access token TTL은 300초이며 refresh rotation과 reuse 0 설정을 사용한다.",
|
|
"self-contained access token은 logout이나 refresh revocation 뒤에도 만료 전까지 유효할 수 있어 짧은 TTL을 수용한다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Facts reconcile realm JSON, JwtDecoderConfig, role converter, and ap1-refresh-logout.md.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "keycloak/import/keycloak-patterns-realm.json",
|
|
"heading": "spa-public client and realm token settings",
|
|
"line_start": 11,
|
|
"line_end": 73,
|
|
"claim_ids": [
|
|
"AP1-C3"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 85
|
|
},
|
|
{
|
|
"id": "AP1_VERIFY",
|
|
"title": "AP1 브라우저 흐름과 token 수명주기 검증 계약",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern1/e2e/pattern1.mjs",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Playwright E2E는 authorization request의 PKCE S256, 보호 API 200, wrong audience와 wrong issuer 401을 검사한다.",
|
|
"E2E는 실행 중 fetch hook이 Bearer token을 관찰할 수 있음을 재현하고 Web Storage에 access token이 남지 않는 것을 확인한다.",
|
|
"refresh token rotation과 이전 refresh token 거부, revocation 뒤 refresh 거부, 이미 발급된 access JWT의 만료 전 유효성을 검사한다.",
|
|
"검증 코드는 존재하지만 이번 문서 조사에서는 파괴적인 volume 초기화를 포함한 verify-pattern1.sh를 실행하지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern1@bb8fd9333d7da1c6424d6e0b039fc1c80c2cf0ca. Test-defined evidence, not a fresh execution result.",
|
|
"source_type": "canonical-project",
|
|
"status": "test-defined",
|
|
"path": "e2e/pattern1.mjs",
|
|
"heading": "AP1 Playwright acceptance contract",
|
|
"line_start": 82,
|
|
"line_end": 202,
|
|
"claim_ids": [
|
|
"AP1-T1"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 80
|
|
},
|
|
{
|
|
"id": "AP2_BOUNDARY",
|
|
"title": "AP2 confidential token mediator의 책임 경계",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/docs/ap2-token-boundary.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"브라우저는 Spring mediator에서 로그인을 시작하고 confidential client인 mediator가 client secret으로 authorization code를 교환한다.",
|
|
"mediator는 access와 refresh token을 OAuth2AuthorizedClientService에 보관한다.",
|
|
"브라우저가 token endpoint를 호출하면 mediator는 현재 access token, token type, 만료 시각만 no-store 응답으로 전달한다.",
|
|
"브라우저는 전달받은 access token을 memory에서 사용해 Resource Server를 직접 Bearer 방식으로 호출하며 refresh token은 받지 않는다.",
|
|
"선택 이유는 브라우저에서 code 교환과 refresh token을 제거하면서 Bearer 중심 API 호출은 유지하는 데 있다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff.",
|
|
"source_type": "branch-note",
|
|
"status": "reviewed",
|
|
"path": "docs/ap2-token-boundary.md",
|
|
"heading": "책임 경계",
|
|
"line_start": 3,
|
|
"line_end": 18,
|
|
"claim_ids": [
|
|
"AP2-C1"
|
|
],
|
|
"decision_ids": [
|
|
"AP2-D1"
|
|
],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP2_IMPLEMENTATION",
|
|
"title": "AP2 access-only handoff의 실제 구현",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"GET /token/access는 Keycloak access token 원문, token type, expires_at을 반환한다.",
|
|
"응답에는 Cache-Control no-store와 Pragma no-cache가 붙고 refresh token 필드는 없다.",
|
|
"반복 호출을 막는 nonce, consume, delete 로직은 구현되어 있지 않다.",
|
|
"따라서 현재 branch를 one-time handoff code 구현이라고 설명할 수 없고 access-only token handoff라고 좁혀야 한다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. This implementation takes precedence over the broader wording in the common trade-off matrix.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed-discrepancy",
|
|
"path": "token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
|
|
"heading": "accessToken",
|
|
"line_start": 28,
|
|
"line_end": 55,
|
|
"claim_ids": [
|
|
"AP2-C2"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP2_LOGIN_FLOW",
|
|
"title": "AP2 browser entry와 Spring oauth2Login code 교환",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/resources/static/app.js",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Login button은 browser를 /oauth2/authorization/keycloak로 이동시키며 Spring Security가 token-mediating-confidential client의 authorization request를 시작한다.",
|
|
"Spring Security는 authorization request와 state를 HttpSession에 저장하고 AP2_SESSION으로 callback transaction을 연결한 뒤 authenticated SecurityContext를 같은 session 경계에 둔다.",
|
|
"Keycloak callback은 /login/oauth2/code/keycloak이고 token endpoint의 client authentication method는 client_secret_basic이다.",
|
|
"Spring oauth2Login이 code를 server-to-server로 교환하고 성공 뒤 root URL로 돌려보낸다.",
|
|
"AP2 client 설정에는 PKCE S256 강제 속성이 없고 E2E도 AP2 authorization request의 challenge를 검사하지 않는다.",
|
|
"AP2_SESSION은 OAuth token 값이 아니라 server login state를 찾는 HttpOnly SameSite=Lax session cookie다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Facts reconcile static app.js, SecurityConfig, application.yml, realm JSON, and pattern2 E2E.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "token-mediator/src/main/resources/static/app.js",
|
|
"heading": "loginButton click and OAuth client registration",
|
|
"line_start": 7,
|
|
"line_end": 9,
|
|
"claim_ids": [
|
|
"AP2-C4"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 90
|
|
},
|
|
{
|
|
"id": "AP2_BOUNDARY_RUNTIME",
|
|
"title": "AP2 token boundary endpoint input과 output",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/java/com/example/keycloakpattern/mediator/TokenBoundaryController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"GET /token/boundary는 AP2_SESSION으로 인증된 principal을 입력으로 받고 registration ID keycloak과 principal name으로 authorized client를 조회한다.",
|
|
"성공 응답은 pattern, principal, accessTokenStored, refreshTokenStored, browserReceivesRefreshToken의 다섯 필드이며 no-store와 no-cache를 사용한다.",
|
|
"Authorized client가 없더라도 endpoint는 token 보관 boolean을 false로 둔 200 상태 진단 응답을 만들며 token 부재 자체를 실패로 강제하지 않는다.",
|
|
"Preferred username을 principal name으로 쓰도록 client provider가 설정되어 local regular-user login의 principal 값은 regular-user로 구성된다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Exact payload shape reconciled with TokenBoundaryControllerTest.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "token-mediator/src/main/java/com/example/keycloakpattern/mediator/TokenBoundaryController.java",
|
|
"heading": "tokenBoundary",
|
|
"line_start": 25,
|
|
"line_end": 42,
|
|
"claim_ids": [
|
|
"AP2-C5"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 95
|
|
},
|
|
{
|
|
"id": "AP2_ACCESS_RUNTIME",
|
|
"title": "AP2 access handoff와 browser direct API의 data transformation",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"GET /token/access는 OAuth2AuthorizeRequest에 registration ID keycloak과 현재 Authentication을 넣고 OAuth2AuthorizedClientManager.authorize를 호출한다.",
|
|
"성공 응답의 정확한 키 집합은 access_token, token_type, expires_at이며 raw Keycloak JWT가 access_token 값으로 browser에 전달된다.",
|
|
"Authorized client 또는 access token이 없으면 controller는 401과 No authorized Keycloak client is available reason을 만든다; 정확한 Spring error body는 별도로 고정되지 않았다.",
|
|
"JavaScript는 access_token을 지역 변수로 읽어 http://localhost:8081/api/me의 Authorization Bearer header로 즉시 변환하며 persistent Web Storage에 쓰지 않는다.",
|
|
"현재 controller는 매 GET마다 현재 access token을 반환하고 nonce, consume flag, delete 또는 replay rejection을 구현하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Facts reconcile AccessTokenController, static app.js, and tests.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "token-mediator/src/main/java/com/example/keycloakpattern/mediator/AccessTokenController.java",
|
|
"heading": "accessToken",
|
|
"line_start": 28,
|
|
"line_end": 54,
|
|
"claim_ids": [
|
|
"AP2-C6"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP2_RESOURCE_RUNTIME",
|
|
"title": "AP2 Resource Server의 JWT input과 /api/me output",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/backend/src/main/java/com/example/keycloakpattern/ApiController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"브라우저는 AP2 UI origin에서 GET /api/me에 Accept application/json과 Authorization Bearer access token을 보낸다.",
|
|
"Resource Server는 stateless로 signature, issuer, timestamp와 keycloak-pattern-api audience를 검증한다.",
|
|
"ApiController.currentUser는 검증된 Jwt를 입력으로 subject, username, issuer, audience 네 필드의 JSON을 반환한다.",
|
|
"AP2 UI origin에는 /api/**의 GET과 OPTIONS 및 Authorization과 Content-Type header만 허용하도록 CORS가 설정된다.",
|
|
"커밋된 E2E는 실제 Keycloak JWT로 status 200, regular-user username과 expected audience를 검사하도록 정의하지만 이번 조사에서 실행하지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Facts reconcile backend controller, security/decoder/validator configuration, and E2E.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "backend/src/main/java/com/example/keycloakpattern/ApiController.java",
|
|
"heading": "currentUser",
|
|
"line_start": 21,
|
|
"line_end": 28,
|
|
"claim_ids": [
|
|
"AP2-C7"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 95
|
|
},
|
|
{
|
|
"id": "AP2_GUARDRAILS",
|
|
"title": "AP2 session, refresh custody, CORS와 audience 가드레일",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/token-mediator/src/main/resources/application.yml",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"AP2_SESSION은 HttpOnly와 SameSite=Lax를 사용하고 실제 OAuth token을 cookie 안에 넣지 않는다.",
|
|
"client_secret_basic confidential client와 environment-provided secret을 사용한다.",
|
|
"downstream API는 AP2 UI origin의 GET과 OPTIONS만 CORS로 허용하고 stateless JWT Resource Server로 동작한다.",
|
|
"access token 노출은 남고 mediator session과 authorized-client 상태가 추가되므로 AP1보다 수평 확장이 복잡하다.",
|
|
"durable shared authorized-client store, logout, refresh 이후 동작은 현재 branch에 구현·검증 근거가 없다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. The scaling cost is an implementation-grounded inference, not a recorded project ADR.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "token-mediator/src/main/resources/application.yml",
|
|
"heading": "AP2 session and OAuth client configuration",
|
|
"line_start": 3,
|
|
"line_end": 34,
|
|
"claim_ids": [
|
|
"AP2-C3"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 85
|
|
},
|
|
{
|
|
"id": "AP2_VERIFY",
|
|
"title": "AP2 access-only 전달과 브라우저 직접 API 호출 검증 계약",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern2/e2e/pattern2.mjs",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Playwright E2E는 server에 access와 refresh token이 있고 browser 응답에는 refresh token이 없음을 검사한다.",
|
|
"access 응답이 정확히 access_token, expires_at, token_type 세 필드이고 no-store인지 검사한다.",
|
|
"access token audience와 직접 Resource Server 호출 200, AP2_SESSION의 HttpOnly와 SameSite=Lax, Web Storage 비사용을 검사한다.",
|
|
"검증 코드는 존재하지만 이번 조사에서는 verify-pattern2.sh를 새로 실행하지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern2@d019846f8725bdb0badde33043b020dc252e32ff. Test-defined evidence, not a fresh execution result.",
|
|
"source_type": "canonical-project",
|
|
"status": "test-defined",
|
|
"path": "e2e/pattern2.mjs",
|
|
"heading": "AP2 Playwright acceptance contract",
|
|
"line_start": 39,
|
|
"line_end": 112,
|
|
"claim_ids": [
|
|
"AP2-T1"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 80
|
|
},
|
|
{
|
|
"id": "AP3_BOUNDARY",
|
|
"title": "AP3 BFF의 tokenless browser 경계",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/docs/ap3-bff-boundary.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"BFF가 confidential client와 PKCE S256으로 authorization code를 교환하고 access와 refresh token을 server에 보관한다.",
|
|
"브라우저에는 OAuth token 대신 HttpOnly AP3_SESSION만 남는다.",
|
|
"브라우저가 BFF API를 cookie로 호출하면 BFF가 Bearer access token을 붙여 내부 Resource Server를 호출한다.",
|
|
"선택 이유는 브라우저에서 OAuth token을 제거하고 application authorization과 session을 중앙화하는 데 있다.",
|
|
"대안 AP1은 stateless와 protocol transparency를 얻고, AP2는 access token 직접 전달을 유지하며, AP4는 edge에서 기존 upstream을 보호한다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677.",
|
|
"source_type": "branch-note",
|
|
"status": "reviewed",
|
|
"path": "docs/ap3-bff-boundary.md",
|
|
"heading": "요청과 token 경계",
|
|
"line_start": 3,
|
|
"line_end": 18,
|
|
"claim_ids": [
|
|
"AP3-C1"
|
|
],
|
|
"decision_ids": [
|
|
"AP3-D1"
|
|
],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP3_TRADEOFF",
|
|
"title": "AP3와 AP1의 위협 모델·운영비 교환",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/docs/bff-vs-spa-direct.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"BFF는 browser JavaScript가 token을 읽지 못하게 하지만 XSS가 same-origin request를 악용하는 것까지 없애지는 않는다.",
|
|
"cookie session으로 바뀌므로 CSRF 방어가 필요하고 backend session store가 필요하다.",
|
|
"학습 구성은 session과 authorized client를 단일 instance memory에 두므로 재시작 시 session이 사라진다.",
|
|
"scale-out에는 sticky session 또는 Spring Session과 Redis 같은 shared store, 저장 token 암호화 정책이 필요하다.",
|
|
"BFF 선택은 절대적인 보안 등급이 아니라 browser token 비노출과 stateful 운영비의 교환이다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677.",
|
|
"source_type": "branch-note",
|
|
"status": "reviewed",
|
|
"path": "docs/bff-vs-spa-direct.md",
|
|
"heading": "BFF vs SPA direct",
|
|
"line_start": 3,
|
|
"line_end": 22,
|
|
"claim_ids": [
|
|
"AP3-C2"
|
|
],
|
|
"decision_ids": [
|
|
"AP3-D2"
|
|
],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP3_GUARDRAILS",
|
|
"title": "AP3 CSRF token과 SameSite 가드레일",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Spring CookieCsrfTokenRepository가 JavaScript-readable XSRF-TOKEN을 발급하고 client는 X-XSRF-TOKEN header를 보낸다.",
|
|
"AP3_SESSION은 HttpOnly와 SameSite=Lax다.",
|
|
"SameSite는 CSRF token의 대체가 아니라 defense-in-depth다.",
|
|
"BFF는 access와 refresh token 값을 browser 응답에 넣지 않고 BFF가 downstream Bearer header를 만든다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Facts reconcile SecurityConfig, application.yml, and BffController.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
|
|
"heading": "bffSecurity",
|
|
"line_start": 25,
|
|
"line_end": 58,
|
|
"claim_ids": [
|
|
"AP3-C3"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 85
|
|
},
|
|
{
|
|
"id": "AP3_LOGIN_FLOW",
|
|
"title": "AP3 oauth2Login과 server-side PKCE data flow",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Login button은 /oauth2/authorization/keycloak로 이동하고 SecurityConfig의 DefaultOAuth2AuthorizationRequestResolver가 withPkce customizer로 state, verifier와 S256 challenge를 준비한다.",
|
|
"Authorization request는 bff-confidential, response_type code, callback /login/oauth2/code/keycloak, openid profile email scope와 PKCE S256 challenge를 사용한다.",
|
|
"Callback 뒤 BFF가 client_secret_basic, authorization code와 verifier로 server-to-server token 교환을 수행하고 access와 refresh token은 authorized-client service에 저장하며 ID token에서 구성된 OIDC principal은 HttpSession SecurityContext에 연결한다.",
|
|
"브라우저에는 OAuth token 대신 HttpOnly SameSite=Lax AP3_SESSION이 남고 성공 뒤 root URL로 이동한다.",
|
|
"현재 application에는 Spring Session, Redis, JDBC authorized-client store 의존성이 없어 session과 token state는 single-process memory 경계다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Framework-mediated steps are reconciled with SecurityConfig, application.yml, realm config, E2E, and Spring defaults.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "bff/src/main/java/com/example/keycloakpattern/bff/SecurityConfig.java",
|
|
"heading": "authorizationRequestResolver and bffSecurity",
|
|
"line_start": 20,
|
|
"line_end": 58,
|
|
"claim_ids": [
|
|
"AP3-C4"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 95
|
|
},
|
|
{
|
|
"id": "AP3_BOUNDARY_RUNTIME",
|
|
"title": "AP3 token boundary endpoint의 input과 관측 output",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"GET /bff/token-boundary는 AP3_SESSION으로 복원된 Authentication을 입력으로 받고 keycloak registration과 principal name으로 authorized client를 직접 조회한다.",
|
|
"정상 응답은 pattern, principal, accessTokenStoredOnServer, refreshTokenStoredOnServer, browserTokenCount, csrfProtectionEnabled 여섯 필드이며 no-store와 no-cache를 사용한다.",
|
|
"browserTokenCount 값 0은 controller의 literal 진단 필드이고 실제 browser를 측정한 값은 아니므로 E2E의 storage와 network 검사가 별도로 필요하다.",
|
|
"Authorized client가 없더라도 인증된 요청이면 server token 보관 boolean이 false인 200 진단 응답을 만들며 access나 refresh token 원문은 직렬화하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
|
|
"heading": "tokenBoundary",
|
|
"line_start": 44,
|
|
"line_end": 64,
|
|
"claim_ids": [
|
|
"AP3-C5"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 95
|
|
},
|
|
{
|
|
"id": "AP3_API_RUNTIME",
|
|
"title": "AP3 session input에서 downstream Bearer와 reader JSON까지",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"GET /bff/api/me는 browser Authorization header 없이 AP3_SESSION으로 들어오며 BffController.currentUser가 현재 Authentication을 받는다.",
|
|
"authorizedClient helper는 OAuth2AuthorizeRequest를 만들고 refresh-token-capable OAuth2AuthorizedClientManager.authorize를 호출해 현재 access token을 얻는다.",
|
|
"BFF RestClient는 internal Resource Server GET /api/me에 server-held access token을 Bearer header로 붙이고 browser session cookie는 전달하지 않는다.",
|
|
"Resource Server는 JWT signature, issuer, timestamp와 keycloak-pattern-api audience를 검증하고 subject, username, issuer, audience JSON을 반환한다.",
|
|
"BFF는 downstream ResponseEntity Map을 반환하지만 downstream 401, timeout과 unavailable을 명시적으로 그대로 매핑하거나 retry하는 계약은 구현하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Facts reconcile BffController, manager bean, backend JWT configuration, and E2E.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
|
|
"heading": "currentUser and authorizedClient",
|
|
"line_start": 67,
|
|
"line_end": 111,
|
|
"claim_ids": [
|
|
"AP3-C6"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP3_CSRF_RUNTIME",
|
|
"title": "AP3 CSRF cookie-to-header transformation과 preferences output",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/CsrfController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"GET /bff/csrf는 authenticated session을 입력으로 받고 headerName, parameterName, token JSON과 JavaScript-readable XSRF-TOKEN cookie를 no-store로 반환한다.",
|
|
"JSON body의 token은 XOR-masked request attribute token이고 XSRF-TOKEN cookie에는 raw token이 있으므로 두 문자열을 동일하다고 설명할 수 없다.",
|
|
"SPA는 JSON의 headerName을 읽고 document.cookie의 raw XSRF-TOKEN 값을 X-XSRF-TOKEN request header에 넣는다.",
|
|
"SpaCsrfTokenRequestHandler는 token attribute 노출에는 XOR handler를 사용하지만 expected header가 있으면 plain resolver로 submitted raw token을 읽는다.",
|
|
"POST /bff/api/preferences는 AP3_SESSION, XSRF-TOKEN cookie, matching X-XSRF-TOKEN header와 form theme를 입력으로 받으며 header가 없거나 틀리면 controller 전에 403이다.",
|
|
"정상 POST는 updated, theme, principal JSON을 반환한다; SameSite=Lax는 별도 방어선이며 CSRF token 검증을 대체하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Facts reconcile CsrfController, SecurityConfig, SpaCsrfTokenRequestHandler, app.js, BffController, and tests.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "bff/src/main/java/com/example/keycloakpattern/bff/CsrfController.java",
|
|
"heading": "csrf",
|
|
"line_start": 14,
|
|
"line_end": 23,
|
|
"claim_ids": [
|
|
"AP3-C7"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP3_PREFERENCE_SCOPE",
|
|
"title": "AP3 preferences 예시의 process-global state 간극",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Preference theme은 singleton controller의 AtomicReference String 한 개에 저장되고 user 또는 session key가 없다.",
|
|
"한 사용자의 update가 process 안의 다른 사용자 조회에도 보일 수 있고 재시작하면 system으로 초기화된다.",
|
|
"AtomicReference는 set과 get 원자성만 제공하며 사용자 격리, 입력 validation, persistence, audit 또는 authorization을 제공하지 않는다.",
|
|
"현재 POST는 arbitrary theme string을 받아 인증된 principal만 응답에 기록하고 role 또는 ownership을 검사하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. This is an implementation-grounded scope warning for the worked example.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed-gap",
|
|
"path": "bff/src/main/java/com/example/keycloakpattern/bff/BffController.java",
|
|
"heading": "preferenceTheme and updatePreferences",
|
|
"line_start": 28,
|
|
"line_end": 95,
|
|
"claim_ids": [
|
|
"AP3-C8"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 90
|
|
},
|
|
{
|
|
"id": "AP3_VERIFY",
|
|
"title": "AP3 browser token 비노출과 CSRF 방어 검증 계약",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern3/e2e/pattern3.mjs",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Playwright E2E는 PKCE S256, server token 보관, browser token count 0, token endpoint와 Resource Server 직접 호출 부재를 검사한다.",
|
|
"AP3_SESSION의 HttpOnly와 SameSite=Lax, 빈 Web Storage를 검사한다.",
|
|
"CSRF token 없는 POST 403, 올바른 header가 있는 POST 200, cross-site POST에서 session cookie 제외를 검사한다.",
|
|
"검증 코드는 존재하지만 이번 조사에서는 verify-pattern3.sh를 새로 실행하지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern3@934c5da5d6edc2429dfb558b773656e46f21d677. Test-defined evidence, not a fresh execution result.",
|
|
"source_type": "canonical-project",
|
|
"status": "test-defined",
|
|
"path": "e2e/pattern3.mjs",
|
|
"heading": "AP3 Playwright acceptance contract",
|
|
"line_start": 43,
|
|
"line_end": 191,
|
|
"claim_ids": [
|
|
"AP3-T1"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 80
|
|
},
|
|
{
|
|
"id": "AP4_BOUNDARY",
|
|
"title": "AP4 oauth2-proxy와 nginx edge 책임 경계",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/docs/ap4-edge-forward-auth.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"oauth2-proxy가 confidential edge-proxy client와 PKCE S256으로 code를 교환하고 browser에는 HttpOnly AP4_SESSION만 남긴다.",
|
|
"nginx auth_request가 oauth2-proxy의 인증 결과를 확인하고 허용된 identity header만 upstream application에 전달한다.",
|
|
"선택 이유는 OAuth와 OIDC를 모르는 기존 upstream을 수정하기 어려울 때 edge에서 인증을 일괄 적용하는 데 있다.",
|
|
"수용 비용은 proxy session 운영과 identity header 신뢰 경계를 네트워크·application 양쪽에서 강제해야 한다는 점이다.",
|
|
"대안은 application-owned session과 authorization을 제공하는 AP3 또는 Traefik ForwardAuth 같은 다른 edge policy point다.",
|
|
"최종 hardened 구현은 upstream에 internal token 검증을 요구하므로 완전한 무수정 통합이 아니라 OAuth 비인지 application에 최소 신뢰경계 통합을 추가하는 형태다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563.",
|
|
"source_type": "branch-note",
|
|
"status": "reviewed",
|
|
"path": "docs/ap4-edge-forward-auth.md",
|
|
"heading": "AP4 oauth2-proxy Edge Forward Auth",
|
|
"line_start": 3,
|
|
"line_end": 70,
|
|
"claim_ids": [
|
|
"AP4-C1"
|
|
],
|
|
"decision_ids": [
|
|
"AP4-D1"
|
|
],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP4_NGINX",
|
|
"title": "AP4 auth_request와 identity header 덮어쓰기",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/frontend/default.conf.template",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"정확 일치 /oauth2/auth location은 internal이고 auth subrequest body를 전달하지 않는다.",
|
|
"일반 browser 요청의 401은 login 302로 바꾸지만 /api/edge는 redirect 없이 JSON 401을 반환한다.",
|
|
"client가 보낸 identity와 internal token header는 사용하지 않고 oauth2-proxy 결과와 server-side internal token으로 덮어쓴다.",
|
|
"backend와 oauth2-proxy port는 host에 publish하지 않고 nginx만 application entry point로 노출한다.",
|
|
"학습용 nginx 예제는 보호 경로를 범용 upstream path로 보존하지 않고 /edge/me로 전달하므로 identity flow fixture이지 완성형 transparent reverse proxy가 아니다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Facts reconcile nginx template and docker-compose.yml.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "frontend/default.conf.template",
|
|
"heading": "nginx AP4 server configuration",
|
|
"line_start": 13,
|
|
"line_end": 74,
|
|
"claim_ids": [
|
|
"AP4-C2"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 90
|
|
},
|
|
{
|
|
"id": "AP4_LOGIN_RUNTIME",
|
|
"title": "AP4 unauthenticated navigation에서 oauth2-proxy session까지",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/docker-compose.yml",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Cookie가 없는 GET /는 nginx auth_request를 통해 internal /oauth2/auth를 조회하고 oauth2-proxy 401을 /oauth2/start redirect로 변환한다.",
|
|
"oauth2-proxy는 edge-proxy confidential client, S256 challenge, browser-facing login URL, server-facing token/JWKS/userinfo URL과 callback /oauth2/callback을 사용한다.",
|
|
"Callback code 교환은 oauth2-proxy와 Keycloak 사이의 server-to-server 요청이고 browser request log에는 token endpoint call이 없어야 한다.",
|
|
"로그인 뒤 browser에는 HttpOnly SameSite=Lax AP4_SESSION이 남으며 local HTTP fixture는 Secure false이고 production HTTPS에서는 secure cookie가 필요하다.",
|
|
"별도 Redis 같은 server-side session store는 없고 session-cookie-minimal은 client-side cookie에 access, refresh, ID token을 보관하지 않으므로 persistent refresh-token custody나 refresh lifecycle은 구현·검증되지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Facts reconcile Compose flags, nginx template, docs/ap4-edge-forward-auth.md, docs/edge-forwardauth-google-federation.md, and E2E.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "docker-compose.yml",
|
|
"heading": "oauth2-proxy service",
|
|
"line_start": 92,
|
|
"line_end": 143,
|
|
"claim_ids": [
|
|
"AP4-C5"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP4_REQUEST_RUNTIME",
|
|
"title": "AP4 external request에서 auth subrequest와 upstream input까지",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/frontend/default.conf.template",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Authenticated GET /api/edge는 먼저 body 없는 internal /oauth2/auth subrequest로 변환되고 original URL, forwarded host, protocol, URI와 client address context가 oauth2-proxy에 전달된다.",
|
|
"Nginx는 oauth2-proxy response의 X-Auth-Request-User, X-Auth-Request-Email과 Set-Cookie를 추출한다.",
|
|
"원래 external /api/edge URL은 upstream GET /edge/me로 다시 매핑되고 client-supplied identity/internal headers는 extracted user/email과 server-side internal token으로 덮어쓴다.",
|
|
"Unauthenticated exact /api/edge는 redirect 없이 401 JSON error authentication required를 반환하지만 general / location은 login 302로 바뀐다.",
|
|
"External /oauth2/auth는 internal location 때문에 접근할 수 없고 current example maps protected routes to one identity endpoint rather than preserving arbitrary upstream paths."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Exact nginx behavior, not a generic forward-auth claim.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "frontend/default.conf.template",
|
|
"heading": "auth_request and upstream mapping",
|
|
"line_start": 13,
|
|
"line_end": 74,
|
|
"claim_ids": [
|
|
"AP4-C6"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP4_RESPONSE_RUNTIME",
|
|
"title": "AP4 trusted header input에서 edge identity JSON까지",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"EdgeIdentityController.currentUser는 X-Auth-Request-User를 읽고 X-Internal-Auth-Token을 configured bytes와 MessageDigest.isEqual로 비교한다.",
|
|
"User header가 blank이거나 internal token이 없거나 틀리면 401과 error trusted edge authentication is required JSON을 반환한다.",
|
|
"정상 응답은 pattern AP4-edge-forward-auth, user, email, identityHeader X-Auth-Request-User 네 필드다.",
|
|
"Spring Security는 /edge/**를 permitAll로 두므로 current internal-token check는 /edge/me controller의 local guard이며 모든 edge endpoint의 centralized filter가 아니다.",
|
|
"현재 response와 test는 user/email identity만 다루고 role, groups, tenant 또는 fine-grained authorization contract를 구현하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Facts reconcile controller, SecurityConfig, unit tests, and E2E.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
|
|
"heading": "currentUser and hasValidInternalToken",
|
|
"line_start": 27,
|
|
"line_end": 53,
|
|
"claim_ids": [
|
|
"AP4-C7"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 100
|
|
},
|
|
{
|
|
"id": "AP4_BACKEND",
|
|
"title": "AP4 upstream의 내부 token 검증",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"upstream endpoint는 X-Auth-Request-User와 X-Internal-Auth-Token이 모두 있어야 identity를 받아들인다.",
|
|
"internal token은 MessageDigest.isEqual로 비교하며 누락되거나 틀리면 401을 반환한다.",
|
|
"shared token은 defense-in-depth이고 production에서는 secret manager 주입·rotation 또는 mTLS와 workload identity가 더 강한 대안이다.",
|
|
"현재 upstream은 user와 email만 소비하며 role header 또는 application authorization 전달은 구현·검증하지 않는다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Secret lifecycle guidance is from docs/ap4-edge-forward-auth.md lines 67-70.",
|
|
"source_type": "canonical-project",
|
|
"status": "reviewed",
|
|
"path": "backend/src/main/java/com/example/keycloakpattern/EdgeIdentityController.java",
|
|
"heading": "currentUser and hasValidInternalToken",
|
|
"line_start": 18,
|
|
"line_end": 53,
|
|
"claim_ids": [
|
|
"AP4-C3"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 85
|
|
},
|
|
{
|
|
"id": "AP4_VERIFY",
|
|
"title": "AP4 edge login과 header spoofing 방어 검증 계약",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/e2e/pattern4.mjs",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Playwright E2E는 unauthenticated 302, PKCE S256, server-to-server token 교환, HttpOnly SameSite=Lax AP4_SESSION을 검사한다.",
|
|
"공격자가 identity와 internal token header를 보내도 nginx가 덮어써 authenticated user가 바뀌지 않는지 검사한다.",
|
|
"외부 /oauth2/auth 접근은 404, API unauthenticated 요청은 redirect 없는 401, oauth2-proxy와 backend host port는 접근 불가인지 검사한다.",
|
|
"검증 코드는 존재하지만 이번 조사에서는 volume을 삭제하는 verify-pattern4.sh를 새로 실행하지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563. Test-defined evidence, not a fresh execution result.",
|
|
"source_type": "canonical-project",
|
|
"status": "test-defined",
|
|
"path": "e2e/pattern4.mjs",
|
|
"heading": "AP4 Playwright acceptance contract",
|
|
"line_start": 44,
|
|
"line_end": 131,
|
|
"claim_ids": [
|
|
"AP4-T1"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 80
|
|
},
|
|
{
|
|
"id": "AP4_ALTERNATIVE",
|
|
"title": "AP4 Traefik ForwardAuth 대안과 추가 비용",
|
|
"url": "repo://keycloak-pattern/develop-keycloak-pattern4/docs/traefik-forwardauth-alternative.md",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"Traefik forwardAuth도 oauth2-proxy /oauth2/auth를 policy point로 사용할 수 있지만 OIDC client나 session manager 자체는 아니다.",
|
|
"trustForwardHeader=false와 허용 identity header 복사가 필요하다.",
|
|
"nginx의 error_page와 같은 login redirect UX는 별도 middleware 또는 oauth2-proxy profile을 설계해야 한다.",
|
|
"repository baseline은 학습 가시성이 높은 nginx 조합을 유지하고 Traefik은 configuration-load 수준의 대안으로만 검증한다.",
|
|
"Traefik 예제는 hardened backend가 요구하는 X-Internal-Auth-Token을 주입하지 않아 현재 /edge/me를 그대로 통과하는 drop-in 대안으로 입증되지 않았다."
|
|
],
|
|
"notes": "Git ref develop-keycloak-pattern4@f4aea65dc6255eae07b20ebbe21e02fb6115e563.",
|
|
"source_type": "branch-note",
|
|
"status": "config-tested",
|
|
"path": "docs/traefik-forwardauth-alternative.md",
|
|
"heading": "Traefik ForwardAuth alternative",
|
|
"line_start": 3,
|
|
"line_end": 22,
|
|
"claim_ids": [
|
|
"AP4-C4"
|
|
],
|
|
"decision_ids": [
|
|
"AP4-D2"
|
|
],
|
|
"priority": 85
|
|
},
|
|
{
|
|
"id": "BRANCH_REACHABILITY",
|
|
"title": "네 pattern branch와 39개 feature ref의 도달성",
|
|
"url": "repo://keycloak-pattern/develop/docs/keycloak-branch-manifest.tsv",
|
|
"publisher": "keycloak-pattern Git repository",
|
|
"accessed": "",
|
|
"facts": [
|
|
"manifest에는 common, ap1, ap2, ap3, ap4 target으로 분류된 39개 feature branch가 있다.",
|
|
"읽기 전용 Git 검사에서 origin의 39개 feature ref가 모두 존재하고 선언된 develop 또는 pattern branch tip의 ancestor임을 확인했다.",
|
|
"repository audit script 자체는 현재 로컬에 없는 별도 branch-note inventory 경로를 요구해 이번 환경에서는 완료되지 않았다."
|
|
],
|
|
"notes": "Read-only audit on develop@c07593c47144674b35e1a2fc3f2f7cfdb349f683. Remote refs were accepted because local feature refs are not present. This is internal execution evidence.",
|
|
"source_type": "canonical-project",
|
|
"status": "partially-verified",
|
|
"path": "docs/keycloak-branch-manifest.tsv",
|
|
"heading": "branch target delivery registry",
|
|
"line_start": 1,
|
|
"line_end": 40,
|
|
"claim_ids": [
|
|
"COMMON-T1"
|
|
],
|
|
"decision_ids": [],
|
|
"priority": 70
|
|
}
|
|
]
|
|
}
|