Published on

Keycloak OAuth2 로그인 무한 리다이렉트 해결법

Authors

서버에 Keycloak을 붙였는데 로그인 → 콜백 → 다시 로그인이 끝없이 반복되는 현상은 생각보다 흔합니다. 브라우저는 계속 302를 따라가고, 애플리케이션은 “세션이 없다/인증이 안 됐다”고 판단해 다시 Keycloak으로 보내며, Keycloak은 정상적으로 토큰을 발급했는데도 최종적으로 사용자는 로그인 화면만 맴돕니다.

이 글은 Keycloak(OIDC) + OAuth2 Authorization Code Flow 기준으로, 무한 리다이렉트의 핵심 원인을 쿠키/세션, 리다이렉트 URI, 프록시/HTTPS, SameSite, 콜백 처리 관점에서 빠르게 좁혀 해결하는 방법을 다룹니다.

> 인프라 레벨에서 408/504처럼 “겉보기엔 앱이 정상인데 요청이 이상하게 흐르는” 문제를 같이 겪는다면, 트래픽 경로 점검 관점은 EKS에서 ALB Ingress 408 Request Timeout 해결 가이드, EKS ALB Ingress 504인데 Pod는 정상일 때도 참고가 됩니다.

증상 패턴: 어디서 루프가 도는지 먼저 고정

무한 리다이렉트는 크게 2가지 패턴으로 나뉩니다.

패턴 A: 앱이 콜백을 처리했는데도 “세션 없음”

흐름 예:

  1. /login → Keycloak /auth로 302
  2. Keycloak 로그인 성공 → redirect_uricode 포함해서 302
  3. /callback?code=...에서 토큰 교환 성공(또는 실패)
  4. 앱이 세션 쿠키를 심었어야 하는데 브라우저에 남지 않음 → 다시 /login

이 경우는 쿠키/세션 저장 실패가 1순위입니다.

패턴 B: Keycloak이 redirect_uri를 계속 바꿈/거부

흐름 예:

  • Keycloak이 redirect_uri mismatch로 에러를 내거나,
  • https로 가야 하는데 http로 내려가며 앱이 다시 https로 올려 보내는 식의 스킴/호스트 불일치로 루프가 생깁니다.

이 경우는 프록시 헤더/외부 URL 인식이 1순위입니다.

1) 가장 흔한 원인: SameSite/secure 쿠키 설정 불일치

최근 브라우저는 크로스사이트 이동(특히 IdP → 앱 콜백)에서 쿠키 정책이 꽤 빡빡합니다.

체크 포인트

  • 앱이 발급하는 세션 쿠키가 SameSite=Lax/None 중 무엇인지
  • HTTPS 환경에서 SameSite=None이면 반드시 Secure가 필요
  • 서브도메인 간 공유가 필요하면 Domain=.example.com 고려

해결 예: Express 세션 쿠키

import express from "express";
import session from "express-session";

const app = express();

app.set("trust proxy", 1); // 프록시 뒤(ingress/alb/nginx)에서 secure 쿠키 쓰려면 중요

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,        // HTTPS 종단이 프록시에 있어도 trust proxy 설정과 함께 사용
      sameSite: "none",    // Keycloak에서 돌아오는 크로스사이트 콜백 고려
      // domain: ".example.com", // 필요할 때만
    },
  })
);

자주 하는 실수

  • sameSite: 'none'인데 secure: false → 브라우저가 쿠키를 저장하지 않음
  • 프록시 뒤인데 trust proxy 미설정 → Express가 HTTPS로 인식 못해 secure 쿠키를 안 심거나 무시

2) 프록시/Ingress 뒤에서 HTTPS 스킴 인식 실패

Keycloak은 토큰/리다이렉트 검증 과정에서 “요청이 어떤 스킴/호스트로 들어왔는지”를 꽤 중요하게 봅니다. 앱도 마찬가지로 redirect_uri를 만들 때 내부 URL을 써버리면 루프가 납니다.

대표 시나리오

  • 외부는 https://app.example.com
  • 내부 컨테이너는 http://app:3000
  • 앱이 redirect_uri=http://app:3000/callback을 만들어 Keycloak에 보냄
  • Keycloak은 등록된 https://app.example.com/callback과 안 맞거나,
  • 콜백이 http로 내려가서 앱이 다시 https로 리다이렉트 → 상태값/쿠키가 꼬이며 루프

해결: 외부 URL을 명시적으로 고정

  • 앱 설정에 PUBLIC_BASE_URL(또는 ISSUER, REDIRECT_URI)을 두고 외부 주소로 고정하세요.

예: Spring Security (application.yml)

server:
  forward-headers-strategy: framework

spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: my-client
            client-secret: ${KEYCLOAK_CLIENT_SECRET}
            scope: openid, profile, email
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
        provider:
          keycloak:
            issuer-uri: "https://keycloak.example.com/realms/myrealm"

Ingress/ALB/Nginx에서 X-Forwarded-Proto: https가 제대로 전달되는지도 확인합니다.

3) Keycloak Client 설정: Valid Redirect URIs / Web Origins

Keycloak Admin Console에서 Client 설정이 부정확하면, 겉으로는 로그인 성공처럼 보이는데 최종적으로 다시 인증으로 돌아가기도 합니다.

필수 점검

  • Valid Redirect URIs: 콜백 URL을 정확히 포함
    • 예: https://app.example.com/* (가능하면 더 좁게)
  • Web Origins: SPA/프론트가 토큰을 다룬다면 정확한 Origin 필요
    • 예: https://app.example.com
  • Root URL / Home URL: 일부 프레임워크가 여기 값을 참조

주의

  • http://localhost로 개발하다가 운영에서 https://app.example.com으로 바꾸면서 누락
  • www 유무, trailing slash 차이
  • Path 기반 라우팅(/app)을 쓰는데 Redirect URI에 반영하지 않음

4) Authorization Code 교환 실패인데 에러를 삼키는 경우

앱이 콜백에서 code를 받아 토큰 엔드포인트로 교환할 때 실패하면 정상이라면 에러가 보여야 합니다. 하지만 일부 미들웨어/필터가 실패를 “다시 로그인”으로 처리해 루프가 됩니다.

토큰 교환 실패 주요 원인

  • client_secret 불일치
  • redirect_uri교환 요청 시점에 최초 요청과 다름(정확히 같아야 함)
  • 서버 시간이 틀어져서 토큰 검증 실패(nbf, exp)

진단 팁

  • 앱 로그에 token endpoint 응답 코드/바디를 남기세요(민감정보 마스킹).
  • Keycloak 이벤트 로그(Realm Settings → Events)에서 LOGIN, CODE_TO_TOKEN 실패를 확인하세요.

Node(예: openid-client)에서 교환 시 redirect_uri 고정 예:

import { Issuer } from "openid-client";

const keycloak = await Issuer.discover(process.env.OIDC_ISSUER);
const client = new keycloak.Client({
  client_id: process.env.OIDC_CLIENT_ID,
  client_secret: process.env.OIDC_CLIENT_SECRET,
});

const redirectUri = "https://app.example.com/callback";

// 콜백 핸들러에서
const params = client.callbackParams(req);
const tokenSet = await client.callback(redirectUri, params, {
  state: req.session.state,
  nonce: req.session.nonce,
});

여기서 redirectUri가 환경마다 바뀌거나, 내부 주소로 계산되면 루프가 시작됩니다.

5) state/nonce 세션이 유지되지 않는 문제(특히 멀티 인스턴스)

OIDC는 CSRF 방지를 위해 state, 재사용 공격 방지를 위해 nonce를 씁니다. 보통 로그인 시작 시 세션에 저장했다가 콜백에서 검증합니다.

루프를 만드는 전형적인 조건

  • 로그인 시작 요청은 인스턴스 A
  • 콜백은 로드밸런서 때문에 인스턴스 B
  • 세션 스토어가 메모리(in-memory)라서 B에는 state가 없음 → 검증 실패 → 다시 로그인

해결책

  • 세션을 Redis 같은 외부 스토어로 공유
  • 또는 LB sticky session(권장도는 낮음)

Express + Redis 예:

import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

app.set("trust proxy", 1);
app.use(
  session({
    store: new RedisStore({ client: redis }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { secure: true, sameSite: "none", httpOnly: true },
  })
);

6) Keycloak 자체의 “외부 URL” 설정(Hostname) 문제

Keycloak을 프록시 뒤에 두면, Keycloak이 생성하는 링크/issuer/redirect가 내부 주소를 기준으로 나오면서 꼬일 수 있습니다.

Keycloak(Quarkus 배포)에서 자주 보는 설정

  • KC_PROXY_HEADERS=xforwarded
  • KC_HOSTNAME=keycloak.example.com
  • KC_HOSTNAME_STRICT=true/false
  • KC_HTTP_ENABLED=false(운영)

환경변수 예:

KC_PROXY_HEADERS=xforwarded
KC_HOSTNAME=keycloak.example.com
KC_HOSTNAME_STRICT=true
KC_HTTP_ENABLED=false

핵심은 Keycloak이 자신을 https://keycloak.example.com 으로 인식하게 만드는 것입니다. 그렇지 않으면 issuer mismatch, redirect 생성 오류가 연쇄적으로 발생합니다.

7) 빠른 트러블슈팅 체크리스트(10분 컷)

아래 순서대로 보면 원인 범위를 빠르게 줄일 수 있습니다.

  1. 브라우저 DevTools → Network에서 302 체인을 펼쳐서
    • 어디서 /login으로 다시 돌아가는지
    • 콜백 요청에 Set-Cookie가 내려오는지, 내려오면 저장되는지 확인
  2. 쿠키 확인
    • SameSite=None이면 Secure 여부
    • 도메인/패스가 맞는지
  3. 앱이 인식하는 외부 스킴/호스트 확인
    • X-Forwarded-Proto, X-Forwarded-Host
    • 프레임워크의 trust proxy/forward header 설정
  4. Keycloak Client 설정
    • Valid Redirect URIs, Web Origins
  5. 토큰 교환 실패 로그
    • invalid_grant, redirect_uri mismatch 여부
  6. 멀티 인스턴스라면 세션 공유(Redis 등)

마무리: 루프는 “인증 실패”가 아니라 “상태 유지 실패”인 경우가 많다

Keycloak 로그인 무한 리다이렉트는 Keycloak 자체 문제라기보다, 대부분 (1) 쿠키가 저장되지 않거나 (2) 외부 URL 인식이 틀리거나 (3) 세션이 인스턴스 간 공유되지 않는 문제로 귀결됩니다.

특히 운영에서 Ingress/ALB/Cloud Load Balancer를 끼우는 순간 https 종단, 프록시 헤더, SameSite 정책이 한 번에 얽히며 증상이 폭발합니다. 위 체크리스트대로 네트워크 체인과 쿠키 저장 여부부터 확인하면, 불필요하게 Keycloak 설정을 뜯어고치지 않고도 빠르게 해결할 수 있습니다.