Published on

Keycloak OAuth2 로그인 무한 리다이렉트 8가지 원인

Authors

로그인 버튼을 누르면 Keycloak로 갔다가 다시 애플리케이션으로 돌아오고, 다시 Keycloak로 튕기며 끝없이 반복되는 현상은 OAuth2/OIDC에서 가장 흔한 장애 중 하나입니다. 겉으로는 단순한 리다이렉트 루프처럼 보이지만, 실제 원인은 쿠키/세션, 리다이렉트 URI, 프록시 헤더, HTTPS 종단, SameSite 정책, 클러스터 세션 공유 등 다양한 층에 걸쳐 있습니다.

이 글에서는 Keycloak 기반 OAuth2 로그인 무한 리다이렉트의 대표 원인 8가지를 증상과 함께 정리하고, 각 케이스별로 확인 포인트와 해결책을 제공합니다.

참고로 “루프”는 대개 아래 패턴 중 하나로 나타납니다.

  • GET /oauth2/authorization/keycloak 요청이 반복
  • 302 응답이 Keycloak authorizeredirect_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/keycloak
  • http://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 혼재)

증상

  • issuerhttps://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=NoneSecure 조합이 충돌할 수 있으니, 로컬은 HTTP로 단순화하거나(혹은 로컬도 HTTPS) 환경별로 분기합니다.


원인 5) state/nonce 검증 실패(세션 저장소/도메인/경로 문제)

증상

  • Spring Security 로그에 Invalid state parameter 또는 authorization_request_not_found
  • Keycloak은 정상적으로 code 를 돌려주는데, 애플리케이션이 콜백을 거부하고 다시 로그인

왜 루프가 생기나

statenonce 는 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-configurationissuer 값과 실제 접근 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분 진단법처럼 “반복을 만드는 조건”을 먼저 분리하는 방식이 유효합니다.


마무리: 루프를 끊는 가장 효율적인 순서

  1. 브라우저 네트워크에서 redirect_uri 와 쿠키 흐름을 먼저 확인
  2. 프록시 환경이면 X-Forwarded-Proto/Host 와 Keycloak KC_PROXY/hostname 정합성 점검
  3. state 검증 실패 여부를 Spring Security 로그로 확인
  4. 멀티 인스턴스면 세션 공유 또는 스티키 세션 적용

무한 리다이렉트는 “인증이 실패했다”가 아니라 “인증이 성공해도 앱이 성공으로 인정하지 못한다”는 신호인 경우가 많습니다. 위 8가지를 순서대로 체크하면 대부분의 케이스는 30분 내에 원인까지 도달할 수 있습니다.