테오의 스프린트에서 만들었던 토이 프로젝트 '삿치'에 카카오로그인을 붙여 로그인 가능한 서비스로 만들려고 했다.
로그인을 구현하는 김에 JWT의 refresh token을 활용해보면 어떨까? 하는 마음에 검색을 시작했고
silent refresh라고 불리는 방법을 사용하기로 했다.
silent refresh는 access token을 javascript 변수에 두고 refresh token은 http only cookie에 두어 access token의 수명이 다하면 사용자가 눈치채지 못하게 access token을 교체해주는 방법이다. 새로고침 했을때도 cookie에 refresh token이 있으니 얼마든지 다시 로그인 상태로 만들 수 있는것이다.
보안상 access token을 local storage에 놓지 않는 것이 좋다는 이야기는 끊임 없이 들어지만 그렇다면 어떻게 관리를 해야 할지 몰라 못하고 있었고 이번이 기회다 싶어 refresh token 사용법을 익혀보자 생각해서 시작했는데 이렇게 많은 난관에 부딛힐 줄 몰랐고. 그 결과 끝내 해냈고 그 결과 백엔드와 프론트엔드 양쪽 시점에서 쿠키에 대한 지식을 많이 얻었고 나중에 활용하기 위해 여기 기록한다.
지인에게 쿠키에 대해서 설명 해드리다 보니 실제로 해보고 싶다고 하셔서 간단하게 실습해 볼 수 있는 간단한 프로젝트를 만들었습니다.
https://github.com/soorokim/cookie-study
MDN링크: Set-Cookie
위 링크의 MDN문서를 보면 다양한 옵션들이 존재한다.
Expires<Date>: 쿠키의 생존 수명이다. 설정하지 않으면 '세션쿠키'로 동작하게 되고 '세션쿠키'는 브라우저가 닫히면 사라진다. 형태는 Date형식이다.
Expires를 설정하면 설정된 기간까지 쿠키가 유지된다. 클라이언트의 시간에 상대적인 값으로 취급
Max-Age<number>: 쿠키의 만료시간을 초단위로 설정할수 있다. 0또는 음수가 지정되면 쿠키는 즉시 만료되고 ie6,7,8에서는 사용 할수 없다. Expires와 같은 기능을 하는 값, 함께 지정됐을때 Max-Age가 우선권을 갖는다.
Domain: 쿠키가 적용되어야 하는 호스트를 지정한다. 지정되어 있지 않으면 현재 URI를 기준으로 적용되고, 서브도메인은 포함하지 않는다. 백앤드의 domain이어야 한다. 사실 잘 모르겠다..ㅠ
ex) http://localhost:3000/auth/login -> localhost
Path: 쿠키 헤더를 보내기전에 리소스에 있어야 하는 URL경로를 나타낸다.
paths는 두가지 경우에 사용되는데
1. 브라우저 프론트엔드에서 백엔드로 요청을 보낼때 백엔드의 경로가 일치하면 request header에 싣어보낸다.
2. 프론트엔드에서 현재 uri의 path와 쿠키의 path가 일치할때 document.cookie에 접근하여 값을 가져온다.
ex) 만약 백엔드의 토큰 refresh할 때만 사용 할것이고
endpoint가 http://localhost:3000/auth/refresh 라면
path=/auth/refresh로 설정 하면된다. 참고로 path=/auth라고 적게되면 하위 경로를 모두 포함한다. (/auth, /auth/login, /auth/refresh)
!! 기본적으로 path는 어디서든 접근 할 수 있도록 "/"로 지정한다고 한다.
Secure: 보안쿠키들은 https프로토콜을 사용할때만 전송됩니다.
HttpOnly: httpOnly쿠키로 설정하게 되면 클라이언트 사이드의 자바스크립트에서 해당 쿠키에 접근 할 수 없습니다.
SameSite: 쿠키가 동작될 same site규칙입니다. CSRF에 대한 일부 보호를 제공합니다.(여기서 same site규칙은 same origin과 헷갈릴수 있는데 비슷하지만 조금 다릅니다. 아래서 설명하겠습니다.)
same-site은 lax, strict, none값으로 설정 할 수 있습니다.
strict인 경우에는
a사이트 -> a사이트 인 경우에만 쿠키를 전송하고
lax인 경우에는
b사이트 -> a사이트인 경우 몇가지 예외를 두어 쿠키를 전송할 수 있고
none인 경우에는
모든사이트에서 a사이트로 요청할때 쿠키를 전송합니다.
하지만 none인 경우에는 secure옵션을 반드시 필요로하고
secure옵션을 사용 하게되면 https 프로토콜로 요청해야합니다.
Google Developers링크: 동일사이트 및 동일 출처 이해하기
- 주의: 브라우저는 공개접미사에 대한 쿠키설정을 거부한다.
예를 들어 heroku에 업로드되어 xxx.herokuapp.com의 주소를 사용하는 클라이언트와 서버는 쿠키를 주고 받을 수 없다고 한다. stackoverflow 글
Google Developers링크: SameSite 쿠키 설명
추가 설명은 링크로 대체한다!
app.enableCors({
origin: 'http://frontend-url.com',
credentials: true,
});
nest에서의 cors설정은 간단하게 이렇게 해 주면된다.
origin은 Access-Control-Allow-Origins값이고
credentials는 Access-Control-Allow-Credentials값이다.
프론트엔드에서 백엔드에 쿠키를 보내기위해서는 axios에서는 withCredential을 true로 설정해주어야하고 fetch에서는 credentials옵션을 true로 주게되는데 이때 백엔드에서 Access-Control-Allow-Origins값이 '*'(모든 오리진에 대한 접근 허용)로 설정되어있으면 아래와 같은 오류를 확인 할 수있다.
Access-Control-Allow-Credentials은 프론트에서 받는 증명정보를 포함한 요청을 허용하기위해서 설정 해준다. (자격증명: 쿠키, authorization 헤더들 또는 TLS 클라이언트 인증서)이를 위해서는 프론트엔드에서 withCredential을 true로 설정 해주어야 한다.
cookie는 백엔드에서 로그인시 payload에 access token을 담아 응답하고 set-cookie를 사용해 httpOnly옵션과 path옵션을 사용해 응답객체에 추가해준다.
res.cookie('refresh_token', refreshToken, {
path: '/auth',
httpOnly: true,
});
추가 적으로 실무에서 사용하려면 백엔드와 프론트엔드 모두 ssl을 적용하여 https 환경으로 만들고 secure옵션을 주어야 할것 같다.
Axios.create({
baseURL: 'http://example-backend-url',
responseType: 'json' as const,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiConfiguration.accessToken}`,
}),
},
withCredentials: true, // 이 부분!
timeout: 10 * 1000,
});
프론트엔드에서 해줘야 할 설정은 간단하다.
httpOnly 쿠키로 잘 받아진다.
httpOnly 쿠키가 잘 전송되고 백엔드에서도 확인 가능하다!.