
인증(Authentication): 사용자의 신원을 확인하는 프로세스 (로그인)

api.auth.login()) 요청HTTPOnly, samesite=strict옵션으로 쿠키에 담아주며 access token은 response 해준다.authStore.login() 호출api.me.get() 요청을 통해 유저의 정보를 요청한다.axios.interceptor를 통해 만약 store에 access token이 존재한다면 요청 헤더에 authorization으로 토큰을 함께 전달한다.Access Token: 최소한의 유저 정보만을 담은 JWT토큰
Refresh Token: 만료된 Access Token을 다시 발급시켜주기 위한 토큰
Q. 각 토큰들은 어디에 저장이 되어야 안전할까?
'안전'하다라는 것부터 정의를 해야할 것 같다.
토큰을 탈취당했을 때 어떤 문제점이 발생할 수 있을까?
그럼 어떻게 토큰이 탈취될 수 있을까?
대표적인 두 가지의 웹사이트 공격이 있다.
그러면 토큰을 웹에 저장할 수 있는 공간은 어디어디 있을까?
Cookie : 매 요청마다 함께 서버로 전송되며 옵션에 따라 보안 설정이 가능하다.Local Storage: 영구 보관이 가능하고(기간 설정 불가) 도메인마다 스토리지 생성Session Storage: 세션(브라우저, 탭)이 종료되면 데이터가 사라진다.Memory: 런타임 메모리에 저장되며 새로고침 시 초기화된다.그러면 우리는 예상되는 공격으로부터 안전하게 보관할 수 있는 곳에 토큰을 저장하면 안전하다라고 말할 수 있을 것이다.
Cookie에는 옵션을 통해 여러 보안 설정을 할 수 있다.
그 중에는 XSS와 CSRF를 막을 수 있는 보안 설정도 존재한다.
Secure: https 통신에서만 쿠키 접근 가능HTTPOnly: Javascript의 document.cookie API를 통해 쿠키 접근 불가SameSite: 방문 중인 사이트와 다른 사이트의 리소스를 요청하는 것을 Cross Site 요청이라고 하며 이에 대한 응답으로 설정된 쿠키를 Third Party Cookie라고 부른다. 해당 속성은 세 가지 옵션이 제공된다.Strict: 타 사이트에서 시작된 모든 요청에 쿠키를 포함하지 않음Lax: 일부 안전한 방법(GET허용, POST불허)으로 타 사이트에서 접근 시 쿠키 전송 허용None: 제약 없음 (Secure 속성이 켜져야 한다.)Expires/Max-Age: Expires는 날짜와 시간으로, Max-Age는 초 시간으로 쿠키의 만료 기간을 설정할 수 있다.Domain: 쿠키가 적용되는 도메인을 설정하고 해당 도메인과 서브 도메인들의 요청에 쿠키가 포함될 수 있다.Path: 경로를 설정하면 해당 경로 이하의 요청에 대해서만 쿠키가 포함된다. /articles > /articles/a/b/c 포함SameSite에 대해 더 자세히 보자면,
a.com과 그 하위 도메인인 api.a.com은 서브 도메인 관계이기에 같은 사이트(Same Site)로 판단한다.
완전히 다른 사이트에 대해 쿠키 전송 제약을 주는 옵션이다.
예시)
[b.com]이라는 사이트에 로그인을 하여 각각 쿠키를 얻게 되었다.
- 쿠키A: SameSite=Strict
- 쿠키B: SameSite=Lax
- 쿠키C: SameSite=None; Secure
1. [a.com]에서 [b.com] 링크를 클릭 (안전한 요청)
- 쿠키 A: 전송 X
- 쿠키 B: 전송 O
- 쿠키 C: 전송 O
2. [a.com]에서 [b.com]의 이미지 요청(<img src="https://b.com/image.png">)
- 쿠키 A: 전송 X
- 쿠키 B: 전송 X
- 쿠키 C: 전송 O
그렇다면 쿠키를 통해서 어떻게 XSS와 CSRF를 막을 수 있을까?
HTTPOnly는 자바스크립트의 코드를 통해 쿠키에 접근, document.cookie API를 사용할 수 없기 때문에 악성 스크립트를 포함하여 공격하는 XSS를 방지할 수 있다.
SameSite=Strict는 동일한 사이트에서만 들어오는 요청에 대해 쿠키를 전송하기 때문에 외부 사이트에서 인증된 토큰을 통해 요청을 보내오는 CSRF 공격을 방지할 수 있다.
Lax설정 또한 안전한 네비게이션(GET 요청)에 대해서만 쿠키를 보내고 POST나 AJAX, iframe과 같은 요청에는 쿠키를 안보내기 때문에 어느 정도 CSRF 내성이 있긴 하지만 Strict가 제일 안전하다.
그렇다면 Access Token과 Refresh Token을 Cookie에 저장하는 것이 안전한 방법인가?
Cookie 보안 속성을 통해 XSS, CSRF 공격들을 예방하여 토큰을 웹에 저장시킬 수 있다는 장점이 있다.
하지만 보안을 강화시키게 되면 개발자 또한 접근이 어려워지는 경우가 있다.
예를 들어 HTTPOnly는 자바스크립트 코드를 통해 쿠키에 접근을 막는 속성이다. 즉, 클라이언트는 해당 쿠키를 다를 수 없으며, 요청/응답을 통해서만 쿠키를 전달할 수 있다.
하지만 Access Token은 요청 헤더에 담아 서버로 전달하여 인가를 받기위해 만들어진 토큰이지만 HTTPOnly 옵션 때문에 코드 상으로 쿠키에서 추출할 수 없어, 요청 헤더에 직접 담을 수 없다는 문제가 발생한다.
//plugins/axios.ts
const accessToken = document.cookie.get(...) // HTTPOnly 속성으로 쿠키 추출 ❌
if (accessToken) {
config.headers.Authorization = `Bearer ${token}`;
}
그렇기 때문에 Refresh Token은 쿠키에 저장하고 Access Token은 다른 방식으로 저장할 필요가 있다.
바로 메모리에 저장하는 방식이다.
메모리를 저장해주는 방식은 여러가지가 있겠지만 현재 Vue를 통해 제작하고 있기에 이에 친숙한 Pinia를 사용하여 전역으로 Store에 토큰을 저장해주는 방식을 채택하였다.
그렇다면 메모리는 CSRF와 XSS 공격으로부터 안전한가?
메모리에 저장하는 방식은 XSS 공격을 당할 수도 있다는 단점이 있기에 이를 보안할 방법들을 설정해주는 것이 좋다.
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self';" />인가(Authorization): 특정 작업을 수행할 수 있는 권한을 가지고 있는지 검사
권한이 필요한 API를 요청하는 것에 대한 설계는 인증 설계 단계에서 api.me.get() 요청을 보내는 것과 동일하다.
하지만 토큰이 만료되었을 경우를 포함한 다른 예시로 다이어그램을 제작하였다.

api.todo.get(_id)를 통해 해당 유저의 todo 데이터를 요청한다.axios.interceptor를 통해 요청의 헤더에 store에 저장된 access token을 추가한다.만약 3번에서 access token이 만료되거나 유효하지 않았을 경우, 401 UnauthorizedException를 발생시킨다.
axios.interceptor에서 응답 에러의 코드가 401일 경우, store에 저장된 auth.refreshAccessToken()을 호출한다.api.auth.refresh() API를 요청한다.만약 4번 refreh token이 만료되거나 유효하지 않을 경우, 로그아웃을 시키고 유저를 로그인 페이지로 이동시킨다.