- Published on
Keycloak OAuth2 로그인 루프(무한 302) 해결 가이드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 로그에는 302만 가득하고, 브라우저는 로그인 화면으로 되돌아가며, 애플리케이션은 끝없이 Authorization Code 플로우를 다시 시작하는 현상. Keycloak 연동에서 흔히 말하는 로그인 루프(무한 302) 는 대부분 “인증은 됐는데 세션이 유지되지 않거나, 콜백이 유효하지 않거나, 토큰 검증 기준(issuer/audience)이 어긋나서 다시 인증을 시도하는” 케이스로 압축됩니다.
이 글은 원인을 증상별로 빠르게 좁히는 체크리스트와, Keycloak/리버스 프록시/애플리케이션 설정을 어디부터 어떻게 고쳐야 하는지에 집중합니다. (Kubernetes/Ingress 환경에서 특히 빈번합니다. 장애 트러블슈팅 루틴은 Kubernetes CrashLoopBackOff 원인별 10분 진단 글의 접근법과 유사하게 “관측 → 가설 → 최소 변경 검증” 순서로 진행하면 시간을 크게 줄일 수 있습니다.)
1) 무한 302의 전형적인 흐름 이해
OIDC Authorization Code Flow 기준으로 정상 흐름은 대략 이렇습니다.
- 앱이 보호된 리소스 접근 감지 → Keycloak
/auth로 302 - 로그인 성공 → Keycloak가 앱의
redirect_uri(콜백 URL)로code를 붙여 302 - 앱이
code로 토큰 교환(/token) → 세션/쿠키 저장 - 이후 요청은 세션/쿠키로 인증 상태 유지
무한 302는 보통 3~4 단계에서 문제가 생깁니다.
- 세션 쿠키가 브라우저에 저장되지 않음 → 매 요청이 “미인증”으로 판단되어 다시 1로
- 콜백 URL/redirect_uri 불일치 → Keycloak가 다시 로그인 또는 에러 후 재시도
- issuer/realm URL이 외부 URL과 불일치 → 토큰 검증 실패로 앱이 재인증
- 프록시가
X-Forwarded-*를 잘못 전달 → Keycloak가 잘못된 URL로 리다이렉트
2) 10분 내 원인 좁히기: 관측 포인트 4가지
2.1 브라우저 Network 탭에서 확인할 것
- 302 체인이
app → keycloak → app/callback → app → keycloak ...형태인지 Set-Cookie가 내려오는지, 내려와도 차단(회색/경고) 되는지- 콜백 요청에
code,state,session_state가 포함되는지 - 리다이렉트 URL이 http/https, host, path가 의도한 값인지
2.2 Keycloak 이벤트/로그에서 확인할 것
Keycloak Admin Console에서:
Events활성화 후LOGIN,CODE_TO_TOKEN관련 이벤트 확인- 실패 이벤트에
invalid_redirect_uri,cookie_not_found,session_not_active같은 힌트가 있는지
컨테이너 로그 레벨을 올려 더 자세히 보려면(Quarkus 기반 Keycloak 17+):
# 예: Docker/K8s 환경 변수
KC_LOG_LEVEL=DEBUG
KC_LOG_CONSOLE_OUTPUT=json
2.3 애플리케이션 로그에서 확인할 것
Spring Security(OAuth2 Client)라면 다음 키워드가 단서가 됩니다.
Invalid state parameterAuthorization request not found in sessionJwt issuer mismatchFailed to validate the token
2.4 프록시/Ingress 레벨에서 확인할 것
X-Forwarded-Proto,X-Forwarded-Host가 올바른지- TLS 종료 지점(ELB/Ingress/Envoy/Nginx)과 앱/Keycloak이 인지하는 스킴이 일치하는지
- (특히 Envoy) 리다이렉트/헤더 조작 여부
EKS에서 Envoy를 쓰는 경우 503 이슈처럼 “프록시가 실제 원인”인 경우가 많습니다. 프록시 관측 루틴은 EKS에서 Envoy 503 UF·URX 원인과 해결 10분 도 함께 참고하면 좋습니다.
3) 원인 1: Keycloak이 외부 URL을 모르고 잘못 리다이렉트
가장 흔한 케이스입니다. Keycloak이 자신이 http://keycloak:8080 같은 내부 주소로 서비스된다고 생각하면, 로그인 후 리다이렉트가 내부 주소/스킴으로 생성되어 브라우저가 쿠키를 못 붙이거나(도메인 불일치), Mixed Content/차단으로 이어져 루프가 발생합니다.
3.1 Keycloak 17+ (Quarkus)에서 프록시 인지 설정
Keycloak이 리버스 프록시 뒤에 있다면 아래를 우선 점검합니다.
# 대표적인 권장 조합(환경에 맞게 조정)
KC_PROXY=edge
KC_HTTP_ENABLED=true
KC_HOSTNAME=auth.example.com
KC_HOSTNAME_STRICT=true
KC_HOSTNAME_STRICT_HTTPS=true
KC_PROXY=edge: 외부에서 TLS 종료(HTTPS)하고 내부는 HTTP로 들어오는 전형적인 Ingress/ALB 패턴에 적합KC_HOSTNAME*: Keycloak이 생성하는 모든 URL(issuer, redirect 등)의 기준을 외부 도메인으로 고정
3.2 Ingress/Nginx에서 X-Forwarded-* 전달
Nginx Ingress를 쓰면 보통 자동이지만, 커스텀 프록시/Envoy/ALB 조합에서는 누락되기도 합니다.
예: Nginx 리버스 프록시 설정
location / {
proxy_pass http://keycloak:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
검증 포인트: 로그인 후 리다이렉트 URL이 반드시 https://auth.example.com/... 형태로 유지되는지 확인하세요.
4) 원인 2: 쿠키가 저장/전송되지 않아 세션이 유지되지 않음
무한 302의 두 번째 왕좌는 쿠키입니다. OIDC 로그인 과정에서 Keycloak과 앱은 여러 쿠키를 사용합니다.
- Keycloak 도메인 쿠키(SSO 세션)
- 앱 도메인 쿠키(세션/STATE 저장)
4.1 SameSite/secure 정책으로 쿠키가 차단되는 경우
최근 브라우저는 기본적으로 쿠키 정책이 엄격합니다.
- HTTPS가 아닌데
Secure쿠키를 쓰면 저장되지 않음 - 크로스사이트 리다이렉트에서
SameSite=Lax/Strict때문에 쿠키가 안 붙는 경우
Keycloak은 보통 올바르게 설정되지만, “외부는 HTTPS인데 내부에서 HTTP로 인식”하는 순간 Secure 판단이 꼬여 쿠키가 기대대로 동작하지 않습니다. 결국 3)에서 말한 프록시/hostname 설정이 쿠키 문제로도 이어집니다.
4.2 Spring Boot/Spring Security에서 state 저장 세션이 날아가는 경우
Spring Security OAuth2 Client는 기본적으로 state를 세션에 저장합니다. 그런데 다음 상황이면 콜백에서 state를 찾지 못해 인증이 실패하고 재시도 루프가 납니다.
- 로드밸런서 뒤에서 세션 스티키가 없고, 서버가 여러 대인데 세션이 메모리 기반
JSESSIONID쿠키가 차단/미전송- 콜백 경로가 프록시에서 다른 호스트/스킴으로 바뀌어 세션 쿠키 스코프가 달라짐
해결 방향:
- 세션을 Redis(Spring Session)로 공유하거나
- 최소한 로그인 플로우 동안은 스티키 세션을 켜거나
- 쿠키 도메인/경로/secure 설정을 외부 기준으로 정렬
Spring Session Redis 예시:
dependencies {
implementation 'org.springframework.session:spring-session-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
spring:
session:
store-type: redis
data:
redis:
host: redis
port: 6379
5) 원인 3: redirect_uri 불일치 (특히 경로/슬래시/포트)
Keycloak은 redirect_uri를 엄격하게 검증합니다. 앱이 요청한 redirect_uri와 Keycloak Client 설정의 Valid Redirect URIs가 맞지 않으면 로그인 후 콜백이 거부되거나, 애매한 재시도로 루프처럼 보일 수 있습니다.
5.1 Keycloak Client 설정 체크
Keycloak Admin Console → Client → Settings:
- Valid Redirect URIs: 예)
https://app.example.com/login/oauth2/code/keycloak - Valid Post Logout Redirect URIs: 로그아웃 루프가 있다면 여기도 점검
- Web Origins: CORS 문제가 섞이면 로그인 후 API 호출에서 다시 리다이렉트가 날 수 있음
와일드카드 *는 편하지만 보안상 지양하고, 최소 범위로 설정하세요.
5.2 Spring Boot 설정 예시
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-client
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid,profile,email
provider:
keycloak:
issuer-uri: https://auth.example.com/realms/myrealm
여기서 issuer-uri가 외부 URL과 반드시 일치해야 합니다(다음 섹션).
6) 원인 4: issuer(발급자) 불일치로 토큰 검증 실패 → 재인증
앱이 토큰을 받긴 받았는데, 검증 단계에서 실패하면 “인증 안 됨”으로 처리되어 다시 OAuth2 로그인으로 튕기면서 302 루프가 발생합니다.
전형적인 메시지:
The Issuer "http://keycloak:8080/realms/..." did not match "https://auth.example.com/realms/..."
6.1 해결: Keycloak의 외부 hostname 고정 + 앱의 issuer-uri 통일
- Keycloak이 발급하는
iss클레임이 외부 URL이 되도록KC_HOSTNAME/프록시 설정 정리 - 앱의
issuer-uri도 동일한 외부 URL로 설정
검증 방법:
# OIDC discovery 문서가 외부 URL 기준으로 내려오는지 확인
curl -s https://auth.example.com/realms/myrealm/.well-known/openid-configuration | jq .issuer
출력이 https://auth.example.com/realms/myrealm 여야 합니다.
7) 원인 5: Ingress/ALB에서 헤더 또는 경로 재작성으로 콜백이 변형됨
예를 들어 /auth로 들어온 요청을 내부에서 /로 rewrite하거나, X-Forwarded-Prefix를 잘못 주면 Keycloak이 콜백 경로를 다르게 계산할 수 있습니다. 특히 “Keycloak을 /keycloak 같은 서브패스로 제공”할 때 자주 터집니다.
7.1 서브패스 제공 시 권장: 가능하면 서브도메인 사용
- 비권장:
https://example.com/keycloak(서브패스) - 권장:
https://auth.example.com(서브도메인)
서브패스는 프록시 rewrite/쿠키 path/issuer URL까지 모두 복잡해져 루프 가능성이 급증합니다.
8) 실전 트러블슈팅 절차(재현 → 최소 변경 → 검증)
아래 순서대로 하면 대부분의 무한 302는 빠르게 끝납니다.
8.1 1단계: 리다이렉트 체인 캡처
# -L로 따라가되, 쿠키/헤더를 보고 싶으면 -v 사용
curl -vkL https://app.example.com/protected \
-o /dev/null
- 중간에
Location:이http://로 떨어지거나 내부 호스트로 바뀌는지 확인
8.2 2단계: OIDC discovery의 issuer 확인
curl -s https://auth.example.com/realms/myrealm/.well-known/openid-configuration | jq -r .issuer
- 내부 주소가 나오면 Keycloak hostname/proxy 설정부터 수정
8.3 3단계: 브라우저에서 쿠키 차단 여부 확인
- Application 탭 → Cookies
Secure,SameSite, Domain/Path 확인- 콜백 직후 앱 세션 쿠키가 생기는지 확인
8.4 4단계: 멀티 인스턴스면 세션 공유/스티키 검토
- 로그인 요청과 콜백 요청이 서로 다른 파드로 가면
state가 사라질 수 있음 - Redis 세션 또는 스티키로 먼저 안정화 후, 근본적으로 stateless(JWT) 구조로 개선
9) 자주 쓰는 “정답 조합” 예시 (K8s + Ingress + Keycloak + Spring)
9.1 Keycloak (환경 변수)
env:
- name: KC_PROXY
value: edge
- name: KC_HOSTNAME
value: auth.example.com
- name: KC_HOSTNAME_STRICT
value: "true"
- name: KC_HOSTNAME_STRICT_HTTPS
value: "true"
- name: KC_HTTP_ENABLED
value: "true"
9.2 Spring Boot (issuer-uri 외부 통일)
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://auth.example.com/realms/myrealm
registration:
keycloak:
client-id: my-client
scope: openid,profile,email
9.3 Ingress/Nginx (TLS 종료 + forwarded headers)
환경마다 다르지만 핵심은 X-Forwarded-Proto: https가 보장되는 것입니다.
10) 마무리: 무한 302는 “URL 기준점”과 “쿠키” 문제다
Keycloak OAuth2/OIDC 로그인 루프는 겉으로는 복잡해 보여도, 대부분 아래 두 축 중 하나로 정리됩니다.
- 외부 URL 기준점이 불일치: Keycloak이 만드는 URL(issuer/redirect)이 외부와 다름
- 세션/쿠키가 유지되지 않음: SameSite/Secure, 도메인/경로, 멀티 인스턴스 세션 문제
가장 먼저 /.well-known/openid-configuration의 issuer가 외부 URL인지 확인하고, 다음으로 브라우저에서 쿠키가 정상 저장/전송되는지 확인하면 “무한 302”의 80%는 10분 안에 끝납니다.
운영 환경이 EKS/Ingress/Envoy처럼 프록시 레이어가 두꺼울수록 원인이 애플리케이션이 아니라 라우팅/헤더/스킴 인지에서 출발하는 경우가 많습니다. 이때는 프록시 관측을 먼저 강화하고(요청 헤더/리다이렉트/쿠키), 최소 변경으로 한 가지씩 고쳐 검증하는 방식이 가장 빠릅니다.