- Published on
Keycloak OAuth2 로그인 무한 리다이렉트 8가지 원인
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
로그인 버튼을 누르면 Keycloak로 갔다가 다시 애플리케이션으로 돌아오고, 다시 Keycloak로 튕기며 끝없이 반복되는 현상은 OAuth2/OIDC에서 가장 흔한 장애 중 하나입니다. 겉으로는 단순한 리다이렉트 루프처럼 보이지만, 실제 원인은 쿠키/세션, 리다이렉트 URI, 프록시 헤더, HTTPS 종단, SameSite 정책, 클러스터 세션 공유 등 다양한 층에 걸쳐 있습니다.
이 글에서는 Keycloak 기반 OAuth2 로그인 무한 리다이렉트의 대표 원인 8가지를 증상과 함께 정리하고, 각 케이스별로 확인 포인트와 해결책을 제공합니다.
참고로 “루프”는 대개 아래 패턴 중 하나로 나타납니다.
GET /oauth2/authorization/keycloak요청이 반복302응답이Keycloak authorize와redirect_uri사이에서 반복- 콜백(
redirect_uri)까지는 오는데 애플리케이션이 “인증 실패”로 판단하고 다시 로그인으로 보냄
진단을 시작하기 전에: 5분 체크리스트
1) 브라우저 네트워크 탭에서 확인할 것
authorize요청의redirect_uri값이 정확한지- 콜백으로 돌아온 뒤 애플리케이션이 다시
authorize로 보내는지 - 쿠키가 설정/전송되는지(특히
Set-Cookie와 다음 요청의Cookie) state/nonce관련 에러가 있는지
2) Keycloak 로그 레벨 올리기
Keycloak(Quarkus 배포 기준)에서 로그를 올려 원인을 좁힙니다.
# 컨테이너 실행 예시
KC_LOG_LEVEL=DEBUG
KC_LOG_CONSOLE_LEVEL=DEBUG
또는 실행 시 옵션:
./kc.sh start --log-level=DEBUG
3) 애플리케이션(Spring Security) 로그
# application.properties
logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.security.oauth2=DEBUG
원인 1) redirect_uri 불일치(클라이언트 설정/환경별 URL 차이)
증상
- Keycloak 로그인 후 콜백으로 “돌아오는 것처럼 보이지만” 다시 로그인으로 이동
- Keycloak 이벤트 로그에
invalid_redirect_uri혹은Client not allowed to redirect류 흔적
왜 루프가 생기나
Keycloak이 콜백을 허용하지 않으면 인증 코드가 애플리케이션에 전달되지 못하거나, 애플리케이션이 기대한 콜백 URL과 달라 토큰 교환이 실패합니다. 그 결과 애플리케이션은 “미인증”으로 판단하고 다시 로그인으로 보냅니다.
해결
- Keycloak Admin Console
Clients에서Valid Redirect URIs를 환경별로 정확히 등록 Web Origins도 함께 점검(특히 SPA)
예시(개발 환경):
http://localhost:8080/login/oauth2/code/keycloakhttp://localhost:3000/*
Spring Security 설정 예시:
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-client
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid
provider:
keycloak:
issuer-uri: https://auth.example.com/realms/my-realm
issuer-uri 와 실제 접근 URL이 어긋나면 2번 원인과 결합해 루프가 심해집니다.
원인 2) 프록시 뒤에서 X-Forwarded-* 처리 미흡(스킴/호스트 오인)
증상
- 외부에서는 HTTPS로 접속하는데, 내부 애플리케이션은 HTTP로 인식
- 콜백 URL이
http://로 생성되어 Keycloak이 거부하거나, 다시 HTTPS로 리다이렉트되며 반복
왜 루프가 생기나
리버스 프록시(Nginx, ALB, Ingress) 뒤에서 앱이 원래 요청의 스킴/호스트를 모르면 redirect_uri 를 잘못 생성합니다. Keycloak은 등록된 URI와 다르다고 보고 거부하거나, 브라우저가 스킴 변경 리다이렉트를 반복합니다.
해결
(1) Spring Boot에서 Forwarded 헤더 처리 활성화
server.forward-headers-strategy=framework
(2) 프록시에서 헤더 전달 확인
Nginx 예시:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Kubernetes Ingress를 쓴다면 Ingress Controller의 use-forwarded-headers 옵션도 함께 확인합니다.
원인 3) Keycloak hostname / proxy 설정 오류(외부 URL과 내부 URL 혼재)
증상
issuer는https://auth.example.com인데 실제 authorize endpoint가 다른 호스트로 리다이렉트- Keycloak이 생성하는 링크가 내부 도메인/포트로 튀어 브라우저에서 다시 외부로 돌아오며 반복
왜 루프가 생기나
Keycloak이 “자신의 외부 공개 주소”를 잘못 알고 있으면, OIDC 메타데이터/authorize URL/로그인 폼 액션이 뒤섞입니다. 특히 프록시 뒤에서 KC_PROXY 또는 hostname 관련 설정이 맞지 않으면 루프가 쉽게 발생합니다.
해결(Quarkus Keycloak 기준)
KC_PROXY=edge
KC_HOSTNAME=auth.example.com
KC_HTTP_ENABLED=true
KC_HOSTNAME_STRICT=true
환경에 따라 KC_HOSTNAME_URL 또는 KC_HOSTNAME_ADMIN_URL 을 분리해 설정하는 것도 고려합니다.
원인 4) SameSite 쿠키 정책으로 세션 쿠키가 누락됨(특히 크로스 사이트)
증상
- Keycloak 로그인 화면은 정상
- 로그인 성공 후 콜백으로 돌아오지만, 애플리케이션 세션이 유지되지 않아 다시 로그인으로 이동
- 브라우저 콘솔/네트워크에서 쿠키가 차단되었다는 힌트
왜 루프가 생기나
OAuth2는 “다른 사이트로 이동했다가 돌아오는 흐름”이므로, 쿠키 SameSite 설정이 보수적이면 세션 쿠키가 콜백 시점에 전송되지 않습니다. 그러면 애플리케이션은 state 검증/세션 복원을 못 하고 실패하며 루프가 납니다.
해결
- HTTPS 환경에서 세션 쿠키를
SameSite=None; Secure로 설정 - Spring Boot 예시:
server.servlet.session.cookie.same-site=none
server.servlet.session.cookie.secure=true
만약 HTTP 개발 환경이라면 SameSite=None 과 Secure 조합이 충돌할 수 있으니, 로컬은 HTTP로 단순화하거나(혹은 로컬도 HTTPS) 환경별로 분기합니다.
원인 5) state/nonce 검증 실패(세션 저장소/도메인/경로 문제)
증상
- Spring Security 로그에
Invalid state parameter또는authorization_request_not_found - Keycloak은 정상적으로
code를 돌려주는데, 애플리케이션이 콜백을 거부하고 다시 로그인
왜 루프가 생기나
state 와 nonce 는 CSRF 및 재전송 방지를 위해 “인증 요청 시점에 세션에 저장”됩니다. 콜백 시점에 동일 세션을 찾지 못하면 검증이 실패합니다. 원인은 다음이 많습니다.
- 로드밸런서 뒤에서 세션 스티키가 없거나, 세션 공유가 안 됨
- 세션 쿠키 도메인/경로가 잘못되어 콜백에 쿠키가 안 붙음
- 앞서 언급한 SameSite 차단
해결
- 단일 인스턴스가 아니라면 세션 공유(예: Redis) 또는 스티키 세션 적용
- 쿠키
Domain/Path를 점검 - 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
spring.data.redis.host=redis
spring.data.redis.port=6379
클러스터에서 “상태 저장”이 깨지면 루프는 거의 필연입니다. 비슷한 무한 반복 문제를 다룬 글로 Argo CD Sync 실패 - OutOfSync 무한 반복 해결도 함께 참고하면, 반복 현상을 구조적으로 끊는 관점에 도움이 됩니다.
원인 6) TLS 종단(SSL termination) 불일치로 issuer/리다이렉트가 흔들림
증상
.well-known/openid-configuration의issuer값과 실제 접근 URL이 다름- 특정 환경에서만(예: 운영 ALB 뒤) 루프
왜 루프가 생기나
OIDC는 issuer 정합성에 민감합니다. 앱이 issuer-uri 로 메타데이터를 가져오고, 토큰의 iss 와 비교합니다. TLS 종단이 프록시에서 일어나는데 내부 통신이 HTTP라서 Keycloak/앱이 서로 다른 URL을 기준으로 동작하면 인증 후 검증 단계에서 실패하고 다시 로그인으로 보냅니다.
해결
- Keycloak 외부 URL을 기준으로
issuer가 일관되게 나오도록KC_HOSTNAME*및 프록시 설정 정리 - Spring의
issuer-uri는 반드시 외부에서 접근 가능한 최종 URL로 - 프록시의
X-Forwarded-Proto누락 여부 재확인(2번과 세트)
원인 7) 클라이언트 타입/플로우 설정 불일치(Confidential vs Public, PKCE)
증상
- SPA인데
client-secret기반으로 설정되어 있거나, 반대로 서버 사이드인데 Public 클라이언트로 설정 - Keycloak에서 로그인 성공 후 토큰 교환 단계에서 실패하고 다시 authorize로 회귀
왜 루프가 생기나
클라이언트 유형이 맞지 않으면 코드 교환(token endpoint)에서 막힙니다. 애플리케이션은 토큰을 못 받으니 “미인증”으로 로그인 재시도 루프가 발생합니다.
해결
- 서버 사이드(Spring Boot 등): 보통
Confidential+client-secret - SPA: 보통
Public+ PKCE
Keycloak 클라이언트 설정에서 다음을 점검합니다.
Standard Flow Enabled가 켜져 있는지- Public 클라이언트면
Client authentication이 꺼져 있는지 - PKCE를 쓰면
S256설정 일치
Spring Security에서 PKCE를 직접 다루는 경우(커스텀)에는 code_verifier 저장/복원도 세션 이슈와 결합될 수 있습니다.
원인 8) 인증 성공 핸들러/보안 설정이 콜백을 다시 보호해서 루프
증상
/login/oauth2/code/keycloak또는 콜백 경로가 인증 필요로 잡혀 있음- 로그인 성공 후 특정 페이지로 가야 하는데, 다시
/oauth2/authorization/keycloak로 이동
왜 루프가 생기나
보안 설정에서 콜백 엔드포인트 또는 정적 리소스, 성공 후 랜딩 URL을 잘못 보호하면, 인증 처리 중간 단계에서 다시 인증을 요구하게 됩니다. 특히 커스텀 AuthenticationSuccessHandler 가 특정 조건에서 sendRedirect("/login") 같은 동작을 하면 루프가 쉽게 생깁니다.
해결: Spring Security 설정 점검
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login/oauth2/**", "/oauth2/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/", true)
);
return http.build();
}
- 콜백 경로(
/login/oauth2/code/...)는 Spring Security가 처리하므로 일반적으로 허용되어야 합니다. - 실패 시
/error가 다시 인증을 요구하면 “에러 페이지 접근 자체가 루프”가 되기도 합니다.
재현/확인에 유용한 로그 포인트
Keycloak 이벤트
Admin Console에서 Events 를 켜고, 다음을 중심으로 봅니다.
- 로그인 성공/실패 이벤트 반복 여부
redirect_uri거부CODE_TO_TOKEN단계 실패 흔적
애플리케이션
OAuth2AuthorizationRequest저장/복원 실패- 세션 ID가 콜백 전후로 바뀌는지
- 응답
Set-Cookie가 브라우저에서 실제로 저장되는지
운영에서 특히 많이 터지는 조합(패턴별 빠른 결론)
패턴 A: HTTPS 프록시 뒤에서만 루프
- 2번(Forwarded 헤더) + 6번(TLS 종단) + 3번(Keycloak hostname) 우선 점검
패턴 B: Chrome에서는 루프, 특정 브라우저/인앱 웹뷰에서 더 심함
- 4번(SameSite) 우선 점검
패턴 C: 멀티 인스턴스에서만 루프
- 5번(state/세션 공유) 우선 점검
클러스터/배포 환경에서 반복 장애를 추적하는 관점은 K8s CrashLoopBackOff 원인 10분 진단법처럼 “반복을 만드는 조건”을 먼저 분리하는 방식이 유효합니다.
마무리: 루프를 끊는 가장 효율적인 순서
- 브라우저 네트워크에서
redirect_uri와 쿠키 흐름을 먼저 확인 - 프록시 환경이면
X-Forwarded-Proto/Host와 KeycloakKC_PROXY/hostname 정합성 점검 state검증 실패 여부를 Spring Security 로그로 확인- 멀티 인스턴스면 세션 공유 또는 스티키 세션 적용
무한 리다이렉트는 “인증이 실패했다”가 아니라 “인증이 성공해도 앱이 성공으로 인정하지 못한다”는 신호인 경우가 많습니다. 위 8가지를 순서대로 체크하면 대부분의 케이스는 30분 내에 원인까지 도달할 수 있습니다.