JWT(JSON Web Token)의 인증/인가 체계

김준기·2024년 2월 25일

✅ JWT란?


헤더(header), 페이로드(payload), 서명(signature) 세 파트가 점(.)으로 구분되어 있으며, 헤더(header)는 토큰의 타입과 서명 알고리즘의 정보가 담겨있습니다.
페이로드(payload)는 실제로 사용될 데이터들이 담기고, 서명(signature)은 토큰이 유효한 토큰인지 검증하는 문자열입니다.

JWT는 JWS와 JWE라는 두 가지 구현체를 가지고 있으며 JWS는 비밀키 알고리즘을, JWE는 공개키 알고리즘을 이용하여 서명합니다.
본 게시글에서는 JWS를 응용한 인증과 인가 체계에 대해 작성하였습니다.

인증(Authentication) 절차

✅ 인증이란?

사용자의 신원을 검증하는 프로세스로 비밀번호, 일회용 핀, 인증 앱, 생체인식등 신원을 증명하는 기타 정보를 확인하여 이루어집니다.

아래는 브라우저에서 일반적인 로그인 환경(id, password)의 인증 절차입니다.

  1. 사용자가 로그인 페이지를 요청한다.
  2. 서버는 로그인 페이지와 함께 특정한 토큰을 사용자의 쿠키에 저장한다.
  3. 사용자는 자신의 ID와 비밀번호를 서버에 보낸다.
  4. 서버는 보내온 정보가 올바른지 검증한다.
  5. 검증이 완료되면, 서버는 새로운 토큰을 사용자에게 보낸다.

이제 각 단계를 좀 더 자세히 살펴보겠습니다.

로그인 토큰의 생성

csrf_login_token과 login_jwt_access_token은 로그인 절차에서 중요한 역할을 하는 토큰입니다.

  • csrf_login_token은 UUID v4로 설정된 고유한 값입니다.
  • login_jwt_access_token은 csrf_login_token값을 payload에 csrf라는 클레임(Claim)으로 갖는 jwt입니다.

토큰의 전달

사용자가 로그인 페이지를 요청하면, 서버는 다음과 같이 토큰을 전달합니다.

  • 로그인 페이지에 csrf_login_token과 login_jwt_access_token을 쿠키에 담아서 응답합니다.
  • csrf_login_token은 HTTP Only를 false로 설정하며, login_jwt_access_token은 HTTP Only를 true로 설정합니다.

로그인 요청의 검증

사용자가 로그인 정보(ID와 비밀번호)를 서버에 보내면, 서버는 다음과 같이 검증합니다.

  • HTTP 헤더의 X-CSRF-Token 키 값을 csrf_login_token으로 설정해 보냅니다.
  • 서버는 쿠키에 있는 login_jwt_access_token 토큰이 유효한지 검증하고, payload에 저장된 CSRF 토큰과 X-CSRF-Token에 저장된 토큰이 동일한지 확인합니다.

토큰의 발급

데이터베이스에 저장된 ID와 비밀번호를 서버가 비교하여 사용자의 인증을 진행합니다. 이 인증 과정이 성공적으로 완료되면, 서버는 Refresh Token과 Access Token을 생성해 사용자의 쿠키에 저장하게 됩니다. 또한, 이 과정에서 이미지에는 나타나 있지 않지만, csrf 확인을 위한 토큰 두 개도 쿠키에 저장되어, 실제로는 총 4개의 토큰이 사용자의 쿠키에 저장되게 됩니다.

  • Access Token과 Refresh Token을 쿠키에 저장하는 이유는 HTTP Only 속성을 true로 설정할 경우 XSS 공격을 막을 수 있기 때문입니다.
  • 또한, Refresh Token을 생성할 때는 DB의 Session Table에도 동시에 추가해야 합니다. 이렇게 하면 서버에서 로그아웃 처리를 보다 효과적으로 할 수 있고, JWT의 Stateless 특성을 덜 침해하게 됩니다.

✅ Access Token?

사용자가 자신을 증명하고, 서비스의 다양한 기능에 접근할 수 있는 권한을 부여받는 데 사용됩니다.
Access Token이 유효한 동안에는, 이 토큰을 이용해 서버에 요청을 보낼 수 있습니다. 만약 Access Token이 만료되거나 잘못된 경우, 서버는 요청을 거부하고 사용자는 다시 로그인하거나 새로운 토큰을 발급받아야 합니다.

✅ Refresh Token?

Access Token이 만료된 후에도 사용자가 계속해서 시스템에 접근할 수 있도록 돕는 토큰입니다.
Access Token의 유효 기간이 끝나면, 사용자는 다시 로그인해야하는 번거로움이 있습니다. 이런 불편함을 줄이기 위해 Refresh Token이 도입되었습니다.
Refresh Token은 일반적으로 Access Token보다 훨씬 긴 유효 기간을 갖고 있습니다. Access Token이 만료되면, 사용자는 이 Refresh Token을 서버에 보내 새로운 Access Token을 발급받습니다. 이 과정은 일반적으로 사용자가 명시적으로 로그아웃하지 않는 한 백그라운드에서 자동으로 이루어집니다.
이렇게 하면 사용자는 서비스 사용 중 로그인 세션이 유지되며, 서비스 사용에 끊김 없이 이어갈 수 있습니다. 하지만 보안상의 이유로, Refresh Token도 일정 시간 후에는 만료되며, 그 경우에는 사용자가 다시 로그인해야 합니다.

✅ xss란?

XSS는 웹 사이트의 관리자가 아닌, 악의적인 목적을 가진 제 3자가 악성 스크립트를 삽입하여 웹사이트를 조작하는 취약점을 의미합니다. 이를 통해, 의도치 않은 명령을 실행시키거나 사용자의 세션 정보를 탈취하는 등의 행위가 가능해집니다.

이러한 XSS 공격을 예방하는 한 가지 방법은 'HTTP Only' 속성을 활용하는 것입니다. 이 속성을 true로 설정하고 데이터를 쿠키에 저장하면, 스크립트를 통해 쿠키에 접근하는 것을 방지할 수 있습니다. 이렇게 함으로써, XSS 공격으로부터 웹사이트를 보호할 수 있습니다.

✅ 로그인 페이지에서 CSRF 공격: 어떻게 대응할까요?

일반적으로, CSRF(Cross-Site Request Forgery) 공격의 대상은 이미 로그인된 사용자가 원하지 않는 요청을 보내게 만드는 상황입니다. 그러나 로그인 CSRF 공격은 상황이 약간 다릅니다. 이 경우, 사용자가 공격자가 제어하는 계정으로 로그인하게 만들어, 본래 그들이 접근하려고 했던 중요 정보를 공격자에게 노출시키게 됩니다.
CSRF 공격을 방어하는 방법 중 하나로, 이 게시글에서는 이중 제출 쿠키(Double Submit Cookies) 전략을 사용하였습니다. 이 방법은 csrf_token이라는 랜덤한 토큰을 생성하고, 이를 JWT(JSON Web Tokens)에 담아서 HTTP Only 속성을 부여하는 것입니다. 또한, 이 csrf_token을 HTTP Only를 해제한 상태로 쿠키에 저장합니다.
로그인 요청을 할 때, 요청 헤더에 X-CSRF-Token을 추가하고, 이의 값으로 csrf_token을 설정합니다. 이후 요청이 서버에 도달하면, 서버는 JWT에 저장된 csrf_token과 헤더에 담긴 csrf_token을 비교합니다. 두 토큰이 일치하면 로그인 프로세스를 진행하게 됩니다.

인가(Authorization) 절차

✅ 인가란?

해당 사용자가 수행하려는 작업에 대한 권한이 있는지 확인하는 과정입니다. 예를 들어, 어떤 웹사이트에서 로그인(인증) 후에 사용자가 관리자 페이지에 접근하려 할 때, 그 사용자가 관리자 권한을 가진 사용자인지를 확인(인가)하는 것이 필요합니다.

Get 요청에서의 인가


위의 그림은 GET 요청을 통한 인증 절차의 흐름을 나타냅니다.
1. 사용자는 요청을 보낼 때, Access Token을 쿠키에 포함시켜 전송합니다.
2. 이 요청을 받은 서버는 다음으로 Access Token의 유효성과 무결성을 확인합니다.
3. 만약 Access Token이 유효하다면, 서버는 데이터베이스에서 요청받은 데이터를 찾아 사용자에게 제공합니다.

Post 요청에서의 인가

위의 그림은 POST 요청을 통한 인증 절차의 흐름을 나타냅니다.
1. 사용자는 요청을 보낼 때, Access Token을 쿠키에 포함시키고, 쿠키에 저장되어 있는 csrf access token을 X-CSRF-Token 헤더에 담어 요청합니다.
2. 요청을 받은 서버는 Access Token의 유효성과 무결성을 확인하고 Access Token에 저장된 csrf token의 값과 X-CSRF-Token의 값이 같은지 확인합니다.
3. 검증이 성공적이라면 db에 요청받은 값을 추가합니다.
4. 요청의 성공 유무를 사용자에게 제공합니다.

Access Token의 재발급

Access Token은 보안 위협이 있을 수 있기 때문에 유지 기간이 짧습니다. 때문에 Refresh Token을 이용하여 Access Token을 주기적으로 갱신해야 합니다. 아래는 이러한 과정을 통한 절차의 흐름을 보여줍니다.

1. 사용자가 요청을 보내지만, Access Token이 만료되었다는 응답을 받습니다.
2. 이때, 사용자는 Refresh Token을 쿠키에 넣어 서버에 전달하고, 동시에 X-CSRF-Token 헤더에 csrf refresh token을 담아 Access Token 발행 요청을 보냅니다.
3. 서버는 이 요청을 받아 Refresh Token의 유효성과 무결성을 확인합니다. 그리고 Refresh Token에 저장된 csrf token 값과 X-CSRF-Token 헤더의 값이 일치하는지 확인합니다.
4. 또한, 서버는 데이터베이스에서 해당 Refresh Token이 세션 테이블에 저장되어 있는지도 확인합니다.
5. 모든 검증이 끝나면, 서버는 새로운 Access Token과 Refresh Token을 생성하며, 생성된 Refresh Token은 데이터베이스에 갱신합니다.
6. 마지막으로, 생성된 Access Token과 Refresh Token을 HTTP Only 속성으로 설정하여 쿠키에 담아 사용자에게 반환합니다.

✅ RRT(Rotation Refresh Token)?

JWT(JSON Web Tokens)의 가장 큰 장점은 database 조회 없이 빠른 세션 검증이 가능하다는 점입니다. 그러나 보안을 위해 토큰의 블랙리스트나 거부 목록을 유지 관리하며, 모든 API 호출에서 쿼리를 실행해야 하는 경우 이러한 장점이 손실될 수 있습니다. 이를 해결하기 위한 대안으로, 세션 관리 솔루션을 고려해볼 수 있습니다.

사용자가 로그인하면, 서버는 수명이 짧은 JWT(Access Token)와 수명이 긴 JWT(Refresh Token)를 발급합니다. 사용자에게 접근 권한이 부여되면 이 두 토큰이 클라이언트로 전송됩니다.

사용자가 리소스에 접근하려면, 모든 요청에 Access Token을 함께 보냅니다. Access Token이 만료되면, 클라이언트는 Refresh Token을 사용하여 새로운 Access Token과 Refresh Token을 요청합니다. 이 과정을 '회전 새로 고침 토큰(Rotation Refresh Token)'이라고 부릅니다.

클라이언트는 새로 발급받은 JWT를 사용하여 후속 요청을 작성하며, 이 과정이 반복됩니다. 이 방식의 장점은, 접근 권한을 취소하려는 경우에 서버에서 Refresh Token을 무효화하기만 하면 된다는 점입니다. Refresh End Point가 호출되면, 서버는 Refresh Token을 조회하여 만료 여부를 확인하고, 필요한 경우 사용자를 로그아웃 처리합니다.

그러나 이 방식은 클라이언트에 여전히 유효한 Access Token이 남아 있는 문제를 완전히 해결하지는 못합니다. 하지만 이 Access Token의 수명을 매우 짧게 설정(예: 몇 분)하여 이 문제를 크게 신경 쓰지 않아도 되도록 할 수 있습니다.

세션 하이재킹의 감지

공격자가 어떤 수단을 사용하여 Refresh Token을 획득한 후, 사용자가 Access Token을 재발급하기 전에 공격자가 먼저 Refresh Token을 이용해 토큰을 발급받는 상황을 상상해 봅시다. 이런 경우, 사용자가 이후에 Access Token을 재발급하면, 사용자의 세션은 찾을 수 없게 되지만, 공격자는 세션을 계속 유지할 수 있는 문제가 발생합니다.

세션 하이재킹을 어느 정도 감지하기 위해, DB에 세션을 저장할 때 최초 생성된 Refresh Token의 jti와 새로운 Refresh Token의 jti를 함께 저장하도록 설계할 수 있습니다. 또한, Refresh Token에는 최초 생성된 Refresh Token의 jti를 저장하도록 합니다. 이러한 구조를 통해, 세션 하이재킹을 감지하는 흐름은 아래와 같습니다.

1. 공격자가 어떠한 방법으로 Refresh Token을 탈취합니다.
2. 공격자는 탈취한 Refresh Token을 이용하여 토큰을 재발급 받습니다. 이 과정에서 세션 정보가 갱신됩니다.
3. 사용자는 자신의 Refresh Token을 사용하여 토큰 재발급을 시도합니다. 이때 세션에 저장된 최초 생성된 Refresh Token의 jti는 동일하지만, 마지막으로 생성된 Refresh Token의 jti와 사용자의 요청 jti가 다르면 세션 하이재킹이 발생했음을 감지할 수 있습니다.
4. 세션 하이재킹이 감지되면, 사용자를 로그아웃 처리하고, 하이재킹이 발생했음을 알립니다.
5. 이후 공격자가 다시 Refresh Token을 이용하여 토큰을 재발급하려고 시도하면, 이미 로그아웃 처리가 되어 있는 상태이므로 더 이상의 공격은 불가능해집니다.

✅ JWT의 jti란?

JWT의 세 가지 구성 요소 중 하나인 페이로드(payload)에는 여러 등록된 클레임(Registered Claims)이 포함됩니다. 이들 중 jti는 JWT의 고유 식별자를 나타냅니다.

아래는 대표적인 Registered Claims의 종류와 설명입니다:

  • iss: JWT를 발행한 주체(issuer)를 나타냅니다.
  • iat: JWT가 발행된 시간(issued at)을 나타냅니다.
  • exp: JWT의 만료 시간(expiration time)을 나타냅니다.
  • sub: JWT의 주제(subject)를 나타냅니다. 주로 사용자 식별을 위해 사용합니다.
  • aud: JWT의 대상자(audience)를 나타냅니다.
  • nbf: JWT의 사용 가능 시간(not before)을 나타냅니다. 이 시간 이전에는 JWT가 처리되지 않습니다.
  • jti: JWT의 고유 식별자(JWT ID)를 나타냅니다.

JWT 인증/인가 체계의 마무리: 보안과 사용 케이스 고려점

이번 글에서는 JWT(JSON Web Tokens)의 인증 및 인가 체계에 대해 살펴보았습니다. 이 내용은 보안을 중요하게 고려하면서 프론트엔드와 백엔드가 분리된 시스템을 가정하고 작성하였습니다.

보안 면에서는 위에서 설명한 절차 외에도 TLS/SSL을 이용한 HTTPS 통신을 사용하는 것이 네트워크 스니핑을 막아 큰 도움이 됩니다.

대부분의 경우, JWT를 사용하기 보다는 세션을 이용하는 것이 구현 속도와 보안 면에서 더욱 우수합니다. JWT는 서버의 리소스가 적거나, 다양한 서비스로의 확장이 필요한 경우에 추천됩니다.

이번 글에서는 Access Token, Refresh Token, CSRF Access Token, CSRF Refresh Token 등을 쿠키에 저장하는 방법을 다루었습니다. 이들 토큰은 브라우저에서 요청을 보낼 때마다 자동으로 포함되어 전송됩니다. 추가적으로, 쿠키 설정을 통해 쿠키 전송을 제한할 수 있는 PATH를 설정하는 것이 좋습니다.

profile
코딩 잘하고 싶은 백엔드 개발자

0개의 댓글