프로젝트

일반

사용자정보

Actions

버그 #601

진행중
RL HP

[버그] edge.alphea.dev 응답의 Access-Control-Allow-Origin 중복으로 Hub 웹의 모든 Connect RPC 가 브라우저에서 차단됨 (구글 로그인 불가)

버그 #601: [버그] edge.alphea.dev 응답의 Access-Control-Allow-Origin 중복으로 Hub 웹의 모든 Connect RPC 가 브라우저에서 차단됨 (구글 로그인 불가)

Ryan Lee이(가) 약 5시간 전에 추가함.

상태:
진행
우선순위:
긴급
담당자:
시작일:
2026/08/11
완료일:
진척도:

0%

추정시간:

설명

edge.alphea.dev 가 실제 응답에 Access-Control-Allow-Origin 헤더를 두 번 내려보내 브라우저가 응답을 차단합니다. 그 결과 hub.alphea.dev 에서 모든 Connect RPC 호출이 실패하며, 구글 로그인이 콜백 단계에서 끝나지 않습니다. 게이트웨이 측 확인을 요청드립니다.

현상

구글 계정 선택까지는 정상 진행되고 https://hub.alphea.dev/auth/google/callback?state=... 로 정상 복귀하지만, 화면이 "Finishing Google sign-in" 에서 멈추고 아래 메시지가 표시됩니다.

Connect could not be reached. Try again shortly.

재현 환경

  • staging (https://hub.alphea.dev → https://edge.alphea.dev)
  • 확인 일시: 2026-08-11
  • 브라우저: Chrome (데스크톱)
  • Hub Web 커밋: 695784f (main)

브라우저 콘솔 에러 로그

Access to fetch at 'https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin'
from origin 'https://hub.alphea.dev' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values
'https://hub.alphea.dev, https://hub.alphea.dev', but only one is allowed.
Have the server send the header with a valid value.

edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin:1
Failed to load resource: net::ERR_FAILED

값은 동일하지만 헤더가 두 개 존재하며, CORS 규격상 Access-Control-Allow-Origin 은 정확히 하나여야 하므로 브라우저가 응답 전체를 차단합니다.

확인한 내용 — 실제 응답 헤더가 중복입니다

POST /alphea.connect.v1.AuthService/GoogleLogin · Origin: https://hub.alphea.dev

$ curl -s -D - -o /dev/null -X POST \
    "https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin" \
    -H "Origin: https://hub.alphea.dev" \
    -H "Content-Type: application/json" -d '{}' \
  | grep -i "access-control\|vary" | sort | uniq -c

   2 Access-Control-Allow-Origin: https://hub.alphea.dev          ← 중복
   2 Access-Control-Expose-Headers: Grpc-Status, Grpc-Message,
                                    Grpc-Status-Details-Bin,
                                    Connect-Protocol-Version      ← 중복
   2 Vary: Origin                                                 ← 중복

RequestEmailChallenge 도 동일하게 중복됩니다. 즉 구글 로그인만의 문제가 아니라 이메일 OTP 를 포함한 모든 Connect RPC 가 브라우저에서 똑같이 차단됩니다.

preflight 는 정상이라 더 위험합니다

OPTIONS 는 헤더가 하나뿐이라 통과합니다.

$ curl -s -D - -o /dev/null -X OPTIONS \
    "https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin" \
    -H "Origin: https://hub.alphea.dev" \
    -H "Access-Control-Request-Method: POST" \
    -H "Access-Control-Request-Headers: content-type" \
  | grep -i "access-control\|vary" | sort | uniq -c

   1 Access-Control-Allow-Headers: content-type
   1 Access-Control-Allow-Methods: POST
   1 Access-Control-Allow-Origin: https://hub.alphea.dev          ← 여기는 정상 1개
   1 Access-Control-Max-Age: 86400
   1 Vary: Origin, Access-Control-Request-Method, ...

그래서 다음 순서로 진행됩니다.

  1. preflight 통과 → 브라우저가 실제 POST 를 전송
  2. 서버는 요청을 정상 처리 (이 시점에 구글 인가 코드가 소모됨)
  3. 응답은 돌아오지만 브라우저가 ACAO 중복으로 차단 → fetch 가 reject
  4. SDK 가 이를 unavailable 로 매핑 → 화면에 "Connect could not be reached"

인가 코드는 이미 소모된 뒤라 재시도해도 동일하게 실패합니다.

원인 위치 — 두 계층이 각각 CORS 를 붙이고 있습니다

Origin 을 바꿔 가며 어느 계층이 헤더를 붙이는지 분리했습니다.

Origin: https://hub.alphea.dev  →  ACAO 2개,  Vary: Origin 2개
Origin: https://example.com     →  ACAO 0개,  Vary: Origin 2개   ← 양쪽 모두 거부
Origin 헤더 없음                →  ACAO 0개,  Vary: Origin 2개

Vary: Origin 이 어떤 경우에도 항상 2개라는 점이 결정적입니다. 두 계층이 모두 CORS 로직을 실행하고 있고, 양쪽 다 동일한 origin 허용목록을 각자 들고 각자 헤더를 붙이는 중복 구성입니다.

  • 앞단 openresty (응답 헤더 Server: openresty, X-Served-By: edge.alphea.dev) — 설정 위치를 특정하지 못했습니다. Network/인프라 소유로 보입니다.
  • 업스트림 alphea-rpc (alphea-edge-core/alphea-rpc/internal/server/server.go:94, rs/cors) — 응답의 Access-Control-Expose-Headers 값이 이 코드의 ExposedHeaders 목록과 정확히 일치하므로 중복 헤더 중 하나는 여기서 나옵니다.
corsMiddleware := cors.New(cors.Options{
    AllowedOrigins:   []string{"*"},
    AllowedMethods:   []string{"GET", "POST", "OPTIONS"},
    AllowedHeaders:   []string{"*"},
    ExposedHeaders:   []string{"Grpc-Status", "Grpc-Message",
                               "Grpc-Status-Details-Bin", "Connect-Protocol-Version"},
    AllowCredentials: false,
    MaxAge:           86400,
})

다만 코드상 AllowedOrigins 는 "*" 인데 실제 배포본은 example.com 을 거부했으므로, 운영 중인 인스턴스는 환경 설정의 origin 허용목록으로 구동 중입니다. alphea-edge-core 레포 내 hub.alphea.dev 문자열 검색은 0건이었습니다.

제안 — 한쪽만 남기면 됩니다

A안 (권장) alphea-rpc 를 CORS 단일 출처로 두고, openresty 에서 CORS add_header 를 제거합니다. origin 허용목록 로직이 애플리케이션에 이미 있고, Grpc-* expose 헤더처럼 프로토콜을 아는 값은 프록시가 아니라 서버가 관리하는 편이 맞습니다.

B안 openresty 를 단일 출처로 두어야 한다면, 프록시에서 업스트림 헤더를 숨깁니다.

proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header Vary;

함께 요청드립니다 — X-Alphea-Request-Id 노출

Access-Control-Expose-Headers 에 X-Alphea-Request-Id 가 빠져 있습니다. 이 때문에 SDK 가 trace id 를 읽지 못하고 오류 화면에도 표시되지 않아, 장애 발생 시 서버 로그와 대조할 수단이 없습니다. 이번 수정 시 함께 추가해 주시면 좋겠습니다.

수정 후 검증 방법

$ curl -s -D - -o /dev/null -X POST \
    "https://edge.alphea.dev/alphea.connect.v1.AuthService/GoogleLogin" \
    -H "Origin: https://hub.alphea.dev" \
    -H "Content-Type: application/json" -d '{}' \
  | grep -ci "^access-control-allow-origin"

1        ← 1 이 나와야 정상 (현재는 2)

참고 — 정상 확인된 항목

Hub Web 측에는 수정할 부분이 없음을 함께 확인했습니다.

항목 상태
배포 번들의 VITE_CONNECT_BASE_URL 정상 (https://edge.alphea.dev)
배포 번들의 Google client id 정상
Connect 엔드포인트 도달성 정상 (400 invalid_argument 정상 응답)
preflight CORS 정상
실제 응답 CORS 헤더 중복 — 본 이슈
Actions

내보내기 PDF Atom