📕 목차
1. Authentication & Authorization
2. How to control Access Token & Refresh Token
3. RTR(Refresh Token Rotation)
4. How to store Refresh Token in redis? (advanced RTR)
5. Auto Refresh Strategy
1. Authentication & Authorization
JWT가 뭔지는 지난 포스팅들에서 하도 많이 언급을 했으므로 패스.
모든 내용을 다 쓰려니 너무 포스팅이 방대해져서, redis를 사용하는 파트는 추후 따로 작성할 예정..
이긴 한데, 다른 블로그 검색해도 나오는 내용들이니 잘 찾아보면 적용할 수 있을 것이다.
- 인증(Authentication) 거부 : "넌 우리 서비스를 사용할 수 없어. 로그인을 해"
- 인가(Authorization) 거부 : "그래, 네가 누군지는 알겠지만 지금 요청한 자원에 접근할 권한은 없어"
이 두 가지를 혼동하는 사람이 많은데, 일반적으로 인증 과정을 거친 후에 인가 과정을 거친다고 보면 된다.
JWT를 사용하는데 인증 과정에서 문제가 생긴다면 토큰이 유효하지 않다는 등의 이유로 사용자 식별 자체가 불가능한 상태고,
토큰은 유효하지만 접근 권한이 없는 resource에 접근하려고 하면 인가 과정에서 거부당하는 것이다.
2. How to control Access Token & Refresh Token
token을 하나로만 다루자니, 이거 하나 탈취 당하면 해당 유저가 접근 가능한 모든 리소스를 조회할 수 있다는 보안 취약점으로 작용하게 된다.
그래서 자원에 접근하기 위한 access token는 유효 기간을 짧게 두고, access token을 재발급 받기 위한 refresh token은 있어 봐야 access token 재발급 받는 것 말곤 못 하니까 유효 기간을 길게 둔다는데 여기서 의문점이 발생한다.
아니, 어차피 refresh token으로 access token이 재발급 가능할 거 같으면 이전 방식이랑 뭐가 다른 건데?
대부분 블로그에서 무슨 암기 공식 마냥 이렇게 적어놓고 치워버려서 혼란스러울 수 있지만, 정확히 따지자면 둘을 분리하고 보관하는 장소가 다르다.
👇 기초적인 보안 상식. XSS, CSRF
⚔️ Cross-Site Scripting(XSS) 공격
- 공격자가 클라이언트 코드에 악의적인 스크립트를 주입하는 방식
- 사용자가 특정 웹 사이트를 신용하는 점을 노림. (공격 대상: 클라이언트)
- 브라우저는 일반적으로 스크립트의 악의성을 식별할 수 없으므로, 웹 애플리케이션 유효성 검사가 취약한 부분을 공격자의 스크립트로 채워넣는다.
- 공격자가 클라이언트의 쿠키와 세션 토큰, 사이트의 보안 정보들에 접근 가능해진다.
- 자바스크립트를 사용하는 공격이 가장 많다. (가장 단순하고 기초적이지만, 많은 웹 사이트가 XSS에 대한 방어 조치를 해두지 않아씨 때문)
- Stored XSS : 스크립트가 서버에 저장되어 실행되는 방식 (가장 일반적이고 가장 위)
- Reflected XSS : URL parameter에 스크립트를 넣어 서버에 저장하지 않고, 그 즉시 스크립트를 만드는 방식
🛡️ XSS 공격 예방
- 유저가 입력값을 제한하여 input을 예측 범위 안에 놓는다면, 미리 입력될 데이터 값을 통제 가능하다.
- 악성 HTML을 필터링 해주는 라이브러리를 사용한다.
- DOM 상의 텍스트를 읽을 때 innerHTML 사용을 지양하고, textContent 등의 메서드로 대체한다.
- 중요한 정보는 서버에서만 다루고 쿠키에 담지 않아야 한다.
- httponly 속성을 on으로 설정 : document.cookie로 쿠키에 접근하는 것을 막는다.
- 정보 암호화
⚔️ Cross-Site Request Forgery (CSRF 혹은 XSRF) 공격
- 공격자가 사용자 동의 없이 브라우저로 하여금 서버에 어떠한 요청을 보내게 하는 방식
- 특정 웹 사이트가 사용자의 웹 브라우저를 신용하는 상태를 노림 (공격 대상: 서버)
- 사용자가 로그인한 상태로 위조된 공격 코드가 삽입된 요청을 날리면, 서버는 위조된 공격이 믿을 수 있는 사용자로부터 발송된 것으로 판단하게 되어 공격에 노출된다.
- 두 가지 조건이 만족된 상태에서 동작한다. (공격자가 클라이언트 PC를 감염하는 방식이 아님)
- 클라이언트가 위조 요청을 전송하는 서비스에 로그인한 상태여야 한다.
- 클라이언트는 공격자가 만든 피싱 사이트에 접속해야 한다.
🛡️CSRF 공격 예방
- Referror 검증
- CSRF Token
- Double Submit Cookie 검증
1️⃣ 자바스크립트 private 변수로 저장
- 보통 access token 관리 방법
- XSS, CSRF 공격에 당항 위험이 없다.
- 새로 고침 혹은 SPA가 아닌 애플리케이션의 경우 페이지 이동 시 access token이 증발한다. (변수가 초기화 되니까)
- 따라서, refresh token 만으로 access token을 재발급 받는 API를 제공해야 한다.
2️⃣ Local storage에 저장
- CSRF 공격에 안전하다.
- JS 코드에 의해 헤더에 담기므로 공격자가 XSS를 뚫어야 정상 사용자인 척 할 수 있다.
- XSS 공격에 취약하다
- 공격자가 local storage에 접근하는 JS 코드 한 줄만 주입하면, 다른 유저의 local storage를 자유롭게 접근할 수 있다.
3️⃣ Cookie(HTTP only)
- XSS 공격으로부터 local storage보다 안전하다.
- httpOnly를 true로 설정하면 JS에서 접근이 불가능하다. (XSS 공격으로 쿠키 정보 탈취 불가)
- 하지만 위조된 request를 보냄으로써 충분히 뚫을 수는 있다. (귀찮아서 그렇지)
- CSRF 공격에 취약하다.
- Cookie는 자동으로 http request header에 담기 때문에, 공격자가 request url만 알면 사용자가 관련 link를 클릭하도록 유도하여 request를 위조할 수 있다.
Native App 환경이라면 더 많은 선택지가 존재할 것이다.
예를 들어, iOS라면 key chain 등등.
🟡 AT와 RT의 secret key
- JWT signature을 통한 유효성 검사를 할 때, 반드시 server만 알고 있어야 하는 key가 있다.
- 수많은 블로그가 AT와 RT의 secret key를 공유하고 있는데, 용도가 다른 jwt의 secret key는 모두 분리하는 것이 맞다.
3. RTR(Refresh Token Rotation)
- Access Token와 Refresh Token 둘 중 하나, 혹은 둘 다 탈취되었다면?
- 보통 AT는 짧은 유효 기간, RT는 긴 유효 기간을 갖는다.
- AT는 그렇다쳐도, RT는 한 번 탈취되면 거의 1~2주 정도의 유효기간을 갖는 기간동안 해커가 계속 사용 가능하다.
- Server는 stateless이므로 탈취 여부를 알 수 없다. 따라서, Token 만료 시간까지 해커의 공격을 차단할 방법이 없다.
- 사용자로부터 도용 신고가 들어온 후에나 log를 탐색해서 수동으로 차단할 수 있다.
- RTR 기법은 RT1를 사용해 AT2를 재발급할 때, RT2도 재발급한다.
- AT 뿐 아니라, RT도 재발급 대상으로 취급한다.
- 블랙리스트 기법을 사용하면 RT의 생명 주기가 너무 길어서 메모리에 부담이 되므로, 새로 발급한 RT2 정보와 유저 정보(ex. userId 혹은 email 같은 식별정보)를 redis에 기록한다.
- 만약 특정 사용자가 잘못된 refresh token으로 refresh를 요청하면, (해당 요청이 정상 사용자인지 해커인지 구분할 수는 없지만)RT가 탈취되었다고 판단하여 해당 refresh token을 폐기처분하고 재로그인 하도록 만든다.
4. How to store refresh token in redis? (advanced RTR)
RT를 Redis에 저장하는 것까진 좋은데, "어떻게 저장할 것인가?"에 대한 이야기를 다뤄보려고 한다.
이거나 저거나 별 다를 바 없어 보이겠지만, 탈취 시나리오를 떠올려보면서 나름대로 고민해본 내용이다.
1️⃣ key : value = RT : userId
- 처음에 적용한 방식. 사실상 value의 userId는 더미값이나 다름없다.
- RT은 어차피 Server가 내려주는 것이므로, 유효한 토큰이라면 반드시 memory에 존재한다.
- RT의 userId 또한 redis에 해당 RT가 존재한다면 언제나 참이 된다.
- RT가 탈취되고, 정상 유저가 먼저 재발급 받은 경우
- 가장 이상적인 시나리오. 해커가 접근하지 못 한다.
- RT가 탈취되고, 해커가 먼저 재발급 받은 경우
- 해커가 먼저 발급 받으면, 정상 유저는 재로그인 해야 한다. 문제는 해커는 해커대로 재발급 받은 RT를 계속 사용하므로 하나의 유저에 대해, 여러 개의 RT가 존재한다.
(redis의 hash 타입으로 저장해도 key의 중복을 허용 안 하지, value는 중복을 허용하므로) - RT가 탈취당하면, 한명의 유저에 대해 탈취당한 인원수만큼의 RT가 발급된다.
- 해커가 먼저 발급 받으면, 정상 유저는 재로그인 해야 한다. 문제는 해커는 해커대로 재발급 받은 RT를 계속 사용하므로 하나의 유저에 대해, 여러 개의 RT가 존재한다.
2️⃣ key : value = userId : RT
- key를 PK 값으로 바꾸는 것만으로도 (1)의 이슈를 해결할 수 있다.
- 하나의 유저에 대해, 반드시 하나의 RT만 존재하도록 보장할 수 있다.
- 공격이 들어와도 판단 가능해진다.
- 정상 사용자와 해커 둘 중 누가 먼저 재발급을 하더라도, pk번에 잘못된 RT 요청이 들어오면 반드시 탈취되었다고 판단한다.
- Redis에서 해당 pk번 user의 RT 정보를 제거하고, AT가 같이 들어왔다면 함께 Black List에 등록한다.
그래서 나는 key, value 쌍을 각각 pk, RT를 넣도록 리팩토링 하는 과정을 거쳤었다.
물론, 이렇게 해도 AT가 탈취된 시나리오에 대해서는 대응하지 못 한다.
AT까지 대응하려면 해당 정보도 Redis에 보관해야 하는데, 그럼 Session하고 뭐가 다른가.
이미 RT를 보관하는 것만으로도 Stateless에 위배되는 상황이다.
5. Auto Refresh Strategy
이 방식은 위 이미지처럼 AT를 재발급하는 시나리오의 불필요한 Network 통신 횟수를 줄여보려고 떠올린 아이디어긴 한데,
하다보니 예외 처리할 것도 많고 여전히 예상치 못 한 보안 구멍이 있다고 생각되어서 관심 있는 사람만 적용하면 된다.
물론 적용하면 나와 똑같은 고뇌를 품게 되겠지..
- AT가 Refresh 되는 요청은 오직 `만료된 AT & 유효한 RT`가 들어있을 때만 존재한다.
- 기존 방식에선 RT가 유효하건 아니건, 만료된 AT라면 반드시 refresh 경로로 보내봐야 알 수 있다.
- 요청이 들어왔을 때, 내부적으로 유효한 RT가 존재한다면 곧바로 AT, RT를 재발급하고 기존 요청까지 처리한다.
- Client는 새로고침으로 인해 AT가 날아간 경우를 제외하고, `만료된 AT & 유효한 RT` 시나리오에서 기존 요청과 재발급된 AT를 받기만 하면 된다.
⚠️ 이 문제의 문제점
- Client가 Server 측에서 재발급 해준 AT를 안 받고, 그대로 요청을 해도 그대로 RTR 처리가 된다.
- RT를 cookie에 담아버려서 자동으로 헤더에 추가되므로, refresh되어도 언제나 유효한 상태가 된다.
- AT1이 만료되어서 유효한 AT2를 만들어줬는데, AT1을 그대로 보내면 AT3가 재발급된다..🥲
- 만약 Client의 AT, RT를 Controller 계층에서 조회할 때 문제가 된다.
- request에는 만료된 AT가 담겨있는데, Server가 intercepter에서 재발급한 AT, RT를 response에 담아버렸으니 둘 다 확인해야 한다.
처음 고안했을 때는 천재적인 발상이라고 생각했는데, 프로젝트가 진행될 수록 문제점이 많다는 걸 알 수 있다.
Client를 정말 신뢰할 수 있는 경우를 제외하고는 기존 방식대로 하는 게 낫다고 생각한다.