호기롭게 해보겠다는 이 한 마디를 시작으로 Toks의 OAuth2.0 인증 구현을 맡아서 하게 되었다.
덕분에 고통스럽지만 매우 매우 성장할 수 있었다고 생각한다.
OAuth 2.0은 인증을 위한 개방형 표준 프로토콜이다.
유저가 똑스 서비스에 접속을 했을 때 만약 똑스에서 퀴즈 문제를 풀면 카카오톡으로 알림을 보내는 기능을 제공하고 싶다고 해보자. 그렇다면 유저가 똑스에 카카오 아이디와 비밀번호를 입력하고 똑스가 유저의 카카오에 접근할 수 있도록 하면 될 것이다. 그러나 이는 너무 위험해 보이는 방법이 아닌가.. ?! 만약 똑스의 서버가 털려서 유저의 카카오 아이디와 비밀번호가 유출되는 사고가 발생한다면 .....
이 때 OAuth를 이용하면 유저의 아이디와 비밀번호를 직접 똑스에 가지고 있지 않고 카카오에서 발급해주는 Access Token를 가지고 카카오에 접근할 수 있게 해준다. Access Token을 사용하게 된다면 유저의 정보를 보관할 필요 없이 로그인을 다룰 수 있게 된다.
이를 조금 있어보이게 얘기하면
OAuth2.프로토콜에서는 Third-Party 프로그램에게 리소스 소유자를 대신하여 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식을 제공한다.
라고 할 수 있다.
OAuth2.0의 주요 용어로는 다음이 있다.
똑스는 일단 소셜로그인으로 카카오 로그인만 사용하기로 결정했다.
일단 내가 위의 인증 과정을 보고 기존에 카카오 소셜 로그인을 구현하던 방법을 기반으로 이해한 바를 간단하게 설명해보자면
백에서 스프링 시큐리티를 이용하여 인증 구현을 해서 백에서 카카오 로그인페이지로 리다이렉트를 시키고 인가코드를 발급받고 이를 이용하여 토큰을 생성하는 작업까지 모두 처리해주었다. 그래서 다시 우리의 로그인 과정을 정리해보면..
이 과정을 이해하는 데까지 시간이 꽤 오래 걸렸다. 이해하는 과정에서 백에서 로그인을 맡은 분과 엄청나게 많은 대화를 주고 받았는데 죄송하기도 하면서 동시에 성장하는 과정이었다고 생각한다.
자 이제 겨우 백과 토큰을 주고 받는 과정까지는 이해가 되었는데, Toks에서 로그인을 구현하기 위해 고려해야 할 사항이 몇 가지 있었다.
axios interceptors를 사용하게 되면 비동기 응답이 then 또는 catch로 처리되기 전과 request를 하기 직전에 request와 response를 가로챌 수 있다.
즉, axios interceptor를 사용하여 401과 같은 auth 에러 처리를 해놓으면 axios reponse를 받는 곳에서 하나하나 에러 체킹을 하지 않아도 된다는 엄청난 장점이 있다.
사실 여기서는 크게 장단점을 따지고 진행을 했기 보다는 백과의 논의를 통해 결정했다. 백에서 쿠키 설정을 추가로 진행하는 것이 일정에 무리가 있다고 판단하여 비교적 간단한 헤더에 토큰을 넣어 통신하는 방식을 선택했다.
그러나 Access Token과 Refresh Token을 모두 로컬이나 세션 스토리지에 저장하는 것 보다는 Refresh Token을 쿠키에 저장하는 방식이 더 보안상 안전하다고 하여 이 부분은 2차 mvp에서 수정을 할 수도 있을 것 같다.
다만, 어떤 방법이든 trade-off가 존재하기 때문에 단순히 보안 상의 장단점만 따지기 보다는 팀원들과 어떤 방법이 현재 우리 서비스에 더 적합한 방법인지 충분히 논의를 해보고 결정을 내릴 것이다.
위의 질문에 대해 현구님과 열심히 로그인 플로우에 대해 고민하고 정리한 결과물.. 아마 두고두고 꺼내볼 듯 싶다..
사진으로 정리한 위의 플로우를 글로 정리해보자.
Toks를 방문하는 사람을 1️⃣ 아예 Toks를 처음 방문하는 사람, 2️⃣ 회원이지만 Access Token이 만료된 사람, 3️⃣ 회원이지만 Refresh Token이 만료된사람 이렇게 세 가지 케이스로 분류했다.
1️⃣ 아예 처음 방문하는 사람은 바로 로그인을 하게 하면 된다.
2️⃣ 회원이지만 Access Token이 만료된 사람은 Refresh Token을 백으로 보내 Refresh Token이 유효하다면 새로운 Access Token을 발급받는다.
3️⃣ 만약 Refresh Token까지 만료가 된 회원이라면 로그인 페이지로 리다이렉트 시켜서 다시 로그인을 하게 한다. 바로 위의 이 부분들이 axios interceptors에서 처리되어야 하는 부분이다.
로그인 시 이미 닉네임이 설정이 되어 있는 회원이라면, 즉 이미 회원 가입을 진행한 회원이라면 닉네임 설정 페이지가 아닌 홈페이지로 리다이렉트 시키고 만약 닉네임 설정이 되어있지 않다면 닉네임 설정 페이지로 리다이렉트 시킨다.
또한 Token을 받는 리다이렉트 페이지에서 토큰을 꺼내 세션스토리지에 저장한다. 그리고 패키지에서 일단 로그인 로직을 시작할 때 세션스토리지에 토큰이 있는지 확인하여 토큰이 있는 경우 불필요한 로직을 수행하지 않도록 한다.
//http.ts
const authToken = {
access: typeof window === 'undefined' && typeof global !== 'undefined' ? null : sessionStorage.getItem('accessToken'),
refresh:
typeof window === 'undefined' && typeof global !== 'undefined' ? null : sessionStorage.getItem('refreshToken'),
};
const redirectToLoginPage = () => {
const isDev = window.location.hostname === 'localhost';
window.location.href = isDev ? 'http://localhost:3000/login' : 'https://tokstudy.com/login';
};
//axios instance
const instance: ToksHttpClient = axios.create({
baseURL: `${BASE_URL}`,
headers: { Authorization: authToken?.access },
});
//1. 요청 인터셉터
instance.interceptors.request.use(
function (config) {
if (config?.headers == null) {
throw new Error(`config.header is undefined`);
}
config.headers['Content-Type'] = 'application/json; charset=utf-8';
config.headers['Authorization'] = authToken?.access;
return config;
},
function (error) {
return Promise.reject(error);
}
);
사실상 에러는 response interceptors에서 처리하고 있다.
//2. 응답 인터셉터
instance.interceptors.response.use(
response => response.data,
async function (error) {
if (error?.status === 401) {
try {
const {
data: { refreshToken },
} = await axios.post('/api/v1/user/renew', authToken.refresh);
//refresh 유효한 경우 새롭게 accesstoken 설정
if (error?.config.headers === undefined) {
error.config.headers = {};
} else {
error.config.headers['Authorization'] = refreshToken;
//sessionStorage에 새 토큰 저장
sessionStorage.setItem('accessToken', refreshToken);
// 중단된 요청 새로운 토큰으로 재전송
const originalResponse = await axios.request(error.config);
return originalResponse.data.data;
}
} catch (err) {
redirectToLoginPage();
}
} else {
throw error;
}
}
);
export const http: ToksHttpClient = instance;
인증 구현을 처음 해보면서 고통스럽기도 했지만 그만큼 배운 것이 너무 많았다. 처음에 가장 어려웠던 것은 백과 토큰을 주고 받는 전체적인 플로우를 이해하는 것과 유저 케이스를 어떻게 나누어 로그인 분기처리를 어떻게 할 것인가를 결정하는 것이었다. 내가 작성한 로직이 모두 맞다고 할 수는 없겠지만 나와 같이 어려움을 겪는 사람이 또 있을 것 같아 로그인 작업을 하던 과정에서 고민했던 생각들과 과정을 자세하게 적어보았다. 글을 적으면서 또 다시 내가 생각했던 과정을 되짚어볼 수 있는 시간이었다. 분명 서비스가 확장되면 이 로그인 로직 또한 수정되어야 할 것이다. 그러나 앞으로는 훨씬 더 쉽고 좋은 방향으로 로그인 코드를 작성할 수 있을 것이라 믿기 때문에 기대가 된다 🤗
참고자료
https://axios-http.com/kr/docs/interceptors
https://dooopark.tistory.com/6
https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie