JWT 토큰 인증 절차

Sheryl Yun·2022년 3월 11일
5

이번 포스트에서는 JWT 인증 절차의 전체적인 과정이 생기기까지
발생했던 문제와 이를 보완했던 과정을 살펴본다.

우선 AccessToken만 있을 때를 가정해보자. (Level 0)

Level 0

  1. [프론트엔드] 사용자의 ID와 비밀번호를 서버에게 준다.

  2. [백엔드] ID와 비밀번호를 검증하고 AccessToken을 반환한다.

  3. [프론트엔드] 서버에게 받은 AccessToken을 이후 API 요청을 할 때마다 헤더에 넣어서 요청과 같이 서버에 보낸다.

  4. [백엔드] AccessToken으로 사용자를 확인한 뒤 요청을 처리해 준다.

문제

AccessToken이 한번 탈취 당하면 공격자가 무제한으로 API에 접근하여 정보를 채갈 수 있다.

해결

AccessToken에 만료시간을 설정한다. (Level 1)

Level 1

  1. [프론트엔드] 사용자의 ID와 비밀번호를 서버에게 준다.

  2. [백엔드] ID와 비밀번호를 검증하고 AccessToken을 반환하면서 서버 측에 AccessToken의 만료시간(10분, 30분 등)을 설정해둔다.

  3. [프론트엔드] AccessToken을 받아 다음 api 호출부터 헤더에 붙여준다.

  4. [백엔드] 클라이언트에서 보내온 AccessToken의 만료시간이 지났는지 확인하고 안 지났다면 요청받은 내용을 처리해준다.

문제

AccessToken의 만료시간이 지나면?
더 이상 요청 시 붙여 보내도 사용자 확인을 할 수 없어서 사용자가 매번 로그인을 다시 해야 한다.

해결

AccessToken의 만료시간이 지나면 RefreshToken을 통해 AccessToken을 재발급받을 수 있도록 한다. (Level 2)

Level 2

  1. [프론트엔드] ID와 비밀번호를 준다.

  2. [백엔드] ID와 비밀번호를 검증하고 만료시간을 설정된 AccessToken와 RefreshToken을 반환한다. (RefreshToken에도 만료시간이 있음)

  3. [프론트엔드] 서버에게 받은 AccessToken을 다음 요청 때부터 헤더에 넣어주고, RefreshToken은 어딘가 안전한 곳에 보관해둔다.

  4. [백엔드] 클라이언트의 요청을 받으면, 헤더의 AccessToken의 만료시간이 지났는지 확인하고, 안 지났다면 요청을 처리하고, 지났다면 실패 응답을 보낸다.

  5. [프론트엔드] 서버로부터 실패 응답을 받으면, 이전에 보낸 AccessToken이 만료되었다는 뜻이므로 RefreshToken을 보내서 새로운 AccessToken 발급 요청을 한다.

  6. [백엔드] 재발급 요청이 오면 RefreshToken의 만료시간이 지나지 않았는지 확인하고 새로운 AccessToken을 만들어 반환한다.

문제

RefreshToken은 좀 더 만료기간이 긴 토큰일 뿐이므로, 마찬가지로 탈취될 경우 공격자가 무제한으로 AccessToken을 재발급받을 수 있다.

그렇다면 왜 굳이 두 토큰을 분리해서 사용하는지? 🙋‍♀️
RefreshToken을 AccessToken과 합쳐서 하나의 토큰으로 쓰지 않는 이유는 사용 빈도와 용도의 차이 때문이다.
AccessToken모든 API 요청마다 사용하는 반면, RefreshTokenAccessToken이 만료되어 재발급이 필요할 때만 사용한다.
따라서 해커에게 노출되는 빈도가 훨씬 낮은 RefreshToken은 더 안전한 곳에 보관해두고, 서버와 요청을 주고 받을 때는 주로 AccessToken을 위주로 사용하게 되는 것이다.

해결: RefreshToken을 탈취당하지 않게 하려면?

처음에 사용자가 로그인을 해서 클라이언트에게 AccessToken과 RefreshToken을 발급해줄 때, 서버도 RefreshToken의 정보를 DB에 저장해둔다.
그리고 나중에 클라이언트 쪽에서 토큰 재발급 요청이 왔을 때, 클라이언트가 보낸 RefreshToken의 정보가 DB에 저장된 정보와 같은지 비교한다. (Level 3)

따라서 사용자가 (해커의 방해로) 다른 환경에서 새로이 로그인하게 되면, RefreshToken의 정보가 달라질 것이므로 기존의 RefreshToken은 사용할 수 없어 요청에 실패 응답을 보내게 된다.

Level 3

  1. [프론트엔드] 로그인을 해서 ID와 비밀번호를 준다.

  2. [백엔드] 요청으로 온 ID와 비밀번호를 검증하고 만료시간이 설정된 AccessToken, RefreshToken을 반환한다. 또, RefreshToken의 정보를 서버쪽 DB에도 { ID, RefreshToken } 형태로 저장해둔다.

  3. [프론트엔드] 서버에서 받은 AccessToken을 매 요청마다 헤더에 넣어주고, RefreshToken은 안전한 곳에 보관해둔다.

  4. [백엔드] AccessToken의 만료기간이 유효하면 요청을 처리해주고, 아니면 실패 응답을 보낸다.

  5. [프론트엔드] 서버한테서 실패 응답이 오면, RefreshToken을 서버에 보내서 AccessToken 재발급 요청을 한다.

  6. [백엔드] 재발급 요청이 오면 받은 RefreshToken의 정보가 DB에 있는 정보와 동일한지 비교해서 서로 같을 때에만 AccessToken을 새로 반환한다.

문제

클라이언트 쪽에서 API 요청을 보내다가, 실패할 경우 재발급을 위해 추가 요청을 하는 방식이어서 불필요한 api 호출이 최소 한 번은 발생한다.

해결

처음에 로그인을 할 때 AccessToken의 만료시간까지 서버에서 전달받아, 토큰의 만료 여부를 클라이언트에서 확인한다. (Level 4)
그러면 '서버가 요청을 처리하기 전에 AccessToken이 만료되었는지 확인'하는 절차가 하나 줄어들기 때문에, AccessToken 만료 시 RefreshToken으로 재발급받는 요청만 1번 발생한다.

Level 4

  1. [프론트엔드] ID와 비밀번호를 준다.

  2. [백엔드] ID와 비밀번호를 검증하고, 만료시간이 설정된 AccessToken 및 RefreshToken과 함께 AccessToken의 만료시간에 대한 정보까지 반환해준다. 이 때 반환해주는 RefreshToken의 정보는 DB에 { ID, RefreshToken }으로 저장해둔다.

  3. [프론트엔드] 서버에게 받은 AccessToken 정보로 요청을 할 때마다 미리 AccessToken의 만료 기간을 확인하고, 만료가 되지 않았으면 헤더에 AccessToken을 넣어 서버에게 API 요청을 한다.

  4. [백엔드] 헤더의 AccessToken이 유효하면 API 응답을 보낸다.

  5. [프론트엔드] 만약 AccessToken의 만료 기간을 확인했을 때 만료되었으면, 서버에 API 요청 대신 RefreshToken을 활용한 재발급 요청만 보낸다.

  6. [백엔드] 재발급 요청을 받으면, 전달받은 RefreshToken의 정보가 DB에 있는 정보와 동일한지 확인한다. 두 정보가 동일하면 새로운 AccessToken과 AccessToken의 만료 시간을 함께 반환하고, 그렇지 않으면 실패 응답을 보낸다.

  7. [프론트엔드] 새로운 AccessToken이 반환되면 만료기간을 기억해두고, 다음 요청 전에 다시 AccessToken의 만료 시간을 확인하여 요청을 진행한다.

문제

아주 사소한 부분이기는 하지만, 클라이언트에서 서버로 api를 요청하는 데 '시간이 걸린다'는 사실에서 문제가 발생할 수 있다. ('간발의 차'로 인한 요청 실패 가능성)

예를 들어, AccessToken의 만료시간이 1시 20분까지인데 클라이언트가 1시 20분이 되기 직전인 1시 19분 59초 쯤에 요청을 보냈다고 하면,
클라이언트 쪽에서는 아직 1시 20분이 아니어서 재발급 요청 없이 요청을 그대로 보낸 것이지만, 서버 쪽에서는 만료시간인 1시 20분이 되고 난 후에 받은 요청이므로 AccessToken이 만료기간이 다 되었다는 실패 응답을 보내게 된다.

해결

단순히 AceessToken의 만료시간이 지났는지 여부로 확인하지 말고, '만료시간이 ~ 이하일 경우'라고 조건을 바꿔준다. (Level 5)

Level 5

  1. [프론트엔드] ID와 비밀번호를 준다.

  2. [백엔드] ID와 비밀번호를 검증하고, 만료시간이 설정된 AccessToken 및 RefreshToken과 함께 AccessToken의 만료시간에 대한 정보까지 반환해준다. 이 때 반환해주는 RefreshToken의 정보는 DB에 { ID, RefreshToken }으로 저장해둔다.

  3. [프론트엔드] 서버에게 받은 AccessToken 정보로 요청을 할 때마다 미리 AccessToken의 만료 기간을 확인한다. 이때 만료 시간이 30초 이내로 남았으면 API 요청 대신 RefreshToken을 활용한 재발급 요청을 보낸다.

  4. [백엔드] 재발급 요청을 받으면 전달받은 RefreshToken의 정보가 DB에 있는 정보와 동일한지 확인한뒤 새로운 AccessToken과 AccessToken의 만료 시간을 함께 반환한다. API 요청이 오면 요청받은 데이터를 보내준다.

  5. [프론트엔드] 새로운 AccessToken이 반환되면 만료기간을 기억해두고, 다음 요청 전에 다시 AccessToken의 만료 시간을 확인하여 요청을 진행한다.


출처: https://han-um.tistory.com/17

profile
영어강사, 프론트엔드 개발자를 거쳐 데이터 분석가를 준비하고 있습니다 ─ 데이터분석 블로그: https://cherylog.tistory.com/

1개의 댓글

comment-user-thumbnail
2022년 5월 3일

좋은 글 감사합니다

따라서 사용자가 (해커의 방해로) 다른 환경에서 새로이 로그인하게 되면, RefreshToken의 정보가 달라질 것이므로 기존의 RefreshToken은 사용할 수 없어 요청에 실패 응답을 보내게 된다.

질문이 있는데 왜 리프레쉬 토큰 정보가 달라지나요?

답글 달기