93 lines
5.4 KiB
Markdown
93 lines
5.4 KiB
Markdown
# Keycloak Resource Server 아키텍처
|
|
|
|
## Why
|
|
|
|
auth-server 가 직접 로그인, OAuth2 callback, JWT 발급, issuer / JWK 공개를 모두 맡으면 인증 프로토콜과 비즈니스 사용자 식별이 강하게 섞입니다. 이번 구조는 인증 주체를 Keycloak 으로 옮기고, auth-server 는 검증된 access token 을 받아 내부 비즈니스 로직만 수행하는 Resource Server 로 제한합니다.
|
|
|
|
## What
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Client[Client] -->|login / token request| Keycloak[Keycloak Realm]
|
|
Keycloak -->|broker login| Google[Google IdP]
|
|
Keycloak -->|broker login| GitHub[GitHub IdP]
|
|
Keycloak -->|access token| Client
|
|
Client -->|Authorization Bearer| AuthServer[auth-server]
|
|
AuthServer -->|JWKS fetch one-time + cached| Keycloak
|
|
AuthServer -->|provider KEYCLOAK + sub| DB[(auth.users)]
|
|
```
|
|
|
|
- Keycloak: 로그인, OAuth2 broker, issuer, token 발급, JWKS 공개를 소유합니다.
|
|
- auth-server: Spring Security Resource Server 로 token signature, issuer, expiry 를 검증합니다.
|
|
- application: 검증된 `sub`, `email`, `name` claim 으로 이미 연결된 내부 사용자를 식별합니다.
|
|
- infrastructure: `provider=KEYCLOAK`, `provider_subject=sub` 기준으로 users row 를 조회합니다.
|
|
|
|
## How
|
|
|
|
### 1. ResourceServer 설정
|
|
|
|
[`ResourceServerSecurityConfiguration`](../../../bootstrap/src/main/java/com/project/auth/config/auth/ResourceServerSecurityConfiguration.java) 가 다음을 wiring 합니다.
|
|
|
|
```java
|
|
http
|
|
.csrf(CsrfConfigurer::disable)
|
|
.authorizeHttpRequests(auth -> auth
|
|
.requestMatchers("/actuator/health", "/actuator/health/**",
|
|
"/livez", "/readyz",
|
|
"/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/v1/auth/me").hasRole("user")
|
|
.anyRequest().authenticated())
|
|
.oauth2ResourceServer(oauth2 -> oauth2
|
|
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter))
|
|
.authenticationEntryPoint(securityExceptionHandler)
|
|
.accessDeniedHandler(securityExceptionHandler));
|
|
```
|
|
|
|
핵심 설정:
|
|
- `spring.security.oauth2.resourceserver.jwt.issuer-uri=https://keycloak.dev.example.com/realms/platform`
|
|
- 이 한 줄로 Spring Boot 가 자동으로 `JwtDecoder` Bean 을 만들고, **OIDC discovery endpoint** (`/.well-known/openid-configuration`) 에서 JWKS URI 를 조회합니다.
|
|
- `NimbusJwtDecoder` 가 JWKS 를 가져와 캐시. 기본 캐시 TTL 5 분 (Spring Security 기본). Keycloak 키 회전이 일어나도 5 분 내 자동 반영.
|
|
|
|
### 2. Token 검증 체인
|
|
|
|
| 단계 | 검증 내용 | 실패 시 |
|
|
|---|---|---|
|
|
| 1 | Bearer header 형식 | 401, `WWW-Authenticate: Bearer` |
|
|
| 2 | JWT 서명 (JWKS 공개키 매칭) | 401, `invalid_token` |
|
|
| 3 | `iss` 가 설정된 issuer-uri 와 일치 | 401, `invalid_token` |
|
|
| 4 | `exp` 미만료, `nbf`/`iat` 유효 | 401, `invalid_token` |
|
|
| 5 | `aud` 가 허용 client 와 일치 (선택) | 401 |
|
|
| 6 | `KeycloakJwtAuthenticationConverter` 가 claim → `AuthenticatedUser` 변환 | — |
|
|
| 7 | `hasRole("user")` 권한 검사 | 403, `access_denied` |
|
|
|
|
1~5 는 Spring Security 가 자동, 6~7 은 본 프로젝트 코드.
|
|
|
|
### 3. Claim → Principal 변환
|
|
|
|
[`KeycloakJwtAuthenticationConverter`](../../../bootstrap/src/main/java/com/project/auth/config/auth/security/KeycloakJwtAuthenticationConverter.java) 가 다음 claim 을 프로젝트 전용 principal 로 변환합니다.
|
|
|
|
- `sub` → `AuthenticatedUser.subject`
|
|
- `email` → `AuthenticatedUser.email`
|
|
- `name` 또는 `preferred_username` → `AuthenticatedUser.name`
|
|
- `scope` → `SCOPE_*`
|
|
- `realm_access.roles` → `ROLE_*`
|
|
|
|
자세한 매핑 정책과 sample JWT payload 는 [03-claim-role-design.md](./03-claim-role-design.md).
|
|
|
|
### 4. 비즈니스 흐름
|
|
|
|
`GET /api/v1/auth/me` 는 `@CurrentUser AuthenticatedUser` 를 받아 `LoadKeycloakUserUseCase` 에 최소 claim 만 넘깁니다. 이 use case 는 `(KEYCLOAK, sub)` 로 기존 내부 사용자 id 를 조회하고, 연결된 사용자가 없으면 `AUTH-004` 404 를 반환합니다 — *자동 생성 / 자동 연결은 하지 않습니다.*
|
|
|
|
## Result
|
|
|
|
- auth-server 내부 JWT 발급기, 로컬 RSA key source, Vault Transit signer, 자체 OIDC discovery / JWKS endpoint 를 모두 제거했습니다 ([04-adr-token-ownership-cleanup.md](./04-adr-token-ownership-cleanup.md)).
|
|
- `/api/v1/auth/login`, `/api/v1/auth/oauth2/keycloak/*`, `/oauth2/authorization/*`, `/login/oauth2/code/*` 는 더 이상 auth-server 의 로그인 경로가 아닙니다.
|
|
- 클라이언트는 Keycloak 에서 token 을 받고 auth-server 에는 Bearer token 만 보냅니다.
|
|
- 내부 사용자 검증은 token signature 검증이 아니라 비즈니스 식별 / 조회 문제로 분리됐습니다.
|
|
|
|
## 운영 고려사항
|
|
|
|
- **JWKS 회전**: Keycloak 측 회전 시 5 분 내 ResourceServer 가 새 키를 fetch. 회전 직후 발급된 token 이 캐시 만료 전 도달하면 `invalid_token` 가능 — Keycloak 측 grace period 또는 `NimbusJwtDecoder` cache refresh 정책 조정 가능.
|
|
- **issuer-uri 변경**: prod 와 dev 가 서로 다른 hostname 이라 환경별 overlay 에서 주입. 이 값이 token 의 `iss` 와 한 글자라도 다르면 모든 token 이 거절됨 — 가장 흔한 운영 사고 패턴.
|
|
- **Realm role 의존**: 기본 사용자에 `user` role 이 부여되지 않으면 401 이 아니라 *401 통과 후 403* 으로 떨어짐. 운영자는 Keycloak realm 의 default-roles-platform 설정에 `user` 가 있는지 확인해야 함.
|