이 전 포스트에 이어 본격적으로 여러 실험을 해보겠습니다.
서두가 길었습니다. 빠르게 코드를 살펴보겠습니다.
우선 저희 서버는 NestJS입니다.
Nest에는다양한 요청을 세분화하여 처리하는 Controller 속 route handler들이 있습니다.
이 라우트 핸들러에서 redirect와 쿠키를 입히는 작업을 수행할 수 있습니다.
@Get(`kakao/redirect`)
async signInByKakao(
@Query(ValidationPipe) signInByKakaoDto: SignInByKakaoDto,
@Res() res: Response,
) {
const { accessToken, refreshToken } = await this.authService.signInByKakao(
signInByKakaoDto,
);
Logger.log('User sign in', 'AuthController');
return res
.cookie(TOKEN.REFRESH_TOKEN, `Bearer ${refreshToken}`, {
maxAge: MAX_SIZE.REFRESH_TOKEN_MAX_AGE,
httpOnly: true,
sameSite: 'none',
secure: true,
})
.cookie(TOKEN.ACCESS_TOKEN, `Bearer ${accessToken}`, {
maxAge: MAX_SIZE.ACCESS_TOKEN_MAX_AGE,
sameSite: 'none',
secure: true,
})
.redirect(process.env.FRONT_URL); // default 302
}
현재 서버 상태 :
현재 BE와 FE의 도메인이 달라 "cross-origin" 상태
Access-Control-Allow-Origin=*
Access-Control-Allow-Credentials=true
쿠키는 secure, httpOnly, secure를 사용하고 있는 상태
위의 코드대로 요청과 응답을 한 번 살펴 보겠습니다.
// Request HTTP Headers
GET /auth/kakao/redirect HTTP/1.1
...
Host: [서버 도메인]
...
Referer: https://accounts.kakao.com/ -> 링크가 있던 곳
...
HTTP/1.1 302 Found
...
Access-Control-Allow-Credentials: true
Set-Cookie: token1=Bearer%[UUID]; Path=/; Expires=Thu, 29 Jun 2023 08:20:47 GMT; HttpOnly; Secure; SameSite=None
Set-Cookie: token2=Bearer%[UUID];; Max-Age=18000; Path=/; Expires=Thu, 22 Jun 2023 13:20:47 GMT; Secure; SameSite=None
Location: [클라이언트 도메인]
...
여기서 이상한 현상은 쿠키 스코프가 프론트 도메인이 아닌 서버 도메인에 잡힌다는 것입니다.

이렇게 되면 프론트에서 httpOnly가 아님에도 불구하고 쿠키를 열어볼 수 없게 됩니다.
하지만 쿠키가 set-cookie가 되어있기 때문에 쿠키 전송 자체는 아무 문제없이 잘 됩니다.
또한 safari 브라우저, 시크릿 모드 등은 쿠키가 전송 되지 않습니다.
찾아보니...

사파리는 기본적으로 크로스 사이트 추적을 막기 때문에 크로스 사이트 환경에서는 쿠키 설정이 한정되어 있습니다 -> 사파리때문에 답은 이미 정해져 있네요;;
MDN에 나온 redirect 정리로는 영속적인 redirect의 사용이 필요해 보였습니다.
지금 서버는 적절하지 않은 redirect 방법을 사용하고 있는 것 같은데요.
영속적인 redirect를 통해 쿠키 스코프를 프론트에 지정할 수 있지 않을까요?
Express에서 redirect를 default 값인 302 FOUND로 응답을 하는데 이를 301 Moved Permanently로 응답해보겠습니다.
...
return res
.cookie(TOKEN.REFRESH_TOKEN, `Bearer ${refreshToken}`, {
maxAge: MAX_SIZE.REFRESH_TOKEN_MAX_AGE,
httpOnly: true,
sameSite: 'none',
secure: true,
})
.cookie(TOKEN.ACCESS_TOKEN, `Bearer ${accessToken}`, {
maxAge: MAX_SIZE.ACCESS_TOKEN_MAX_AGE,
sameSite: 'none',
secure: true,
})
// 302 -> 301
.redirect(HttpStatus.MOVED_PERMANENTLY, process.env.FRONT_URL);
}
아쉽게도 아직도 쿠키 도메인이 서버로 잡힙니다...
하지만 영속적인 redirect를 해서 그런지 partition key는 카카오에서 서버로 변환된 것을 확인 가능합니다.

redirect의 문제가 아니었을까요? redirect가 아닌 일반 Get 환경에서 테스트 해보겠습니다.
@Get('testtest')
tttest(@Res() res: Response){
return (
res
.cookie(TOKEN.ACCESS_TOKEN, `Bearer asdf`, {
maxAge: MAX_SIZE.ACCESS_TOKEN_MAX_AGE,
sameSite: 'none',
secure: true,
})
.cookie(TOKEN.REFRESH_TOKEN, `Bearer asdf`, {
maxAge: MAX_SIZE.REFRESH_TOKEN_MAX_AGE,
httpOnly: true,
sameSite: 'none',
secure: true,
})
);
}
redirect 상황이 아님에도 불구하고 쿠키 스코프가 서버 도메인에 잡힙니다. (도메인 이전으로 인해 서버 도메인 이름이 바뀌었습니다.)
따라서 redirect와 상관없이 cross-origin 상황이 문제라는 것을 유추해 볼 수 있었습니다.

범인은 redirect가 아닌 것 같다. 그러면 누가 문제인가...
cross-origin 상황에 경우, 자동으로 쿠키의 스코프가 서버 도메인으로 할당되는 것을 확인하실 수 있으셨습니다.
그렇다면 쿠키 domain을 명시해주면 되지 않을까요?
return (
res
.cookie(TOKEN.ACCESS_TOKEN, `Bearer ${accessToken}`, {
maxAge: MAX_SIZE.ACCESS_TOKEN_MAX_AGE,
sameSite: 'none',
secure: true,
domain: process.env.FRONT_DOMAIN, // 도메인 추가
})
.cookie(TOKEN.REFRESH_TOKEN, `Bearer ${refreshToken}`, {
maxAge: MAX_SIZE.REFRESH_TOKEN_MAX_AGE,
httpOnly: true,
sameSite: 'none',
secure: true,
domain: process.env.FRONT_DOMAIN,
})
.redirect(HttpStatus.MOVED_PERMANENTLY, process.env.FRONT_URL)
);
}
프론트 도메인을 입히려는 수 많은 시도를 했지만
현재 host URL과 관련한 domain 속성이 잘못됐다라는 오류 메시지와 함께 쿠키가 아예 전송이 되지 않았습니다.

애초에 문제는 Origin이 다르기 때문입니다.
도메인만 맞춰준다면 여러 cross-origin 문제에 대해 해결하기 위한 걱정을 덜 해도 되며
같은 도메인이라면 스코프도 같기 때문에 걱정될 것이 없을 것 같습니다
아주 아주 잘 됩니다.
cross-origin은 보안때문에 제약이 많으니 처음부터 same-origin을 고려하는게 맞는 것같습니다!
애초에 redirect 문제가 아니라 cross-origin이 문제였던 것 같습니다.

쿠키 스코프가 서버로 잡히는 것은 redirect 문제가 아닌 cross-origin 문제!
서버 배포할 때는 클라이언트와 같은 origin으로 지정해주는 것이 보안적으로도, 개발적으로도 좋다!
| 대조군(초기) | 실험1(영속적인 redirect) | 실험2(쿠키 스코프 명시) | 실험3(same-domain) | |
|---|---|---|---|---|
| HTTPS | O | O | O | O |
| Access-Control-Allow-Credentials | O | O | O | O |
| Access-Control-Allow-Origins | FE SERVER | FE SERVER | FE SERVER | FE SERVER |
| 영속적인 redirect | X | O | X | X |
| 쿠키 Domain 명시 | X | X | O | X |
| same-domain | X | X | X | O |
| FE 쿠키 접근 결과 | X | X | X | O |