빼곡 서비스의 로그인 로직은 JWT 기반으로 구성되었다. 한 번 발급된 JWT 은 유효기간이 만료되기 전까지 계속 사용이 가능해 대처가 어려워진다. 그래서 엑세스 토큰의 유효기간을 짧게하고, 리프레시 토큰의 유효기간을 길게두어 엑세스 토큰이 만료되면 리프레시 토큰으로 새로운 엑세스 토큰을 발급 받도록 했다.
그런데, 리프레시 토큰을 갱신할 때 새로운 엑세스 토큰이 발급되지 않는 에러가 발생했다.
정확히는 쿠키 설정이 제대로 되지 않아 쿠키를 읽을 수 없고 때문에 리프레시 토큰을 검증할 수 없어 새로운 엑세스 토큰이 발급되지 않는 것이었다.
(쿠키의 문제임을 확인하기 전에는 백엔드 문제라고 생각했다. 리프레시 토큰 검증 과정에서 엑세스 토큰의 유효성도 함께 검증했기 때문에, 스프링 시큐리티 내부에서 해당 요청이 필터링 되어 에러가 발생했기 때문이다.)
백엔드 친구가 위의 사진을 보내주며 쿠키에 토큰 값이 보이냐고 물어봤다.
https://bbaegok.duckdns.org:50443/backend
https://bbaegok.netlify.app/
두 도메인의 서브 도메인이 다르기 때문에 쿠키 설정이 어렵다는 이야기였다.
이전 프로젝트를 진행할 때는 백엔드와 프론트의 배포 도메인이 달라도 쿠키 설정에 문제가 없었으며, 백엔드 쪽에서 SameSite
설정을 None
으로 설정해놨기 때문에 쿠키를 왜 필터링 하는지 알 수 없었다.
프론트쪽에서도 axios 인스턴스를 생성할 때 withCredentials
옵션을 true
로 설정(인증정보를 같이 전달드리겠습니다)해두었기에 더 혼란스러웠다.
import axios from 'axios';
import { getSessionItem, removeSessionItem, setSessionItem } from 'utils/sessions';
// api 인스턴스
const api = axios.create({
baseURL: import.meta.env.VITE_SERVER_BASE_URL,
timeout: 1000,
withCredentials: true,
});
api.interceptors.request.use(
(config) => {
const newConfig = { ...config };
const accessToken = getSessionItem('accessToken');
if (accessToken) {
newConfig.headers.Authorization = `Bearer ${accessToken}`;
}
return newConfig;
},
(error) => {
return Promise.reject(error);
},
);
그래서 발급받은 로컬에서 리프레시 토큰을 발급받아 토큰 값으로 포스트맨에서 테스트를 진행해보았다.
포스트맨에서는 잘되는 것을 확인했다. 포스트맨도 도메인이 다르지만 SameSite
옵션을 None
으로 설정했기 때문에 문제 없이 동작하는 것을 볼 수 있었다. 하지만 로컬에서는 여전히 문제가 해결되지 않았다.
Https
를 사용하기 위해 mkcert
를 통한 인증서 설치그럼 왜 로컬에서만 안되는 것일까?
FEDeepDive 스터디에서 공부했던 네트워크 이론에서 쿠키 세팅에 여러 옵션이 있었던 것이 생각났다.
쿠키에 저장되어 있는 토큰의 옵션을 보니 SameSite
옵션이 None
, Secure
옵션이 true
로 체크되어 있는 것을 발견했다.
조금 더 자세히 찾아보니 쿠키 정책 개선에 대한 문서를 발견했다.
해석해보자면 다음과 같다.
SameSite=Lax
를 기본으로 여긴다.SameSite=None
으로 설정하며 Secure
옵션을 필수적으로 사용해야 한다.크롬에서 쿠키 정책에 대해 안내한 포스트를 참고하면서 문제를 파악할 수 있었다.
포스트맨의 경우 도메인은 다르지만 Https
프로토콜을 사용하고 있어 Secure
정책에 위반되지 않았고 우리의 로컬 환경은 Https
프로토콜을 사용하고 있기 때문에 Secure
정책에 위반되어 브라우저가 쿠키 전송을 허용하지 않았던 것이다.
따라서 로컬환경에서 신뢰할 수 있는 인증서를 만들어주는 라이브러리 mkcert
를 설치해 로컬에서도 Https
프로토콜을 사용해 개발을 사용할 수 있도록 해주었다.
Get-ExecutionPolicy
실행Set-ExecutionPolicy AllSigned
실행 후 Y 입력Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
choco install mkcert # choco를 사용한 mkcert 설치
mkcert -install # 로컬 루트에 mkcert 추가(어떤 위치에서 작업해도 상관없다.)
cd ${project} #프로젝트 루트 디렉터리로 이동
mkcert localhost # mkcert로 서명된 사이트 인증서 생성
위 명령어를 차례로 입력하면 프로젝트 루트에 localhost-key.pem
파일과 localhost.pem
파일이 만들어 진 것을 볼 수 있다.
생성된 인증서를 가지고 vite
가 실행될 수 있도록 vite-plugin-mkcert
패키지를 설치한다.
pnpm i -D vite-plugin-mkcert
설치된 플러그인을 vite.config
파일에 넣어 로컬 환경에서도 Https
를 통해 서비스 접속이 가능하도록 했다.
그런데 쿠키가 안보내진다…?
네트워크 탭에서 재발급 요청을 열어보면 우측 상단에 쿠키 탭이 존재하지 않으며 애플리케이션 탭에서도 쿠키가 필터링돼서 찾아볼 수 없었다.
절망적인 쿠키 is null..
똑같은 문제를 빙빙 돌고 있다가 브라우저 문제가 아닐까 ? 싶어서 웨일에서도 시도해보았다.
웨일로 작업했더니 401에러가 재귀적으로 호출되는 새로운 에러가 발생했다.
일반 브라우저와 시크릿 모드에서의 에러 차이로 인해 시크릿 모드의 문제일까 싶어서 구글링을 해보나 스택오버플로우에서도 시크릿모드에선 강제로 서드파티 쿠키 막는다고 한다.
즉 시크릿 모드에서는 보안을 강화하기 위해 기본적으로 서드파키 쿠키를 차단한다.
그렇기 때문에 백엔드에서는 쿠키가 없다고 판단해 401 권한 없음 에러와 500 쿠키없음 에러를 번갈아 가며 호출하고 있던 것이었다.
아래는 프론트에서 401 권한 없음 에러를 핸들링하는 부분이다.
api.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const { response, config } = error;
if (response.status === 401) {
try {
const refreshResponse = await api.get('/auth/refresh');
const newAccessToken = refreshResponse.data.data;
const newConfig = { ...config };
removeSessionItem('accessToken');
setSessionItem('accessToken', newAccessToken);
newConfig.headers.Authorization = `Bearer ${newAccessToken}`;
return api(newConfig);
} catch (err) {
// TODO : 로그인 재시도 안내 구현
removeSessionItem('accessToken');
return Promise.reject(err);
}
}
return Promise.reject(error);
},
);
즉 시크릿 모드에서는
하지만 일반 브라우저 모드에서는
시크릿 모드에서 테스트를 했기 때문에 쿠키 문제가 발생했다는 사실을 알게되었다.
(캐싱과 관련된 문제일 것 같아 시크릿모드로 작업했던 것이 문제였다)
401 에러가 재귀적으로 발생하지만 쿠키가 잘 전송되는 것을 확인했다.
알고보니 재귀적으로 발생하던 401 에러는 백엔드에서 리프레시 토큰 값을 일부만 저장해 같은 토큰임에도 데이터베이스에서 찾아볼 수 없어 생기는 오류여서 금방 해결할 수 있었다.
이 모든 과정을 거치고 나니 성공적으로 엑세스 토큰을 재발급 받을 수 있었다.