기록

서브도메인 로그인 공유와 Cookie/JWT 경계

TL;DR

  • a.domain.comb.domain.com은 Origin은 다르지만 Site는 같을 수 있다.
  • 부모 도메인 Cookie는 하위 서브도메인 요청에 함께 실릴 수 있다.
  • Cookie는 운반 수단이고, Session과 JWT는 로그인 상태를 표현·검증하는 방식이다.
  • JWT_SECRET은 Cookie에 넣는 값이 아니라 서버가 Token을 검증하기 위해 공유하는 비밀값이다.
  • Shared Domain Cookie는 SSO 경험을 만들 수 있지만, 중앙 IdP 기반 SSO와 같은 말은 아니다.

요구사항은 두 서비스 사이의 로그인 공유였다.

a.domain.com에서 로그인
  -> b.domain.com으로 이동
  -> 다시 로그인하지 않음

두 서비스는 모두 Next.js로 만들어져 있고, 루트 도메인 domain.com에는 별도 서비스가 없었다. 서버 쪽 설계는 부모 도메인 범위 Cookie에 인증 Token을 담고, 각 서비스가 같은 Secret으로 JWT를 검증하는 방향이었다. 이 구조를 정확히 읽으려면 Cookie, SameSite, Session, JWT를 같은 층위로 놓으면 안 된다.

서브도메인 간 로그인 공유의 핵심은 Set-CookieDomain 속성이다.

Set-Cookie: auth_token=<JWT>;
Domain=domain.com;
Path=/;
HttpOnly;
Secure;
SameSite=Lax

Domain=domain.com으로 설정된 Cookie는 a.domain.com, b.domain.com 요청에 함께 실릴 수 있다. 루트 도메인에 실제 웹 서비스가 배포되어 있어야 하는 것은 아니다. Cookie를 저장하고 요청에 붙이는 주체는 브라우저다. 브라우저는 요청 host와 Cookie domain이 domain-match 되는지 확인한다.

요청 host Domain=domain.com Cookie 포함 가능성
domain.com 가능
a.domain.com 가능
b.domain.com 가능
other.com 불가능

이 방식이 성립하려면 두 서비스가 같은 registrable domain 아래에 있어야 한다. a.company-a.comb.company-b.com처럼 부모 도메인을 공유하지 않으면 같은 Cookie를 나눠 쓸 수 없다.

Same-Origin과 Same-Site

a.domain.comb.domain.com은 Same-Origin이 아니다.

https://a.domain.com
https://b.domain.com

Origin은 scheme, host, port를 함께 본다. 두 URL은 host가 다르므로 Origin이 다르다. SameSite는 다른 기준을 쓴다. scheme과 registrable domain을 본다. 둘 다 https이고 registrable domain이 domain.com이면 Same-Site로 판정될 수 있다.

기준 비교 대상 a.domain.comb.domain.com
Same-Origin scheme + host + port 다름
Same-Site scheme + registrable domain 같음

그래서 SameSite=Lax가 있다고 해서 두 서브도메인 사이의 Cookie 공유가 자동으로 막히는 것은 아니다. SameSite가 막는 대상은 cross-site 요청이고, 두 서브도메인은 origin 기준으로는 다르지만 site 기준으로는 같을 수 있다. 이 구분은 CORS와 함께 볼 때 중요하다. CORS는 Origin을 보고, SameSite Cookie 정책은 Site를 본다. 두 판정이 다른 결과를 낼 수 있다.

Cookie 인증과 JWT 인증을 서로 대립하는 방식으로 표현하면 구조가 흐려진다.

개념 역할
Cookie 브라우저가 값을 저장하고 HTTP 요청에 싣는 메커니즘
Session 여러 요청에 걸쳐 로그인 상태를 유지하는 방식
JWT Claim을 담고 서명할 수 있는 Token 형식

Cookie 안에는 여러 종류의 값이 들어갈 수 있다.

theme=dark
session_id=abc123
auth_token=<JWT>

session_id를 담으면 서버는 그 ID로 Session Store를 조회한다.

Browser
  Cookie: session_id=abc123

Server
  Redis/DB에서 abc123 조회
  -> user 확인

JWT를 담으면 서버는 Token의 서명과 만료를 검증한다.

Browser
  Cookie: auth_token=<JWT>

Server
  verify(token, JWT_SECRET)
  -> sub, exp, role 확인

Cookie는 운반 수단이다. JWT는 운반되는 값의 형식이다. 서버가 별도 Session Store를 조회하는지, Token 자체를 검증하는지는 Cookie 사용 여부만 보고 알 수 없다.

이번 구조는 Shared Domain Cookie에 JWT를 담고, 각 서비스가 같은 Secret으로 Token을 검증하는 형태다.

Login API
  -> JWT 발급
  -> Set-Cookie: auth_token=<JWT>; Domain=domain.com

a.domain.com -> Cookie에서 auth_token 추출 -> JWT 검증

b.domain.com
  -> Cookie에서 auth_token 추출
  -> JWT 검증

여기서 Cookie에 들어가는 값은 JWT다. JWT_SECRET은 서버에만 있어야 한다.

브라우저에 저장 가능 브라우저에 저장하면 안 됨
auth_token=<JWT> JWT_SECRET

두 서버가 같은 HMAC Secret을 공유한다면 두 서버 모두 검증뿐 아니라 서명도 할 수 있다. 발급 권한을 한 곳으로 제한하고 싶다면 비대칭키 구조를 검토할 수 있다.

Auth Server
  private key로 JWT 서명

a.domain.com / b.domain.com
  public key로 JWT 검증

이 구조에서는 각 서비스가 Token을 검증할 수 있지만 임의로 발급할 수는 없다.

Scope와 신뢰 경계

Domain=domain.com은 특정 두 서비스만 여는 설정이 아니다. 해당 부모 도메인 아래의 하위 도메인 전체를 Cookie scope에 넣는다.

auth_token Domain=domain.com
  -> a.domain.com
  -> b.domain.com
  -> admin.domain.com
  -> preview.domain.com

따라서 이 설정은 기술 설정이면서 신뢰 경계 설정이다. 하위 도메인 전체를 같은 인증 영역으로 묶어도 되는지 확인해야 한다. 서비스가 같은 제품군이고, 같은 운영 주체가 관리하며, 같은 계정 체계를 공유한다면 부모 도메인 Cookie는 단순하고 효과적이다. 반대로 신뢰 수준이 다른 서브도메인이 붙어 있거나, 외부 사용자가 서브도메인 콘텐츠를 만들 수 있다면 Cookie scope를 더 좁혀야 한다.

로그아웃과 무효화

JWT를 Stateless하게 검증하면 서버는 매 요청마다 저장소를 조회하지 않는다. 서명과 만료가 맞으면 통과한다. 이 구조의 장점은 단순함이다. 단점은 즉시 무효화가 어렵다는 점이다.

JWT exp = 1h
사용자가 로그아웃
브라우저 Cookie 삭제
이미 복사된 Token은 exp까지 유효할 수 있음

로그아웃 즉시 모든 서비스에서 Token을 막아야 한다면 상태가 필요하다.

요구사항 필요한 설계
짧은 만료만으로 충분 Stateless JWT
즉시 로그아웃 무효화 blacklist, token version, session store
refresh token 회전 저장소 또는 발급 이력 관리

Shared Cookie는 브라우저에 저장된 Token 전달 문제를 해결한다. Token 수명과 무효화 정책은 별도 문제다.

사용자 경험만 보면 Shared Domain Cookie는 SSO처럼 동작한다. 한 서비스에서 로그인하면 다른 서비스에서도 로그인 상태가 유지된다. 하지만 Shared Cookie와 SSO는 같은 말이 아니다.

용어 의미
Shared Cookie 같은 Cookie를 여러 서브도메인이 사용
SSO 한 번의 인증으로 여러 서비스에 접근하는 경험 또는 아키텍처
중앙 IdP SAML, OIDC 등으로 인증 주체를 분리한 구조

SSO 경험은 Shared Cookie로 만들 수 있다. 중앙 인증 서버를 두고 서비스마다 별도 Session을 발급하는 방식으로도 만들 수 있다.

Auth Server
  -> 이미 로그인된 사용자 확인

a.domain.com -> a_session 발급

b.domain.com
  -> b_session 발급

이 경우 두 서비스의 Session은 다르지만 사용자는 다시 로그인하지 않는다. 반대로 Shared Cookie 구조에서는 같은 Token을 여러 서비스가 함께 읽는다. 그래서 이번 구조는 “중앙 IdP 기반 SSO”라기보다 “Shared Domain Cookie로 만든 서브도메인 간 로그인 유지”로 표현하는 편이 정확하다.

구현 체크리스트

이 구조를 Next.js 서비스에 넣을 때 확인할 항목은 다음과 같다.

항목 확인
Cookie name 서비스 간 동일한 이름 사용
Domain 부모 도메인으로 설정
Path 보통 /
HttpOnly 클라이언트 JS에서 Token을 읽지 못하게 설정
Secure HTTPS에서만 전송
SameSite 서비스 이동 흐름에 맞게 Lax 또는 별도 판단
JWT 검증 exp, iss, aud, sub 확인
Secret 관리 서버 환경변수로만 보관
로그아웃 Cookie 삭제와 서버 측 무효화 정책 분리

개발 환경에서는 도메인 조건도 따로 봐야 한다. localhost의 여러 포트는 production 서브도메인 구조와 다르게 동작할 수 있다. 실제 Cookie domain과 SameSite 조건은 배포 도메인에 가까운 환경에서 검증하는 편이 낫다.

정리

서브도메인 로그인 공유는 Token 저장 위치 문제가 아니다. 브라우저가 어떤 범위의 Cookie를 어떤 요청에 싣는지, 서버가 그 값을 어떤 방식으로 검증하는지, 그 Cookie scope 전체를 같은 신뢰 경계로 볼 수 있는지가 핵심이다. 이번 구조는 이렇게 정리할 수 있다.

Cookie
  -> 부모 도메인 범위로 Token을 운반한다

JWT -> 로그인 정보를 서명된 Token으로 표현한다

각 서비스 서버
  -> 같은 검증 기준으로 Token을 읽는다

Cookie, Session, JWT를 같은 층위에 놓지 않으면 설계가 단순해진다. Cookie는 운반하고, JWT는 표현하고, 서버는 검증한다.

참고

관련 글