
내가 하고 있는 서비스는 SSO로그인만 지원하지만, SSO 로그인이 원활하지 않아서 테스트 할 때는 Token기반의 로그인을 사용하고 있다. 내가 처음부터 구현한 서비스가 아니라서 예전에 다른 사람이 구현해놓은 코드를 이번에 개선하게 되면서 공부한 내용을 정리해보려고 한다.
🧐로그인 방식에는 어떤 방식이 있을까
세션 방식
sequenceDiagram
participant c as Client
participant s as Server
participant d as DB
c->>s: 로그인 요청
s->>s: 세션 id 생성
s->>d: 세션 id와 유저 정보 저장
s->>c: 세션 id 전달
c->>s: 요청할 때 세션 id를 담아서 요청
- Client 가 서버에 로그인 요청
- 정상적인 요청인 경우, session id를 만들고, DB에 session id와 유저 정보를 저장
- Client에게 session id 전달
- 이후 client는 요청할 때마다 session id를 담아서 요청
- 서버는 session id를 가지고 유저를 구별
특징
- 사용자의 정보를 서버에서만 관리, Client는 오직 session Id만 저장
- 사용자 정보를 모두 서버에서 관리하기 때문에 외부에 노출되지 않지만, 세션 ID가 탈취당하면 무의미해짐
- 사용자가 많을 수록 DB에서 관리해야하는 세션의 수가 많아져 부하 가중
💳JWT(Json Web Token) 방식
- JWT는 사용자를 인증하고, 식별하기 위한 토큰 기반 인증 방식
- 토큰 자체에 사용자에 대한 일부 정보가 포함되어 있기 때문에 민감한 정보는 포함되면 안됨.
- 사용자에 대한 정보가 토큰에 담겨있고, 클라이언트에서 관리하기 때문에 DB 부하가 발생할 가능성이 낮음.
- 보안과 사용성을 위해서 Access token/Refresh token으로 나뉨
Access token
- API호출 할 때 실제로 사용하는 token
- 탈취되면 위험하기 때문에 보통 유효기간이 짧음
Refresh token
- 오직 Access token을 재발급하기 위한 token
- 상대적으로 유효기간이 길다
❔어디에 저장해야 할까?
이제 토큰을 어디에 저장해야할 지 생각해봐야한다. 토큰이 탈취당하면 악용될 수 있기 때문이다.
어디에 저장할지 정하기 전에, 먼저 어떤 위험들이 있는지에 대해 알아보자. 그래야지 어떻게 막을지 생각할 수 있기 때문이다.
취약점
XSS
발생 조건
- 토큰을 JS로 접근할 수 있는 공간에 저장(로컬 스토리지, Redux, JS 변수에 저장)
- XSS 취약점 존재 (=JS로 접근이 가능)
발생 과정
- XSS 취약점이 존재하는 곳에 악성 스크립트를 실행시켜 token값을 탈취
- 탈취한 token 값을 사용해 API 호출
CSRF
발생조건
- 브라우저가 자동으로 서버에게 쿠키 전송
- CSRF 방어 안되어있음.
발생 과정
- 사용자가 로그인 상태로 악성 사이트 방문
- 악성 사이트에서 우리 서버를 호출하는 코드를 심어서 사용자가 그 코드를 실행하게 되면(이미지 클릭이나 게시글 클릭과 같은 액션에 숨겨놓음), 마치 사용자가 우리 서버를 호출 하는 것처럼 할 수 있음
- 우리 서버에서는 사용자가 호출하는 것처럼 보이기 때문에 요청을 수락하게 됨
크게 정리하면, JS로 접근할 수 없는 곳이여야하고, 로그인한 사용자가 의도한 요청인지 확인을 해주어야 이 취약점들을 막을 수 있다.
저장 위치
로컬
로컬 스토리지/세션 스토리지
- 프론트에서 localStorage.setItem/getItem 함수를 통해 저장/조회할 수 있다.
- 누구나 확인할 수 있기 때문에 (개발자 도구 > Application) 노출될 가능성이 매우 높다
- 그렇기 때문에 XSS 공격에 취약하다
쿠키
- 자동으로 서버에 전송된다.
- 그렇기 때문에 CSRF 공격에 노출되면 자동으로 서버에 전송되어 인증이 되기 때문에 취약하다.
- 그러나 몇가지 옵션으로 방어할 수 있다.
- HttpOnly = 적용
- JS로 접근 불가 (document.cookie 불가능)
- XSS를 방지할 수 있지만, CSRF는 막을 수 없음
- Samesite
- 다른 사이트에서 오는 요청은 쿠키 보내지 않음
- CSRF 방어 가능
- Secure
- HTTPS 요청에서만 쿠키 전송
- HTTP인 경우에는 전송되지 않음
- 중간자 공격 방지(MITM)
- 공용 와이파이, 프록시 환경에서 쿠키 탈취 방지
- HttpOnly = 적용
Redux?
기존에 구현된 코드에서는 AccessToken을 Redux에서도 관리하고 있었다. 그래서 redux에서 관리하는건 괜찮을까 싶어서 찾아봤는데,,
결론은 보안에 취약하다
- 리덕스도 JS를 사용해서 접근할 수 있기 때문에 XSS 에 노출될 수 있다.
- 또한 새로고침을 해도 redux 상태를 유지하기 위해 redux persist를 사용하는데, 이렇게 되면 사실상 localStorage에 저장하는 것과 동일
하다고 한다.
✅ 내가 선택한 방식
공통 적용 사항(Silent Refresh )
-> 401 만료 되면 자동으로 /refresh 호출해서 새로운 access token과 refresh token을 발급해서 전달
Refresh 호출 타이밍
고민 사항🤔
- 요청전에 access 토큰의 여부나 만료일을 확인해서 유효하지 않다면, 그 때 /refresh 호출
→ 클라이언트 시간에 의존하기 때문에 서버와 시간이 다르면 오판할 수 있음 - 요청하고 나서 401을 받게 되면 그 때서야 /refresh 호출 (선택✅)
→ 제일 많이 사용하는 방식, 토큰 검증을 서버에서만 하기 때문에 명확한 역할 분리 - 아니면 발급 받고 나서 setTimeout 을 사용해서 만료되기 1분전에 재발급 요청하기
→ 이것 또한 timeout이 정확하다고 할 수 없음. 시간이 틀어질 수 있는 위험 존재
Access token 저장 위치
후보1 - 웹 내 메모리, private 변수
- 클로저 내부나 특정 모듈의 프라이빗 변수에 담겨 있다면, 변수 이름을 알아내어 접근하기 어려움
- closer 예시
// tokenStore.js export const tokenStore = (() => { let accessToken = null; // 🔒 완전 프라이빗 return { set(token) { accessToken = token; }, get() { return accessToken; }, clear() { accessToken = null; }, }; })(); - 사용자가 새로고침을 하게 되면 변수의 값이 사라지기 때문에 /refresh 요청하는 횟수가 많아짐
후보2 - local storage
- 새로고침을 해도 사라지지 않기 때문에 /refresh 횟수가 상대적으로 적음
- XSS 공격에 매우매우 취약함.
후보3 - cookie + http only
→ 보안적으로 제일 안전하나, 기존 코드가 모두 헤더에 Authorization 값에 토큰을 넣어서 전달하는 방식으로 구현되어있어서 코드 변경 범위가 커짐
선택
→ 클로저 방식으로 메모리에 저장하는 것이 적합하다고 판단.
XSS공격을 막을 수는 없지만, XSS 피해 반경을 줄일 수 있음
- Access token도 cookie로 보내는 건?우리 서비스도 Same site여서 cookie를 사용해도 되지만, 기존에 구현된 방식이 Bearer 로 구현되었기 때문에 한번에 너무 많은 부분을 변경하는 건 리스크가 크다고 생각하여 이부분은 그대로 유지하는 걸로 선택했다.
- 사실 프론트와 서버의 주소가 Same site이면 Access token과 refresh token 모두 cookie를 사용해서 보내도 괜찮다.
Refresh token 저장 위치
→ 탈취 당하지 않도록 쿠키에 저장하고 여러가지 옵션을 설정하기로 결정
- 쿠키를 사용하기 위해서는 withCredential 설정 필요 (FE, BE 둘 다 해당)
- cookie의 http only, secure 설정이 필요
- 왜?
- refresh 토큰은 상대적으로 유효기간이 길고, access token을 발급할 수 있는 key이기 때문에 XSS를 막기 위해 cookie + http only를 사용(JS로 접근할 수 없음)
- CSRF 공격을 막기 위해 sameSite(=Strict) 설정
- secure 설정으로 https 에서만 쿠키가 전송되도록 설정
환경에 따라 쿠키 설정하는 코드 🔽 - 왜?
const isProd = process.env.NODE_ENV === 'production';
res.cookie('refreshToken', token, {
httpOnly: true,
secure: isProd,
sameSite: isProd ? 'strict' : 'lax',
path: '/refresh',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
우선 우리 서비스는 Prod에서는 사용하지 않기 때문에 http only 옵션만 적용하기!
-> 하지만 이 옵션은 곧 변경되었다....
최종 결론
- Access token은 메모리 변수 관리하고, refrsh token은 쿠키로 관리한다.
- refresh token을 위해 서버에서 쿠키 설정을 해주어야 한다.
- Access token이 만료되면 클라이언트에서 알아서 /refresh를 호출해서 access token을 갱신한다.
- /refresh 호출을 했는데 refresh token이 만료되어 401 응답을 받으면 사용자는 로그아웃 된다.
'개발 공부 > Web' 카테고리의 다른 글
| [Web] WebRTC (0) | 2022.07.29 |
|---|---|
| [Web]SocketIO (0) | 2022.07.27 |
| [Web] WebSocket 정리 (0) | 2022.07.26 |
| 라디오 버튼을 이미지로 구현하기 [이전 블로그 게시글] (0) | 2022.02.16 |
| [정리] 웹 폰트란? [이전 블로그 게시글] (0) | 2022.02.16 |