개발을 처음 배우고 어느 정도 웹을 만들 수 있게 되면 한 번쯤 로그인 기능을 구현하게 됩니다.
아이디와 비밀번호를 입력하고 서버에 요청합니다.
정보가 일치하면 로그인에 성공하고, 토큰을 발급받아 저장합니다.
이후 요청마다 토큰을 함께 보내면 됩니다.
그럼 이제 대부분은 생각합니다.
ㅋㅋ 개발 별거 없노
실제로 로그인 기능을 동작하게 만드는 것만 놓고 보면 그렇게 어렵지 않습니다.
하지만 본격적으로 제대로 된 서비스를 구현하기 시작하면 달라집니다.
Auth는 구현하기 어려운 기능이 아니라,
신경쓸게 많지만 필요한 귀찮은 기능입니다.
가장 단순한 로그인부터 생각해 봅시다.
사용자가 아이디와 비밀번호를 입력합니다.
서버가 이를 확인하고 Access Token을 발급합니다.
ID / Password
↓
Server
↓
Access Token
프론트엔드는 이 토큰을 저장하고 API 요청마다 전달합니다.
여기까지만 보면 굉장히 간단합니다.
그런데 바로 다음 문제가 생깁니다.
토큰을 어디에 저장해야 할까요?
localStorage에 저장하면 구현하기 편합니다.
브라우저를 종료해도 데이터가 유지되고, js 딸깍으로 쉽게 가져올 수도 있습니다.
그런데 js에서 딸깍으로 가져올 수 있다는 것은 XSS 공격이 발생했을 때 공격자 역시 토큰에 접근할 수 있다는 의미입니다.
그렇다면 Cookie에 저장하면 될까요?
HttpOnly를 사용하면 js에서 접근할 수 없으니 조금 더 안전해 보입니다.
그런데 Cookie는 조건에 따라 브라우저가 요청에 자동으로 포함합니다.
js의 직접적인 접근은 막았지만, 이제는 CSRF를 생각해야 합니다.
SameSite, Secure, Domain, Path 같은 HTTP 설정들도 고려해야 하고요
뭔가 굉장히 귀찮아지기 시작합니다.
그렇다면 Access Token의 유효기간을 짧게 설정하면 어떨까요?
토큰이 탈취되더라도 사용할 수 있는 시간이 줄어듭니다.
좋은 방법입니다.
대신 새로운 문제가 생깁니다.
토큰이 만료될 때마다 사용자를 로그아웃시킬 건가요?
30분짜리 Access Token을 사용한다고 해서 사용자에게 30분마다 다시 로그인하라고 할 수는 없습니다.
그래서 Refresh Token이 등장합니다.
Access Token 만료
↓
Refresh Token 확인
↓
새로운 Access Token 발급
이러면 해결된거 같죠?
이거는 어떻게 답변하실 건가요?
분명 Refresh Token만 도입하면 해결됐다 생각하셨을텐데 아닙니다. 걍 급한불 하나 끈겁니다
프론트엔드에서도 문제가 생깁니다.
API 요청을 보냈는데 Access Token이 만료되어 401 Unauthorized가 반환됐다고 해봅시다.
그러면 Refresh Token을 이용해 Access Token을 갱신하고 기존 요청을 다시 보내면 됩니다.
그런데 같은 순간 API 요청이 5개 발생했다면 어떻게 될까요?
Request A ─> 401
Request B -> 401
Request C -> 401
Request D -> 401
Request E -> 401
각 요청이 알아서 토큰을 갱신한다면 Refresh 요청 역시 5번 발생할 수 있습니다.
Rotation을 사용하고 있다면 상황은 더 복잡해질 수 있습니다.
첫 번째 요청에서 Refresh Token이 교체됐는데 다른 요청들이 이전 Refresh Token을 사용하려 할 수도 있습니다.
결국 하나의 Refresh 요청이 진행되는 동안 나머지 요청을 기다리게 만들거나, 갱신 작업 자체를 하나의 Promise로 관리하는 등의 처리가 필요해집니다.
단순히 Axios Interceptor에 401이면 refresh 몇 줄 추가하는 것으로 끝나는 문제가 아닙니다.
Auth를 구현하면서 또 하나 헷갈리기 쉬운 것이 있습니다.
Authentication과 Authorization입니다.
Authentication은
"당신이 누구인가?"
를 확인하는 과정입니다.
Authorization은
"당신이 이것을 할 수 있는가?"
를 판단하는 과정입니다.
예를 들어 관리자 페이지가 있다고 해봅시다.
프론트엔드에서 사용자의 권한을 확인해서 관리자 메뉴를 숨겼습니다.
// 관리자일 경우
role === "admin"
그러면 일반 사용자는 관리자 기능을 사용할 수 없을까요?
아닙니다.
UI를 숨긴 것과 권한을 제한한 것은 완전히 다른 문제입니다.
사용자가 직접 API를 호출할 수도 있기 때문에 서버에서도 반드시 권한을 검증해야 합니다.
그리고 서비스가 커지면 단순한 admin / user만으로 부족해질 수도 있습니다.
Auth는 자연스럽게 권한 설계의 문제로 이어집니다.
그렇다면 직접 로그인을 구현하지 않고 Google, GitHub, Discord 같은 OAuth Provider를 사용하면 어떨까요?
비밀번호를 직접 관리하지 않아도 되니 확실히 편해집니다.
하지만 Auth 자체가 사라지는 것은 아닙니다.
OAuth 로그인에 성공한 사용자를 우리 서비스에서는 어떻게 식별할 것인지 결정해야 합니다.
그리고 OAuth 과정 자체에서도 state, PKCE, Redirect URI 같은 보안 요소들을 이해해야 합니다.
결국 OAuth는 Auth를 없애주는 기술이라기보다,
Auth에서 우리가 직접 책임져야 할 영역을 줄여주는 기술에 가깝습니다.
여기까지 공부하고 나면 이런 생각이 들 수 있습니다.
"그래서 가장 안전한 Auth 구조가 뭔데?"
문제는 하나의 정답이 없다는 것입니다.
Access Token을 Cookie에 저장하는 것도,
메모리에 저장하는 것도,
BFF 구조를 사용하는 것도,
Session 기반 인증을 사용하는 것도
각각 이유와 Trade-off가 있습니다.
서비스의 구조와 요구사항에 따라 선택은 달라집니다.
중요한 것은 특정 방식을 외워서 사용하는 것이 아니라,
왜 이 방식을 선택했는지 설명할 수 있는가라고 생각합니다.
이 질문들에 답할 수 있어야 비로소 Auth를 구현했다고 말할 수 있지 않을까요?
Auth는 웹 개발을 배우면서 굉장히 일찍 만나는 기능입니다.
회원가입하고,
로그인하고,
토큰을 저장하고,
로그인한 사용자만 특정 페이지에 접근하도록 만드는 것.
튜토리얼만 따라가도 충분히 구현할 수 있습니다.
그래서 Auth는 쉬워 보입니다.
저 역시 개발을 막 배웠을때는 그렇게 생각했습니다.
하지만 조금만 깊게 들어가면 Auth는 보안, 브라우저, HTTP, 서버, DB 그리고 서비스 정책까지 연결됩니다.
로그인을 만드는 것은 쉽습니다.
하지만 누가 누구인지 안전하게 증명하고, 그 사람이 무엇을 할 수 있는지를 끝까지 보장하는 것은 어렵습니다.
그래서 저는 Auth를 이렇게 생각합니다.
가장 먼저 배우는 기능 중 하나지만, 끝까지 공부하게 되는 기능.
Auth가 쉽다고 생각한다면, 어쩌면 아직 로그인만 구현해 본 것일지도 모릅니다.
프론트 엔드는 강평 ㄷㄷ