BFF 환경에서 쿠키 프록시로 서드파티 쿠키 문제 해결하기
Next.js + Spring Boot 조합의 BFF(Backend For Frontend) 아키텍처에서 OAuth 로그인을 구현하다 보면 "분명히 백엔드가 쿠키를 내려주는데 브라우저에 저장이 안 된다"는 문제가 발생했습니다. 이 게시글은 문제의 원인과 해결방법을 다룹니다.
1. BFF 아키텍처란?
BFF(Backend For Frontend)는 프론트엔드 전용 중간 서버를 두는 아키텍처 패턴입니다. 클라이언트는 백엔드 API를 직접 호출하지 않고, 항상 BFF 서버를 통해 통신합니다.
BFF의 장점은 다음과 같습니다.
이유 | 설명 |
|---|---|
보안 | 백엔드 URL을 브라우저에 노출하지 않음 |
인증 중앙화 | 토큰 검증 및 갱신을 서버에서 일관되게 처리 |
응답 최적화 | 여러 백엔드 API를 조합하거나 가공하여 클라이언트에 맞는 응답 생성 |
CORS 단순화 | 브라우저-BFF Same-Origin 통신 유지 |
이 구조에서 백엔드는 내부 네트워크에만 존재하며, 브라우저는 오직 BFF 서버와만 통신힙니다.
2. CORS와 쿠키
CORS란?
CORS(Cross-Origin Resource Sharing)는 브라우저의 보안 정책으로 다른 출처(Origin)에서의 요청을 제한합니다.
Origin은 Protocol + Domain + Port의 조합입니다.
# Cross-Origin
https://myapp.com:443
https://api.backend.com:443
# Cross-Origin
http://myapp.com:3000
http://myapp.com:8080
# Cross-Origin
http://myapp.com:3000
https://myapp.com:3000서로 다른 Origin으로 요청을 보내면 CORS 정책이 적용됩니다.
쿠키가 CORS 충돌
쿠키는 기본적으로 Same-Origin 원칙을 따릅니다. 브라우저는 요청을 보낸 서버와 같은 도메인에서 발급된 쿠키만 신뢰합니다.
3. 서드파티 쿠키 문제
퍼스트파티 쿠키 VS 서드파티 쿠기
구분 | 설명 | 예시 |
|---|---|---|
퍼스트파티 쿠키 | 현재 방문중인 도메인에서 발급된 쿠키 |
|
서드파티 쿠키 | 현재 방문 중인 도메인과 다른 도메인에서 발급된 쿠키 |
|
브라우저는 서드파티 쿠키를 점점 더 강하게 차단하고 있습니다.
Chrome: 2024년부터 단계적 서드파티 쿠키 폐지 진행 중
Firefox: ETP(Enhanced Tracking Protection)로 기본 차단
Safari: ITP(Intelligent Tracking Protection)로 기본 차단
왜 BFF를 써도 문제가 생기는가?
BFF 아키텍처를 사용하더라도, 백엔드가 발급한 Set-Cookie 헤더를 브라우저에 그대로 전달하면 문제가 발생합니다.
# 백엔드가 발급한 쿠키 (그대로 전달 시)
Set-Cookie: refreshToken=eyJhbGci...; Domain=api.internal; SameSite=None; Secure; HttpOnly
# 브라우저 입장에서 본 쿠키의 출처 → api.internal
# 현재 브라우저가 접속 중인 도메인 → myapp.com
# ❌ 도메인 불일치 → 서드파티 쿠키 → 저장 거부 또는 전송 거부SameSite
SameSite 속성은 쿠키를 언제 전송할지 결정합니다.
SameSite 값 | 동작 |
|---|---|
| 완전히 동일한 사이트의 요청에만 쿠키 전송 |
| 동일 사이트 + 최상위 탐색(링크 클릭 등) 시 전송 (기본값) |
| 크로스 사이드에서도 전송. 단, |
서드파티 쿠키를 허용하려면 SameSite=None; Secure 를 사용해야 하지만, 이는 HTTPS 환경에서만 동작하며 브라우저의 서드파티 차단 정책에 의해 막힐 수 있습니다.
4. 쿠키 프록시로 퍼스트파티화
백엔드가 발급한 쿠키를 브라우저에 직접 전달하지 않고, BFF 서버가 중간에서 쿠키를 가로채 자신의 도메인으로 재발급합니다.
쿠키 프록시의 핵심 원칙은 다음과 같습니다.
백엔드 쿠키는 브라우저에 직접 전달하지 않는다.
BFF가 백엔드 응답의
Set-Cookie헤더를 읽어 직접 발급한다.발급 시
Domain을 BFF 서버의 도메인으로 교체하거나 제거한다.쿠키의 보안 속성
HttpOnly,Secure,SameSite)을 환경에 맞게 조정한다.
5. 구현
Next.js Middleware의 Server Action을 활용한 구현 방법입니다.
5-1. 쿠키 재작성 auth.ts
Domain 속성을 제거하면 브라우저는 자동으로 현재 응답을 보낸 서버의 도메인, 즉 BFF 서버의 도메인을 쿠키 도메인으로 설정합니다. 이 서드파티 쿠키를 퍼스트파티로 세탁하기 위한 핵심 메커니즘입니다.
/**
* 로컬 개발 환경(HTTP)에서 브라우저가 쿠키를 거부하지 않도록 수정합니다.
*
* 백엔드가 내려준 쿠키에는 Secure, SameSite=None 등이 붙어있을 수 있는데,
* 이는 HTTP 로컬 환경에서 저장이 안 됩니다.
*/
export function rewriteCookieForLocal(cookie: string, isLocal: boolean): string {
if (!isLocal) return cookie;
return cookie
.replace(/;\s*Secure\b/gi, '') // Secure 속성 제거 (HTTP 환경)
.replace(/;\s*SameSite=None/gi, '; SameSite=Lax') // None → Lax로 완화
.replace(/;\s*Domain=[^;]+/gi, ''); // 백엔드 Domain 제거 → BFF 도메인이 자동 적용
}
/**
* 백엔드 응답의 모든 Set-Cookie 헤더를 Next.js 응답으로 복사합니다.
* 이것이 "쿠키 프록시"의 핵심입니다.
*/
export function proxyCookies(
backendResponse: Response,
nextResponse: Response,
isLocal: boolean,
): void {
const cookies = backendResponse.headers.getSetCookie();
cookies.forEach((cookie) => {
const rewritten = rewriteCookieForLocal(cookie, isLocal);
nextResponse.headers.append('set-cookie', rewritten); // BFF 도메인으로 재발급
});
}5-2. OAuth 콜백 프록시 및 인라인 토큰 교환 middleware.ts
OAuth 로그인 흐름에서 백엔드가 refreshToken을 Set-Cookie로 내려주는 경우입니다. 포인트는 미들웨어 내부에서 즉시 accessToken까지 발급하여 최종 목적지로 한 번에 리다이렉트하는 것입니다.
// middleware.ts
async function handleOAuthProxy(request: NextRequest) {
const { pathname, search } = request.nextUrl;
const isLocal = isLocalRequest(request);
const backendResponse = await fetchBackend(`${pathname}${search}`, {
method: 'GET',
bffRequest: request,
redirect: 'manual',
});
if (backendResponse.status >= 200 && backendResponse.status <= 299) {
// 1. 백엔드 응답의 Set-Cookie에서 refreshToken 값을 직접 추출
const setCookies = backendResponse.headers.getSetCookie();
const refreshTokenEntry = setCookies.find((c) => c.startsWith('refreshToken='));
const refreshTokenValue = refreshTokenEntry?.split(';')[0].split('=')[1];
if (!refreshTokenValue) {
return NextResponse.redirect(new URL('/login?error=missing_token', request.url));
}
// 2. refreshToken을 직접 주입해 accessToken 즉시 발급 (브라우저 왕복 없음)
const refreshResponse = await fetchBackend('/api/auth/refresh', {
method: 'POST',
headers: { Cookie: `refreshToken=${refreshTokenValue}` },
signal: AbortSignal.timeout(5000),
});
if (!refreshResponse.ok) {
return NextResponse.redirect(new URL('/login?error=token_exchange_failed', request.url));
}
const body = await refreshResponse.json();
const accessToken = body.data?.accessToken;
// 3. 최종 목적지로 바로 리다이렉트 — 중간 경유지 없음
const rawRedirect = request.cookies.get(REDIRECT_COOKIE_KEY)?.value;
let redirectTo = '/';
if (rawRedirect) {
const decoded = decodeURIComponent(rawRedirect);
if (isValidInternalPath(decoded)) redirectTo = decoded;
}
const res = NextResponse.redirect(new URL(redirectTo, request.url));
// refreshToken 등 백엔드 쿠키 프록시
proxyCookies(backendResponse, res, isLocal);
// accessToken은 미들웨어가 직접 발급 → 확실한 퍼스트파티
res.cookies.set('auth-token', accessToken, {
path: '/',
maxAge: 60 * 60 * 24 * 7,
sameSite: 'lax',
secure: !isLocal,
});
res.cookies.delete(REDIRECT_COOKIE_KEY);
return res;
}
// ...
}
5-3. BFF 헤더로 백엔드에 컨텍스트 전달 backend.ts
백엔드가 Set-Cookie의 Domain을 올바르게 설정할 수 있도록, BFF는 클라이언트의 실제 호스트 정보를 커스텀 헤더로 백엔드에 전달합니다.
function buildBffHeaders(info: BffInfo, extra?: HeadersInit): Record<string, string> {
return {
...extra,
'X-BFF-Secret': info.bffSecret, // BFF 인증 키
'X-BFF-Host': info.host, // 실제 클라이언트 호스트 (예: myapp.com)
'X-BFF-Proto': info.proto, // 실제 프로토콜 (https)
'X-BFF-Port': info.port, // 실제 포트
'X-Forwarded-Host': info.host,
'X-Forwarded-Proto': info.proto,
'X-Forwarded-Port': info.port,
};
}
백엔드(Spring Boot)는 이 헤더를 읽어 Set-Cookie의 Domain을 BFF 도메인(myapp.com)으로 설정하거나, BFF가 Domain을 제거하는 방식을 사용합니다.
6. 로컬 환경에서의 문제
로컬 개발 환경에서는 추가적으로 문제가 발생합니다.
문제점
속성 | 프로덕션 | 로컬(HTTP) | 해결 |
|---|---|---|---|
| HTTPS 필요 | HTTP라서 쿠키 저장 안 됨 | 제거 |
| 크로스 사이트 허용 |
|
|
| 제거 필요 | 제거 필요 | 제거 |
해결 방법
rewriteCookieForLocal 함수가 이를 처리합니다.
export function rewriteCookieForLocal(cookie: string, isLocal: boolean): string {
if (!isLocal) return cookie; // 프로덕션은 그대로
return cookie
.replace(/;\s*Secure\b/gi, '') // Secure 제거
.replace(/;\s*SameSite=None/gi, '; SameSite=Lax') // None → Lax
.replace(/;\s*Domain=[^;]+/gi, ''); // 외부 Domain 제거
}// 로컬 환경 판별
function isLocalRequest(request: NextRequest): boolean {
const { hostname, protocol } = request.nextUrl;
return hostname === 'localhost' || hostname === '127.0.0.1' || protocol === 'http:';
}7. 전체 흐름 정리
OAuth 로그인의 전체 흐름을 정리하면 다음과 같습니다.
정리
서드파티 쿠키 문제는 BFF를 쓰더라도 백엔드 쿠키를 그대로 전달하면 여전히 발생합니다. 핵심 해결 원칙은 다음과 같습니다.
**BFF가 백엔드 쿠키를 직접 브라우저에 전달하지 않고, Set-Cookie 헤더를 가로채어 BFF 자신의 도메인으로 재발급한다.**
구체적으로는:
Domain속성 제거 → 브라우저가 BFF 도메인을 자동 적용 (퍼스트파티화)환경별 속성 조정 → 로컬(HTTP):
Secure제거,SameSite=Lax로 변경인라인 토큰 교환 → OAuth 콜백 핸들러 안에서 refresh까지 완료, 브라우저 왕복 최소화
BFF 컨텍스트 헤더 → 백엔드가 올바른 도메인 컨텍스트를 알 수 있도록
X-BFF-Host등의 헤더 전달
이 패턴을 적용하면 Safari, Firefox, Chrome 등 모든 브라우저의 서드파티 쿠키 차단 정책에도 안전하게 인증 상태를 유지할 수 있습니다.
참고 문서
쿠키 & SameSite
MDN — HTTP cookies HTTP 쿠키의 동작 방식,
Domain,Secure,SameSite,HttpOnly속성 공식 레퍼런스MDN — Set-Cookie
Set-Cookie응답 헤더의 모든 속성 명세web.dev — SameSite cookies explained Google이 정리한 SameSite 속성 동작 방식과 브라우저별 기본값 변경 내역
web.dev — First-party cookie recipes 서드파티 쿠키 없이 퍼스트파티 쿠키만으로 인증을 구현하는 패턴 모음
RFC 6265 — HTTP State Management Mechanism 쿠키의
Domain속성이 생략될 때 브라우저가 어떻게 도메인을 결정하는지에 대한 표준 명세 (Section 5.3)
서드파티 쿠키 차단 정책
Chrome Developers — Prepare for third-party cookie restrictions Chrome의 서드파티 쿠키 단계적 폐지 대응 가이드
WebKit — Intelligent Tracking Prevention (ITP) Safari의 서드파티 쿠키 차단 정책(ITP) 공식 설명
Mozilla — Enhanced Tracking Protection (ETP) Firefox의 서드파티 쿠키 차단 정책(ETP) 공식 문서
BFF 패턴
Sam Newman — Backends For Frontends BFF 아키텍처 패턴을 처음 정의한 원문 글
microservices.io — Backends for Frontends pattern 마이크로서비스 컨텍스트에서의 BFF 패턴 설명
CORS
MDN — Cross-Origin Resource Sharing (CORS) CORS 동작 원리, preflight 요청,
Access-Control-*헤더 공식 레퍼런스MDN — Same-origin policy Same-Origin 정책의 정의와 브라우저 보안 모델 설명
Next.js
Next.js Docs — Middleware Next.js Middleware에서 요청/응답 헤더와 쿠키를 다루는 방법 공식 문서
Next.js Docs — cookies() Server Action / Server Component에서 쿠키를 읽고 쓰는
cookies()API 레퍼런스