프로젝트

일반

사용자정보

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 헤더 중복 — 본 이슈

HP Henry Park이(가) 3달 전에 변경 Actions #1

  • 상태을(를) 신규에서 진행(으)로 변경되었습니다.

원인 확정 및 수정 진행 상황입니다. 원인 계층이 이슈 본문과 다릅니다 — 아래 정정을 참고해 주세요.

재현 확인

보고된 절차 그대로 독립 재현했습니다. 실제 POST 응답에 Access-Control-Allow-Origin 2개, Access-Control-Expose-Headers 2개, Vary: Origin 2개. preflight(OPTIONS)는 각각 1개로 정상 통과. 보고 내용과 일치합니다.

정정 — 중복을 만드는 두 계층

이슈 본문은 앞단 openresty + 업스트림 alphea-rpc로 보았으나, 양쪽 팀이 배포본을 각각 읽어 같은 결론에 도달했습니다. 실제로는 둘 다 애플리케이션 계층입니다.

  • 공개 edge gateway의 CORS 핸들러
  • 상위 Foundation 루트 핸들러의 CORS

Network(앞단)에는 CORS를 주입하는 설정이 없습니다. 인프라 담당이 무변경 상태로 독립 확인했습니다 — 프록시 설정 트리에 add_header 등 관련 지시어 0건, edge 호스트는 순수 TLS 리버스 프록시. openresty는 이미 중복된 응답을 앞에서 전달할 뿐입니다.

따라서 제안 B안의 proxy_hide_header는 이 결함에 적용되지 않습니다. 앞단은 애초에 헤더를 붙이지 않으며, 애플리케이션이 소유자인 상태에서 프록시가 상위 헤더를 숨기면 소유자의 헤더가 가려집니다.

확정된 수정 방향 — A안과 같은 취지

애플리케이션이 CORS 단일 소유자가 되고, 내보내기 전에 상위 계층의 CORS 헤더를 제거합니다. 프록시는 건드리지 않습니다.

두 팀이 각각 만든 작업 중 하나를 취소하여 단일 실행 경로로 정리했습니다. Network 쪽은 브랜치/설정/재시작/배포 모두 없이 무변경으로 종료되었습니다.

진행 상태

수정 병합 완료, 파이프라인 통과. 현재 staging 배포 진행 중입니다.

완료 판정 기준

배포 성공만으로 해결로 보지 않습니다. 아래를 모두 확인한 뒤에만 해결 처리합니다.

  1. 실제 POST 응답의 Access-Control-Allow-Origin 1개
  2. preflight 정상
  3. 허용되지 않은 origin은 여전히 거부
  4. Origin 헤더가 없는 요청도 정상
  5. 사람이 실제로 구글 로그인을 끝까지 완료

5번이 핵심입니다. preflight가 통과하는 구조 때문에 서버는 요청을 처리하고 인가 코드를 소모한 뒤 응답만 차단됩니다. 헤더만 보고 판단하면 사용자 관점의 실패를 놓칠 수 있습니다.

함께 요청된 X-Alphea-Request-Id 노출

이번 수정에서 분리합니다. 별도 후속으로 다룹니다.

분리 사유: CORS가 차단하면 브라우저가 응답 자체를 넘기지 않으므로, 이 결함에 한해서는 헤더를 노출해도 SDK가 trace id를 읽을 수 없습니다. 다만 응답이 정상 도달하는 다른 오류에서는 여전히 필요하므로, 그 근거로 별도 진행합니다.

※ 본 코멘트는 Heze가 작성하여 Henry Park 계정으로 등록되었습니다.

Actions

내보내기 PDF Atom