Backend

[Backend] Access Token & Refresh Token

dong_seok 2024. 12. 12. 18:33
1.  Access Token과 Refresh Token의 필요성
2. JWT 인증 체계 설계
3. 결론

 

이전 블로그에서 JWT에 대해서 알아보았습니다. 그렇다면, 이러한 JWT가 일반적으로 많이 사용되는 곳이 어디일까?를 생각해보면 인증 체계였습니다. JWT인 Access TokenRefresh Token을 만들고 이를 이용해 인증 체계를 구축한 과정에 대해 말씀드리도록 하겠습니다.

1.  Access Token과 Refresh Token의 필요성

Access Token 하나만 가지고도 인증할 수 있지 않을까? 라는 생각이 들 수 있지만, 실제로는 하나의 토큰으로는 여러 문제점들이 발생할 수 있습니다. Access Token만 사용한다고 가정해보겠습니다. Payload에 사용자 정보를 담아 통신 간에 JWT를 사용할 것입니다. 서버는 전달받은 Access Token을 디코딩하고 저장된 Secret Key를 이용해 유효성을 검증함으로써 인증을 수행할 수 있습니다.

 

하지만, 만약 이 Access Token이 한번 탈취되었다고 가정해봅시다. 서버는 이 Access Token이 정상적인 클라이언트에서 온 것인지, 아니면 탈취된 악의적인 이용자로부터 온 것인지 구분할 수 없기 때문에 토큰을 무기한으로 갱신하게 될 것입니다. 이는 탈취된 토큰이 장기간 동안 악용될 수 있다는 것을 의미합니다. 또한, Access Token에 사용자 정보가 포함되어 있기 때문에, 탈취된 토큰을 통해 지속적으로 사용자 정보를 악의적으로 사용할 수 있습니다.

 

이러한 문제들을 해결하기 위해 등장한 것이 Refresh Token입니다. Refresh Token은 Access Token과 마찬가지로 JWT 형식을 사용하지만, 몇 가지 중요한 차이점이 있습니다.

 

  Access Token Refresh Token
목적 인증이 필요한 요청을 처리하는 데 사용됩니다. 필요한 사용자 정보와 권한을 담고 있습니다. Access Token을 재발급 받기 위해 사용됩니다. 불필요한 사용자 정보를 담지 않고, 오직 토큰 재발급에만 관여합니다.
일반적인 유효기간 30분~1시간 1주~2주

 

이처럼 Access Token과 Refresh Token을 함께 사용함으로써 보안을 강화하고, Access Token의 짧은 유효 기간 동안만 인증을 수행할 수 있습니다.


2.  JWT 인증 체계 설계

1) 인증 체계 설계

제가 구성한 인증 체계 로직은 다음과 같습니다:

  1. 로그인 과정:
    • 사용자가 ID와 비밀번호를 통해 정상적으로 로그인합니다.
    • 서버는 회원 DB를 조회하여 사용자를 확인합니다.
    • 로그인에 성공하면 서버는 Access TokenRefresh Token을 생성하여 클라이언트에게 발급하고, Redis에 Refresh Token을 저장합니다.
  2. 토큰 저장:
    • 클라이언트는 전달받은 토큰들을 안전한 로컬 저장소(예: Secure Storage)에 저장합니다.
  3. 인증 요청:
    • 클라이언트는 인증이 필요한 API 요청 시마다 Access Token을 헤더에 담아 서버로 전송합니다.
  4. Access Token 검증:
    • 서버는 전달받은 Access Token을 검증하여 유효한 경우 요청에 알맞은 데이터를 응답합니다.
  5. Access Token 만료 처리:
    • 시간이 지나 Access Token이 만료되면, 클라이언트는 만료된 Access Token을 사용해 API 요청을 보냅니다.
    • 서버는 만료된 Access Token을 검증하고, 만료되었다는 응답을 클라이언트에게 반환합니다.
  6. 토큰 갱신 요청:
    • 클라이언트는 만료된 Access Token과 저장된 Refresh Token을 함께 헤더에 담아 Access Token 재발급 API를 호출합니다.
  7. 토큰 검증 및 재발급:
    • 서버는 받은 Access Token이 변조되지 않았는지 검증합니다.
    • Refresh Token을 Redis에 저장된 토큰과 비교하여 동일하고 유효기간이 지나지 않았다면, 새로운 Access Token과 Refresh Token을 발급하고 Redis에 업데이트합니다.
  8. 새 토큰 저장 및 재요청:
    • 클라이언트는 새로 발급받은 토큰들을 다시 로컬 저장소에 저장하고, 새로운 Access Token을 사용해 원래의 API 요청을 재전송합니다.

2) 설계 과정

JWT를 이용한 인증 체계 구축은 Access Token과 Refresh Token을 사용하는 공통점이 있지만, 전달할 토큰의 종류, 저장 위치 등 여러 방식으로 구현할 수 있습니다. 제가 위와 같은 인증 체계를 구축하게 된 근거와 과정에서 겪었던 궁금증들에 대해 설명드리겠습니다.

 

(1) Access Token 만료 시 바로 재발급 가능 여부

클라이언트가 만료된 Access Token으로 API를 호출하면, 서버는 에러 코드를 반환한 뒤 클라이언트가 Refresh Token을 사용해 Access Token 갱신 API를 호출하도록 유도합니다. 그런데, 서버에서 바로 저장된 Refresh Token을 디코딩해 유저 정보를 확인한 뒤 Access Token을 재발급하면, 불필요한 API 호출을 줄일 수 있지 않을까?

 

-> 이 질문은 JWT에 대한 이해도 부족으로 생겼던 궁금증이라고 생각합니다. 이론적으로 가능하지만, 일반적인 구현 방식과는 다릅니다. 이유는 다음과 같습니다.

  1. Payload 차이:
    • Access Token의 Payload는 유저 정보(예: 권한, 사용자 ID 등)를 담는 경우가 많습니다.
    • Refresh Token의 Payload는 보안성을 높이기 위해 최소한의 정보(예: 사용자 ID)만 담습니다.
  2. 서명 불일치:
    • Access Token과 Refresh Token은 서로 다른 Payload를 가지며, 서명(Signature)도 각각 다릅니다.
    • 서버는 Refresh Token으로 Access Token을 대체하려고 하면 서명 검증에서 실패하여 인증 오류가 발생합니다.

결론적으로, Refresh Token을 사용한 Access Token 갱신은 별도의 API 호출로 처리하는 것이 일반적이며, 이는 보안성과 구조적 일관성을 유지하기 위해 중요합니다.

 

(2) JWT 생성 시 Header 명시 여부

아래 코드에서 jwt.encode 호출 시 Header를 명시하지 않았습니다. 이런 경우 JWT가 정상적으로 생성되는가?

access_token = jwt.encode(
	access_token_payload, self.secret, algorithm=ALGORITHM
)

 

-> 처음 FastAPI에서 JWT를 인코딩하려고 할 때, 이론적으로 JWT를 생성할 때 Header, Payload, Signature 세 가지 요소를 명시해야 한다고 공부했습니다. 그러나 jwt.encode 함수에 Payload, Secret Key, Algorithm만 인자로 전달했는데도 JWT가 정상적으로 생성되는 것을 보고 혼동하게 되었습니다.

 

조사를 해보니, jwt.encode 메서드는 내부적으로 Header를 자동으로 생성하여 JWT 토큰에 포함시킵니다. 따라서, 코드에서 명시적으로 Header를 지정하지 않아도 algorithm 매개변수에 따라 기본 Header가 생성됩니다.

예를 들어, HS256 알고리즘을 사용할 경우 Header는 다음과 같이 자동으로 생성됩니다:

{
  "alg": "HS256",
  "typ": "JWT"
}

 

결론적으로, jwt.encode 함수는 Header를 자동으로 생성하고, 이 Header와 Payload를 Secret Key를 이용해 Signature로 만듭니다. 이 세 요소를 Base64로 인코딩하여 최종적인 JWT를 생성하게 됩니다. 이는 제가 이론적으로 공부한 내용과 일치하지만, encode 함수의 구현 차이로 인해 Header를 별도로 지정할 필요가 없다는 것을 알게 되었습니다.

 

(3) Refresh Token 없이 Access Token만 사용하는 경우의 문제점

Access Token의 유효성을 서버에서 Secret Key를 이용해 검증하면 데이터베이스 조회 없이도 인증이 가능합니다. 그렇다면 Refresh Token 없이 Access Token만으로 인증 체계를 구축해도 문제가 없지 않을까요? Refresh Token이 필요한 이유는 무엇인가요?

 

-> 이 질문 역시 JWT 이론에서 비롯된 오해였습니다. 처음에는 서버에 저장된 Secret Key로 토큰의 유효성을 검증하고, 유효하면 바로 새로운 Access Token을 발급하는 방식으로 생각했으나, 이는 여러 문제를 초래할 수 있습니다.

  1. Access Token 탈취 위험:
    • 사용자가 만료된 액세스 토큰으로 갱신 요청을 보내는 것과 탈취된 만료 액세스 토큰으로 갱신 요청을 보내는 것을 서버는 구분할 수가 없습니다. 즉, 탈취된 Access Token은 만료되기 전까지 악용될 수 있습니다.
    • 만약 탈취된 Access Token으로 갱신 요청을 하면, 서버는 클라이언트와 공격자를 구분할 수 없어 무한 갱신 요청을 허용하게 됩니다.
  2. Refresh Token의 역할:
    • Access Token 갱신 시 반드시 Refresh Token을 사용하도록 설계하면, 탈취된 Access Token만으로는 갱신이 불가능합니다.
    • Refresh Token은 클라이언트의 안전한 저장소에 보관되며, 상대적으로 탈취 가능성이 낮습니다.
  3. 세션 제어 가능:
    • Refresh Token을 서버에서 관리하면, 특정 사용자 세션을 강제로 종료하거나 로그아웃 시 모든 세션을 무효화할 수 있습니다.
    • 반면, Access Token만 사용하는 경우 서버에서 세션을 제어할 방법이 없습니다.

 

(4) 인증 요청 시 Access Token만 전송 vs Access Token과 Refresh Token 함께 전송

인증이 필요한 요청을 보낼 때 클라이언트에서 헤더에 Access Token만 담는 경우와 Refresh Token까지 함께 담아서 보내는 경우 중 어떤 방법이 옳을까?

 

-> 각각의 방법에는 장단점이 있어 고민이 많았으나, 저는 Access Token만 헤더에 담아 전송하는 방식을 선택했습니다. 이유는 다음과 같습니다:

  1. 보안성 강화:
    • Refresh Token을 매 요청마다 전송하지 않음으로써, Refresh Token의 노출 위험을 줄였습니다.
  2. 네트워크 효율성:
    • 일반적인 API 요청 시 Refresh Token을 포함하지 않아 네트워크 트래픽을 줄일 수 있습니다.
  3. 클라이언트 복잡성 감소:
    • Refresh Token을 별도로 관리하여, 클라이언트 측에서 토큰 갱신 로직을 명확히 분리할 수 있습니다.
  4. 사용자 경험:
    • Access Token이 만료되었을 때, 클라이언트가 자동으로 갱신 API를 호출하여 새로운 토큰을 받음으로써 사용자는 인증 과정의 복잡성을 느끼지 않습니다.

이러한 이유로, Access Token만을 사용하고, 만료 시 Refresh Token을 별도로 사용하는 방식이 보안과 효율성 면에서 더 적합하다고 판단했습니다.

 

(5) Refresh Token을 Redis에 저장하는 이유

왜 Refresh Token을 Redis에 저장하는 것이지?

 

->JWT 인증 체계에서 Refresh Token을 Redis에 저장하는 이유는 서버 부하를 줄이면서도 보안과 세션 관리를 효율적으로 유지하기 위함입니다. 자세한 이유는 다음과 같습니다:

  1. 고속 데이터 접근:
    • Redis는 메모리 기반 저장소로, 데이터 읽기/쓰기가 매우 빠릅니다.
    • Refresh Token의 조회와 갱신 작업을 빠르게 처리하여 서버 부하를 최소화할 수 있습니다.
  2. 효율적인 TTL 관리:
    • Redis는 각 키에 대해 TTL(Time-To-Live)을 설정할 수 있어, Refresh Token의 만료를 자동으로 관리할 수 있습니다.
    • 만료된 Refresh Token은 자동으로 삭제되므로, 추가적인 관리 작업이 필요 없습니다.
  3. One-Time Token 방식 적용:
    • Refresh Token을 갱신 요청 시마다 새로운 토큰으로 교체하고 기존 토큰을 삭제함으로써, Refresh Token의 재사용 공격을 방지할 수 있습니다.
    • Redis는 이러한 갱신 과정을 빠르게 처리할 수 있어 서버 부하를 크게 줄일 수 있습니다.

이러한 이유로, Refresh Token을 Redis에 저장하는 방식은 보안성과 성능을 모두 만족시키며, 서버 부하를 효과적으로 관리할 수 있는 최적의 방법입니다.


결론

JWT 인증 체계는 Access TokenRefresh Token을 함께 사용함으로써 보안성과 효율성을 동시에 높일 수 있습니다. Access Token은 짧은 유효 기간으로 인증을 수행하고, Refresh Token은 장기간 동안 안전하게 토큰을 갱신하는 역할을 합니다. 이를 통해 토큰 탈취 시의 위험을 줄이고, 사용자 세션을 효과적으로 관리할 수 있습니다.

 

Refresh Token을 Redis와 같은 서버 측 스토리지에 저장함으로써, 토큰의 유효성을 검증하고 보안을 강화할 수 있습니다. 이러한 인증 체계는 대규모 서비스보안이 중요한 애플리케이션에서 특히 유용하게 사용됩니다.


(추가 수정사항)

기존에 Refresh Token Rotation 방식으로 구현한 인증 로직을 팀원들과 검토하던 중, 한 가지 의견이 제시되었습니다.

 

의견 제시사항

"Refresh Token이 한 번 탈취되면, 이를 이용해 계속해서 Access Token과 Refresh Token을 새로 발급받을 수 있는 구조가 아닌가?"

 

이 의견에 동의하게 되었고, 인증 로직을 수정하기로 결정했습니다. 기존 방식에서는 Access Token이 만료될 때마다 Refresh Token을 함께 새롭게 발급했지만, 수정된 로직에서는 Access Token만 새롭게 발급하고, Refresh Token은 계속 재사용하도록 변경하였습니다.

이 과정에서 한 가지 우려되었던 점은, ""사용자가 앱을 정상적으로 사용하더라도 Refresh Token의 유효기간이 지나면 강제로 로그인을 다시 해야 하는 상황이 발생하지 않을까?"" 라는 부분이었습니다. 이를 해결하기 위해 앱 실행하면서 자동로그인할때 Access Token과 Refresh Token을 새롭게 발급하도록 로직을 보완하였습니다.

 

 

 

참고자료

https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-Access-Token-Refresh-Token-%EC%9B%90%EB%A6%AC-feat-JWT

 

 

'Backend' 카테고리의 다른 글

[Backend] JWT  (0) 2024.12.11