[ OAuth 2.0 ] Access Token & Refresh Token으로 보안 강화하기
이전에 로그인 기능을 구현했던 방식에서 보완과 관련한 문제점을 발견하여 이를 해결하기 위해 로그인 기능을 수정하게 되었습니다.
이 과정은 기존의 방식이 왜 잘못되었는지를 분석하고,
더 안전한 방식인 [ OAuth2.0 ] Access Token & Refresh Token 방식에 대해 다루고있습니다.
< 목차 >
1. 기존의 로그인 구현 방식과 문제점
2. 해결방안 : [ OAuth 2.0 ] Access Token & Refresh Token을 사용하자
3. Refresh Token 이란?
4. Refresh Token의 한계
5. Refresh Token은 어디에 저장해야 안전할까?
6. 기존의 문제점을 해결하기에 효과적인 방법인가?
1. 기존의 로그인 구현 방식과 문제점
기존의 로그인 방식은 JWT 방식으로 Access Token만을 사용하는 방식이었습니다. 위의 코드와 같이 아이디와 비밀번호를 서버에 POST한 후 서버에서 보내는 response에 실린 token의 유무로 로그인 가능 여부를 판별하였습니다.
그리고 response에 access token이 있을 경우, localStorage에 TOKEN이라는 keyname으로 access token을 저장하도록 합니다. 저장된 token을 localStorage에서 get하여 토큰의 유무를 판별합니다.
이 방식으로 모든 로그인 기능을 구현하였고, 로그인 시 이용가능한 프로덕트 내 서비스들의 접근 권한도 판별하였습니다.
그런데 이후 이러한 로그인 방식 및 사용자 권한 확인 방식에 보안 문제가 있다는 것을 알게 되었습니다.
문제점
- localStorage에 "TOKEN"을 저장하고나면 token의 유효기간이 만료될 때까지, 혹은 로그아웃을 하기전까지 계속 localStorage에 token이 저장된 상태를 유지한다. 따라서 실제 사용자가 로그인을 한 것이 아니더라도 로그인 상태가 계속 유지된다. 이렇게 될 경우 마찬가지로 보완상 문제가 될 수 있다.
- localStorage에 "TOKEN"이라는 이름으로 token을 저장하고 그 "TOKEN"의 유무로 사용자를 판별하는 것은 "TOKEN"에 유효하지 않은 임의의 값을 저장하여도 이후 토큰의 유효성을 검증하기 하지 않기 때문에 비정상적인 서비스 접근이 가능하다.
- 추가적으로 localStorage에 "TOKEN"이라는 keyname으로 access token을 저장해서 사용하는 방식은 토큰 탈취의 위험성이 있다.
위와 같이 Access Token은 발급된 이후 서버에 저장되지 않고 클라이언트에 저장되어 토큰 자체로 검증을 하며 사용자 권한 인증을 진행하기 때문에, Access Token이 탈취되면 토큰이 만료되기 전까지 토큰을 획득한 사람은 누구나 권한 접근이 가능해지는 문제점이 있었습니다. 토큰의 유효 시간을 부여하여 탈취 문제에 대응하는 방법도 있지만, 만일 유효 기간이 짧을 경우 그만큼 사용자는 로그인을 자주해야 하는 번거로움이 있었습니다.
해결 방안
2. [ OAuth 2.0 ] Access Token & Refresh Token을 사용하자
2-1 .OAuth 2.0란?
OAuth 2.0(Open Authorization 2.0, OAuth2)은 인증을 위한 개방형 표준 프로토콜입니다.
이 프로토콜에서는 Third-Party 프로그램에게 리소스 소유자를 대신하여 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식을 제공합니다. 구글, 페이스북, 카카오, 네이버 등에서 제공하는 간편 로그인 기능도 OAuth2 프로토콜 기반의 사용자 인증 기능을 제공하고 있습니다. OAuth 2.0은 Access Token 과 Refresh Token 사용합니다.
2-2. JWT와 OAuth 2.0의 차이점 ?
- OAuth 2.0 : 권한 인증 프레임워크 중 하나로, 하나의 플랫폼의 권한(아무 의미없는 무작위 문자열 토큰)으로 다양한 플랫폼에서 권한을 행사할 수 있게 해줌으로써 리소스 접근이 가능하게 하는데 목적을 두고있습니다.
- JWT : Cookie, Session을 대신하여 의미있는 문자열 토큰으로써 권한을 행사할 수 있는 토큰의 한 형식입니다. 주로 로그인 세션이나 주고받는 값이 유효한지 검증할 때 쓰입니다.
JWT & Access Token에 대해 더 자세히 알아보기
2-3. Access Token 과 Refresh Token 비교
- Access Token : 클라이언트가 갖고있는 실제로 유저의 정보가 담긴 토큰으로, 클라이언트에서 요청이 오면 서버에서 해당 토큰에 있는 정보를 활용하여 사용자 정보에 맞게 응답을 진행합니다.
- Refresh Token: 새로운 Access Token을 발급해주기 위해 사용하는 토큰으로 짧은 수명을 가지는 Access Token에게 새로운 토큰을 발급해주기 위해 사용합니다.
정리하자면, Access Token은 접근에 관여하는 토큰, Refresh Token은 재발급에 관여하는 토큰이라고 할 수 있습니다.
토큰 기반 인증 방식에서 토큰은 세션과 다르게 stateless 합니다. 다시 말해 서버가 상태를 보관하고 있지 않다는 것입니다. 서버는 한번 발급한 토큰에 대해서 제어권을 가지고 있지 않습니다. 따라서 제 3자에게 토큰 탈취의 위험성이 있기 때문에, 현업에서는 Access Token과 Refresh Token을 모두 사용하여 이중으로 나눠서 인증을 하는 방식을 사용합니다.
3. Refresh Token 이란?
3-1. Refresh Token의 목적
Refresh Token의 목적은 Access Token의 유효 기간을 짧게하여, 자주 재발급 하도록 만들어 보안을 강화하면서도 사용자에게 잦은 로그인 요구를 하지 않도록 하는 것입니다.
기존에 클라이언트가 가지고 있던 Access Token이 만료되었을 때 Access Token을 새로 발급받기 위해 사용합니다.
3-2. Refresh Token 유효 기간
Refresh Token은 Access Token 대비 긴 유효 기간을 갖습니다. Refresh Token을 사용하는 상황에서는 일반적으로 Access Token의 유효기간은 30분 이내, Refresh Token의 유효기간은 2주 정도로 설정하는 것이 일반적입니다. 물론 서비스 성격에 따라 적절한 유효기간을 판단하여 설정 해야합니다.
- Access Token의 유효 기간은 짧게 설정합니다.
- Refresh Token의 유효 기간을 길게 설정합니다.
- 따라서 공격자가 Access Token을 탈취하더라도 짧은 유효 기간이 지나면 사용할 수 없습니다.
- 정상적인 사용자는 Access Token이 만료되었을 시, Access Token과 Refresh Token을 둘 다 서버에 전송하여 새로운 Access Token을 발급받을 수 있습니다.
즉 OTP 인증처럼 짧은 시간 동안에만 사용할 수 있도록 하고 주기적으로 재발급받도록 하여 토큰이 유출되더라도 그 피해를 최소화하는 방식입니다. 단순히 Access Token 만으로는 일일히 IP 주소의 위치를 파악해서 비교하는 게 아닌 이상 토큰의 탈취를 검증하기 어렵기 때문에 토큰이 탈취되더라도 그 피해를 줄이기 위해 토큰의 사용 시간 자체를 줄이는 것입니다.
3-3. Refresh Token 매커니즘
위의 그림은 가장 일반적인 OAuth2.0 매커니즘을 기준으로 설명하고 있습니다.
- 클라이언트가 로그인을 요청하고 성공하면, 서버는 Access Token과 Refresh Token을 함께 제공한다.
- 이후 클라이언트는 인가가 필요한 요청에 Access Token을 실어 보낸다.
- 시간이 조금 흘러 Access Token이 만료 되었다면, 클라이언트는 Refresh Token을 서버로 전달하여 새로운 Access Token을 발급 받는다. ( 사용자(프론트엔드)에서 Access Token의 Payload를 통해 유효기간을 알 수 있다. 따라서 프론트엔드 단에서 API 요청 전에 토큰이 만료됐다면 바로 재발급 요청을 할 수도 있다. )
3-4. Refresh Token의 형태
Refresh Token은 JWT 일수도, 그냥 간단한 문자열 또는 UUID 일수도 있습니다.
- JWT 형태의 Refresh Token
Refresh Token이 만약 JWT 라면, Access Token 와 똑같이 stateless 하고, 토큰 자체에 데이터를 담을 수 있습니다. 이 경우 Refresh Token의 유효성을 검증하기 위해 데이터베이스에 별도로 액세스하지 않아도 됩니다. 따라서 서버의 부하가 상대적으로 적습니다. 하지만 Access Token과 마찬가지로 Refresh Token 을 서버에서 제어할 수 없다는 단점이 존재합니다. 즉, Refresh Token 을 탈취당한 상황에서 토큰을 무효화 시키는 방법이 없습니다.
- JWT 형태가 아닌 Refresh Token
반대로 Refresh Token으로 random string 또는 UUID 등으로 사용한다면, 그 토큰을 사용자와 매핑되도록 데이터베이스에 저장해야합니다. 이런 경우 Refresh Token 사용시 데이터베이스에 액세스 해야합니다. 하지만, 사용자를 강제로 로그아웃 시키거나, 차단할 수 있게됩니다. 또한 Refresh Token이 탈취되었을 경우 그 즉시 무효화시킬 수 있습니다.
4. Refresh Token의 한계
4-1. Access Token을 즉시 차단할 방법의 부재
아무리 Refresh Token이 Access Token의 유효기간을 짧게 만들어 줄 수 있다고 하더라도, 탈취된 Access Token이 유효한 그 짧은 시간 동안에 악용될 수 있다는 위험성이 존재합니다.
4-2. Refresh Token 를 탈취 당할 가능성
그렇다면 Refresh Token 자체가 탈취당한다면 어떻게 될까요? 공격자는 이 토큰의 유효 기간만큼 다시 Access Token을 생성해서 다시 정상적인 사용자인 척 위장할 수 있습니다. 서버 DB에서 Refresh Token을 저장해 직접 추적하는 방법을 사용하면 조금이나마 피해를 줄일 수 있겠지만, 피해가 확인되기 전까진 탈취 여부를 알 방법이 없습니다.
RTR을 사용한다면 Refresh Token을 1회 사용하고 버리게 되어 더 안전하게 사용할 수 있지만, 사용하지 않은 Refresh Token을 탈취당하면 해커는 1회 한정으로 Access Token을 발급받을 수 있습니다. 따라서 클라이언트는 XSS, CSRF 공격으로부터 Refresh Token이 탈취되지 않도록 안전하게 보관해야합니다.
RTR (Refresh Token Rotation)
RTR (Refresh Token Rotation)란?
간단하게 말하자면, Refresh Token을 한번만 사용할 수 있게(One Time Use Only) 만드는 방법입니다.
Refresh Token을 사용하여 새로운 Access Token을 발급받을 때 Refresh Token 도 새롭게 발급받게 됩니다.
이런 방식을 사용하면, 이미 사용된 Refresh Token을 사용하게 되면 서비스측에서 탈취를 확인하여 조치할 수 있습니다.
다만, 사용되지 않은 Refresh Token을 훔쳐 사용하거나, 그냥 지속적으로 Access Token만을 탈취한다면 막을 수 없습니다.
4-3. Refresh Token 탈취에 대한 대안
스택오버플로우의 답변을 보면 다음과 같은 방법을 제안하고 있습니다.
- 데이터베이스에 각 사용자에 1대1로 맵핑되는 Access Token, Refresh Token 쌍을 저장한다.
- 정상적인 사용자는 기존의 Access Token으로 접근하며 서버측에서는 데이터베이스에 저장된 Access Token과 비교하여 검증한다.
- 공격자는 탈취한 Refresh Token으로 새로 Access Token을 생성한다. 그리고 서버측에 전송하면 서버는 데이터베이스에 저장된 Access Token과 공격자에게 받은 Access Token이 다른 것을 확인한다.
- 만약 데이터베이스에 저장된 토큰이 아직 만료되지 않은 경우, 즉 굳이 Access Token을 새로 생성할 이유가 없는 경우 서버는 Refresh Token이 탈취당했다고 가정하고 두 토큰을 모두 만료시킨다.
- 이 경우 정상적인 사용자는 자신의 토큰도 만료됐으니 다시 로그인해야 한다. 하지만 공격자의 토큰 역시 만료됐기 때문에 공격자는 정상적인 사용자의 리소스에 접근할 수 없다.
만약 공격자가 Refresh Token을 탈취해서 정상적인 사용자가 Access Token을 다시 발급받기 전에 자기가 먼저 Access Token을 생성한다면 어떻게 될까요? 이 경우에도 Access Token의 충돌이 일어나기 때문에 서버측에서는 두 토큰을 모두 만료시켜야 할 것입니다. 그래서 ietf 문서에서는 아예 Refresh Token도 Access Token과 같은 유효 기간을 가지도록 하여 사용자가 한 번 Refresh Token으로 Access Token을 발급받았으면 Refresh Token도 다시 발급받도록 하는 것을 권장하고 있습니다.
무엇보다도 Access Token과 Refresh Token을 모두 탈취 당할 경우에는 대안이 없으므로 사전에 이러한 문제가 발생하지 않도록 예방책을 세우는 것이 가장 중요합니다.
5. Refresh Token은 어디에 저장해야 안전할까?
"where to store refresh token on client?"라고 검색해보면 스텍오버 플로우에서는 http-only 속성이 부여된 쿠키에 저장하는 것을 권장하고 있습니다. 왜냐하면 해당 속성이 부여된 쿠키는 자바스크립트 환경에서 접근할 수 없기 때문입니다. 따라서 XSS나 CSRF가 발생하더라도 토큰이 누출되지 않습니다. 일반 쿠키나 브라우저의 로컬 스토리지는 자바스크립트로 자유롭게 접근할 수 있기 때문에 보안 측면에서는 권장되지 않습니다.
그외에 일반적으로 클라이언트 측에서 Refresh Token을 안전하게 저장하는 방법
- Secure Storage: 안드로이드나 iOS 앱 등에서는 안전한 저장소인 Keychain 또는 Keystore에 리프레시 토큰을 저장할 수 있습니다. 이 방법은 앱 내부에서 데이터를 안전하게 보호할 수 있습니다.
- In-Memory: 리프레시 토큰을 메모리에 저장하고 사용하는 방법도 있습니다. 이 경우 앱이나 웹 페이지가 재시작될 때마다 토큰이 다시 로드되어야 합니다.
- Local Storage / Session Storage: 브라우저 환경에서는 Local Storage 또는 Session Storage에 리프레시 토큰을 저장할 수 있지만, 이 방법은 XSS 공격에 노출될 위험이 있으므로 주의가 필요합니다.
- Stateful Approach: 일시적인 리프레시 토큰을 사용하며, 사용할 때마다 서버로부터 새로운 토큰을 요청하는 방식도 고려할 수 있습니다.
6. 기존의 문제점을 해결하기에 효과적인 방법인가?
다시 돌아와서 기존의 문제점에 대한 해결 방안으로 적합한지 확인해보겠습니다.
[ problem 1 ]
localStorage에 "TOKEN"을 저장하고나면 token의 유효기간이 만료될 때까지, 혹은 로그아웃을 하기전까지 계속 localStorage에 token이 저장된 상태를 유지한다. 따라서 실제 사용자가 로그인을 한 것이 아니더라도 로그인 상태가 계속 유지된다. 이렇게 될 경우 마찬가지로 보완상 문제가 될 수 있다.
=> Access Token의 유효 기간을 짧게 하여 보완을 강화한다. 유효 기간이 짧아짐에 따라 사용자가 자주 로그인해야 하는 문제를 Refresh Token을 이용해 해결할 수 있다.
[ problem 2 ]
localStorage에 "TOKEN"이라는 이름으로 token을 저장하고 그 "TOKEN"의 유무로 사용자를 판별하는 것은 "TOKEN"에 유효하지 않은 임의의 값을 저장하여도 이후 토큰의 유효성을 검증하기 하지 않기 때문에 비정상적인 서비스 접근이 가능하다.
=> 인증이 필요한 단계에서는 access token을 서버로 보내 검증 단계를 거치도록 한다.또한 Refresh token을 http-only 속성이 부여된 쿠키에 저장하여 자바스크립트 환경에서 접근할 수 없도록 한다.
[ problem 3 ]
추가적으로 localStorage에 "TOKEN"이라는 keyname으로 access token을 저장해서 사용하는 방식은 토큰 탈취의 위험성이 있다.
=> 데이터베이스에 각 사용자에 1대1로 맵핑되는 Access Token, Refresh Token 쌍을 저장한다. 정상적인 사용자는 기존의 Access Token으로 접근하며 서버측에서는 데이터베이스에 저장된 Access Token과 비교하여 검증한다. 공격자는 탈취한 Refresh Token으로 새로 Access Token을 생성한다. 그리고 서버측에 전송하면 서버는 데이터베이스에 저장된 Access Token과 공격자에게 받은 Access Token이 다른 것을 확인한다. 만약 데이터베이스에 저장된 토큰이 아직 만료되지 않은 경우, 즉 굳이 Access Token을 새로 생성할 이유가 없는 경우 서버는 Refresh Token이 탈취당했다고 가정하고 두 토큰을 모두 만료시킨다. 공격자의 토큰 역시 만료됐기 때문에 공격자는 정상적인 사용자의 리소스에 접근할 수 없다.
Refresh Token을 사용하는 방식도 토큰 탈취의 위협으로부터 완전히 안전하다고 할 수는 없으나, 현재로서는 가장 안전한 대안이라고 생각합니다. 아직 위의 방식으로 로그인 방식을 수정하지 않았습니다. 로그인 방식을 변경하는대로 자세한 내용을 글로 정리해 올리도록 하겠습니다.
참고 문서
https://stackoverflow.com/questions/57650692/where-to-store-the-refresh-token-on-the-client
https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/
https://hudi.blog/refresh-token/
'Web Development > Front End 관련 개념 정리' 카테고리의 다른 글
Local Storage & Session Storage란? (0) | 2023.08.16 |
---|---|
webpack이란? (0) | 2023.08.16 |
[OAuth2.0] OAuth 2.0이란? (0) | 2023.08.15 |
[ JWT ] JWT(Json Web Token) 이란? (0) | 2023.08.15 |
git & github 사용하기 (0) | 2023.04.12 |