로그인을 구성하는 방식은 크게 쿠키와 세션을 사용하는 방식과 토큰 기반의 인증 방식으로 나뉜다.
사용자의 인증 정보가 세션 저장소에 저장되는 방식
사용자가 로그인을 하면, 해당 인증 정보를 서버의 세션 저장소에 저장하고, 사용자에게는 세션ID를 발급하여 브라우저에 쿠키 형태로 저장한다.
인증 절차를 마친 후 클라이언트는 요청마다 Cookie 헤더에 세션ID를 서버로 전송한다. 그러면 서버가 세션ID를 해당하는 세션 정보가 세션 저장소에 존재하는지 확인하여 해당 클라이언트를 인증된 클라이언트로 판단한다.

(출처:https://klloo.github.io/session-jwt/)
토큰 기반 인증은 인증 정보를 클라이언트가 직접 가지고 있는 방식이다. 이때 인증 정보는 토큰의 형태로 브라우저의 로컬 스토리지 또는 쿠키에 저장된다.

(출처: https://klloo.github.io/session-jwt/)
사용자가 아이디와 비밀번호를 입력하고 서버로 요청을 보내면 서버는 db에서 회원을 조회하고 등록된 사용자라면 토큰을 생성하여 클라이언트에 토큰과 함께 응답을 보낸다. 응답이 성공적으로 왔다면 클라이언트는 이후 인증이 필요한 모든 요청에 해당 토큰을 Authorization헤더에 실어 보낸다. 그러면 서버는 요청 헤더로 전달받은 토큰을 검증하고 권한이 있는 사용자라고 판단하면 요청된 데이터를 응답해 주는 것이다.
하지만 이 때 한가지 주의 사항이 있다. 토큰은 발급 시 유효기간 동안 되돌릴 수 없고 탈취당할 경우 토큰에는 인증 정보가 모두 담겨있으므로 위험하다. 이를 해결하기 위해 서버에서는 Access token과 Refresh token 두개의 토큰을 발행한다.
Access Token: 클라이언트 쪽에서 요청 헤더에 담아 보내는 토큰
Refresh Token: Access Token이 만료되거나 잘못된 토큰일 경우 Refresh Token을 사용해 유저를 검증한다. 만약 Refresh Token을 검증하여 인증된 유저일 경우 Access Token을 재발행 해준다.
결론적으로 Access Token의 유효기간을 짧게 설정하고 Refresh Token의 유효기간을 길게 설정하여 Access Token 만료시 Refresh Token을 검증하여 Access Token을 재발행 해주는 것으로 보안적으로 안전하게 유저 인증을 관리 할 수 있다.
JWT(Json Web Token)는 말그대로 웹에서 사용되는 JSON 형식의 토큰에 대한 표준 규경이다.
하나의 JWT 토큰은 헤더(header)와 페이로드(payload), 서명(signature)의 세 부분으로 이루어지며 각 구형이 . 기호로 구분된다.
<헤더>.<페이로드>.<서명>
헤더에는 토큰의 유형과 서명 알고리즘에 명시되고, 페이로드에는 소위 claim이라고도 불리는 사용자의 인증/인가 정보가, 서명에는 헤더와 페이로드가 비밀키로 서명되어 저장된다.
JWT 토큰은 네트워크로 전송되야 하기 때문에 공간을 적게 차지하는 것이 유리하다. 그래서 JSON 형식으로 데이터를 저장할 때 키를 3글자로 줄이는 독특한 관행이 있다.
토큰의 저장 위치를 고민하는 이유는 바로 '보안'이다. 웹 어플리케이션 보안 취약점 중 유저 인증에서 보편적으로 이용되는 취약점은 크게 XSS와 CSRF 공격 두 가지가 있다.
보통 게시판에 악성 스크립트가 담긴 글을 올리는 형태로 이루어지며, 스크립트가 포함된 글을 열어보게 되면 브라우저에서 원치 않는 스크립트가 실행되는 방식이다.
이것을 통해 사용자의 쿠키 정보를 탈취하거나, 사용자 비밀번호를 변경하는 API를 호출하는 행위를 할 수 있다.
API를 요청할 수 있는 클라이언트 도메인이 누구인지 서버에서 통제하고 있지 않다면 CSRF가 가능한데, 이때 공격자가 클라이언트에 저장된 사용자 인증정보를 서버에 보낼 수 있다면, 제대로 로그인한 것처럼 사용자의 정보를 변경하거나 사용자만 가능한 액션들을 수행할 수 있다.
브라우저 저장소에 저장하는 방식으로 자바스크립트 내 글로벌 변수로 읽기 및 쓰기 접근이 가능하다.
자바스크립트로 접근이 가능하기 때문에 Refresh Token 또는 Access Token을 저장해두면 XSS 취약점을 통해 그 안에 담긴 값을 이용해 API 요청을 위조할 수 있다.
브라우저에 쿠키로 저장되고 클라이언트가 HTTP 요청을 보낼 때마다 자동으로 쿠키가 서버에 전송된다. 자바스크립트 내 글로벌 변수로 읽기 및 쓰기 접근이 가능하다.
localStorage 저장 방식과 마찬가지로 Refresh Token, Access Token을 쿠키에 저장해두면 XSS 취약점이 있을 때 담긴 값들을 불러오거나, API 콜을 보내면 쿠키에 담긴 값들이 함께 전송되어 로그인한척 공격을 수행할 수 있게 된다.
쿠키에 Access Token을 저장해 인증에 이용하는 구조에 CSRF 취약점이 있다면 인증 정보가 쿠키에 담겨 서버로 보내지며 공격자는 유저 권한으로 정보를 가져올 수 있다.
쿠키에 Refresh Token만 저장하고 새로운 Access Token을 받아와 인증에 이용하는 구조에서는 CSRF 취약점 공격을 방어할 수 있다. CSRF는 정상 사용자가 img 태그를 보거나 link를 클릭하도록 유도하여 사용자 대신 request를 보내는 방법이다. Refresh Token은 쿠키로 저장되어 있기 때문에 CSRF를 통해 Access Token을 사용자 대신 요청할 순 있지만 response는 사용자가 받기 때문에 공격자는 이 Access Token을 얻지 못하기 때문이다.
브라우저에 쿠키로 저장되는 건 같지만, 자바스크립트 내에서 접근이 불가능합니다. secure을 적용하면 https 접속에서만 동작하게 된다.
httpOnly 쿠키 방식으로 저장된 정보는 자바스크립트 내에서 접근이 불가능하므로 XSS 취약점 공격으로 담긴 값을 불러올 수 없다.
쿠키 저장 방식과 같은 이유로 Access Token은 저장하면 안된다.
httpOnly 쿠키에 담긴 값에 접근할 수는 없지만 XSS 취약점을 노려 API 콜을 요청하면 httpOnly 쿠키에 담긴 값들도 함께 보내져 유저인 척 정보를 빼오거나 액션을 수행할 수 있다.
어떤 저장 방식을 택해도 XSS 취약점이 있다면 보안 이슈가 존재하게 된다. 그러므로 유저 정보 저장 방식을 바꾸는 것만으로는 방어할 수 없고, 클라이언트와 서버에서 추가적으로 XSS 방어 처리를 하는 것이 필수이다.
그래도 최선의 방법을 찾아보자면 Refresh Token만을 secure httpOnly 쿠키에 저장해 CSRF 공격을 방어하고 Access Token은 웹 어플리케이션 내 로컬 변수에 저장하여 API를 요청할 때 Authorization 헤더에 넣어 보내주는 것이 좋다. 하지만 XSS 취약점을 이용한 API 요청 공격은 무방비하니 XSS 자체를 막기 위해 클라이언트와 서버에서 추가적으로 방어할 필요가 있다.