access token, refresh token 그리고... jwt? 그냥 unique token

반응형

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI0YTlhYjYxLWJhZDktMTFlYi05ZTg5LTdiY2Q3ZDNlNWQyOCIsImlhdCI6MTYyMTY3Mjk3Nn0.6yEtyy0L9c9p2RmLQiardzXFd1eX55gp82mX8BYa1lw.U2FsdGVkX1/VJ+g+cOfBprUS47+uvC46T9Y03B2F3/PW3gTl4H/Q+f64bDUdXv5pUeWx6qNAUtfkWK9nNeHQS4nFtk3ynpGpKy6PPnc6g5Q=

 

fullToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI0YTlhYjYxLWJhZDktMTFlYi05ZTg5LTdiY2Q3ZDNlNWQyOCIsImlhdCI6MTYyMTY3Mjk3Nn0.6yEtyy0L9c9p2RmLQiardzXFd1eX55gp82mX8BYa1lw.U2FsdGVkX1/VJ+g+cOfBprUS47+uvC46T9Y03B2F3/PW3gTl4H/Q+f64bDUdXv5pUeWx6qNAUtfkWK9nNeHQS4nFtk3ynpGpKy6PPnc6g5Q=

 

ddd: U2FsdGVkX1/VJ+g+cOfBprUS47+uvC46T9Y03B2F3/PW3gTl4H/Q+f64bDUdXv5pUeWx6qNAUtfkWK9nNeHQS4nFtk3ynpGpKy6PPnc6g5Q=

 

jwt: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImI0YTlhYjYxLWJhZDktMTFlYi05ZTg5LTdiY2Q3ZDNlNWQyOCIsImlhdCI6MTYyMTY3Mjk3Nn0.6yEtyy0L9c9p2RmLQiardzXFd1eX55gp82mX8BYa1lw

 

userId: b4a9ab61-bad9-11eb-9e89-7bcd7d3e5d28

 

verifyDDD ddd: U2FsdGVkX1/VJ+g+cOfBprUS47+uvC46T9Y03B2F3/PW3gTl4H/Q+f64bDUdXv5pUeWx6qNAUtfkWK9nNeHQS4nFtk3ynpGpKy6PPnc6g5Q=

 

verifyDDD secret: 534e5016380647db856b26895ce70f0f

 

verifyDDD userKey: b3263fb0-bad9-11eb-8616-41ea194afbb3

 

verifyDDD decrypted: 1621673002342-b3263fb0-bad9-11eb-8616-41ea194afbb3

 

timestamp: 1621673002342

 

 server time: 1621673004887

 

보통 accessToken 과 refreshToken 을 사용한다.

github 이나 foursquare 는 refreshToken 을 사용하지 않는다고 한다.

개인적으로는 accessToken 은 순수하게 secret 만으로 issue, verify 하여 복호화 되도록하고

refreshToken 은 DB 에도 저장하여 accessToken 다시 요청시에 DB 접근하는 방향도 괜찮은 방식 같다.

하지만 결국 stateless 의 경우 매 요청시 마다 id 정도만 가지고 DB 에서 user 의 여러 정보를 가져오는 과정을 거친다.

따라서 그냥 간단하게 accessToken 을 발급하고 아니 이름도 그냥 token 이라고 하고 어차피 DB 는 매번 select 하기 때문에

해당 token 을 DB 에 저장해서 해당 token 을 가지고 select 하는 방식이 가장 심플해보인다.

대신 token 은 발행시에 unique 한 값이 되도록 하자. token 의 만료는 그냥 DB 에서 지우면 되는 식.

jwt 을 굳이 사용할 이유도 없다. jwt 의 목적이 DB 없이 secret 만으로 jwt 내에 모든 정보를 저장하는 것을 목적으로 하는 것이기에

굳이 복잡한 jwt 이 필요가 없다. 그냥 unique 한 난수 token 이면 된다.

클라이언트 쪽에 저장할 장소는 어디든 알아서 하면 되고 stateless 이기 때문에 매 요청시  Bearer 에 담아서 요청하면 될 것.

access token , refresh token 사용하면 조금은 보안상 나을 것도 같지만... 굳이 그럴 필요도 없다. 어차피 refresh token 털려도 마찬가지이다.

Authorization: Bearer <credentials> 로 인증타입을 Bearer 로 하여도 반드시 JWT 모양일 필요는 없고 OAuth 모양이어도 되는데 OAuth 의 토큰모양은 꼭 정해진 것이 아니기 때문에 이렇게 해도 될 것.

 

결국 토큰이 탈취되면 다 털리게 된다.

리프레쉬 토큰이든 액세스 토큰이든 털리면 다 털린다. 토큰만 던져주면 인증으로 인식하기 때문.

따라서 다른 방법을 좀 보완해보려한다.

jwt 양식이 아닌 그냥 토큰을 사용해도 해당 토큰만 헤더에 담아 보냈을 때 인증으로 간주하면 토큰만 털면 다 털린다고 볼 수 있다.

그래서 항상 토큰이 변경되도록 하는 것을 생각해볼 수 있다. 항상 다른 토큰을 서버에서 내려주는 방식은 부하가 클 수 있다.

그리고 그렇다고 하여 해당 토큰이 한번 털리면 매번 갱신된 걸 그 털어간 토큰으로 동일하게 받을 수 있지 않겠는가?

 

그래서 최초 인증시에 secret 을 한번 내려주어서 해당 secret 을 가지고 클라이언트 측에서 암호화한 값을 함께 보내는 방식이 좋아보인다.

이때 jwt 형태를 가지는 것이 좋아 보인다. 왜냐하면 user id 와 같이 해당 사용자를 특정할 내용은 담겨 있어야 하기 때문이다.

그냥 서버에 저장된 토큰으로 DB 에서 select 하는 것이 아니기 때문이다.

jwt 형태가 aaa.bbb.ccc 에서 aaa 인 헤더에 토큰 타입과 암호화 방식 정보가 있고 bbb 에 페이로드 가 담기고 ccc 에 aaa 와 bbb 를 암호화 방식에 따라 사인한 정보가 담긴다. aaa 와 bbb 는 base64 로 인코딩 되어 있다. bbb 에 사용자 아이디 정보가 들어가서 select 할 수 있도록 해준다.

 

여기에 ddd 라는 정보를 추가로 넣는다.

uuid v1 (시간정보 와 mac address 정보가 담긴 키값) 을 secret 으로 양방향 암호화 하여서 실어준다.

그러면 서버에서는 aaa.bbb.ccc.ddd 를 받게 되고

aaa.bbb.ccc 는 jwt verify 를 통해 동일하게 적용하고 bbb 의 정보를 바탕으로 DB 에서 secret 과 uuid 중 mac address 부분을 가져온다. ddd 를 secret 으로 복호화 하여 mac address 부분이 일치하는지 검증하고 timestamp 부분으로 30초 안에 만들어진 uuid v1 인지 검증한다. 매 요청시 마다 클라이언트에서 ddd 를 만들어 전송하므로 이 값은 매번 변경되기 때문에 누군가 탈취해도 다시 사용할 수 없다.

 

aaa.bbb.ccc 에서 서버측의 jwt 을 위한 secret 으로 한번 검증을 거치고 ddd 를 통해 동일한 mac address 를 가진 기기에서 보낸 것이 맞는지 그리고 timestamp 가 30초 이내에 전송한 것이 맞는지를 검증한다.

 

사실 ddd 를 만들 때 uuid v5 를 사용해도 괜찮다.

timestamp 값을 input string 으로 하고 namespace 를 맥주소라던가 이렇게 해주면 uuid v1 과 다를바가 없고 오히려 명확할 수도 있겠다.

 

게다가 input string 을 timespace + 특정값 그리고 namespace 도  맥주소나 특정 기기 고유값 + 특정값 형식으로 구성할 수도 있다.

그러면 secret 으로 암호화 된 것을 복호화 하여 만료시간 검증 그리고 기기 검증이 될 수 있다.

 

jwt 의 만료일을 사용할 수 없는 이유는 매번 클라이언트에서 변경하는 것이 아닌 서버측에서 발행하는 것이기 때문이다. jwt 은 한번 서버에서 발행하고 변경되는 것이 아니기 때문.

 

secret 을 최초 인증시 클라이언트로 내려주면 그것으로 암호화 해서 ddd 를 만들어 보낼 경우 이것의 유효성만 통과되어도 충분히 인증의 보안성이 지켜질 수 있다. 그러나 매번 변하는 값을 넣어주어야 하기 때문에 매번 변하는 값이 서버측에서 복호화하였을 때 유효하다는 것을 입증하려면 무언가 규칙적인 특정값이 존재해야 한다. 그것을 기기라던가 단말을 특정할 수 있는 고유값과 특정값으로 해주면 될 것. 일종의 해싱된 값을 secret 으로 암호화 할 것이기 때문에 해싱된 값 앞에 timestamp 정보는 구분자로 넣어주는 것이 좋다. timestamp 값과 고유값이 합쳐져 해싱된 값이 기대하는 값이 되어야 하기 때문. 고유값 부분을 DB 에 저장해놓으면 된다.

 

1620094851878-고유값 => v5 uuid , 고유값은 그대로 서버측에서 가지고 있으면 개인정보가 될 수도 있으므로 단방향 해싱 암호화를 적용해서 1620094851878-해싱암호화된고유값 => v5 uuid 로 해준다. 최초 인증시 클라이언트는 해싱암호화된고유값을 함께 던져주어야 하고 서버는 secret 을 응답해주어야 한다. 그리고 서버는 해싱암호화된고유값과 secret 을 저장해둔다.

클라이언트는 timestamp-해싱암호화된고유값 을 v5 uuid 로 만든 후 secret 으로 암호화 (최종암호화된값)하여 timestamp 와 함께 서버로 요청시마다 ddd 로 보낸다. 

최종암호화된값-timestamp 가 ddd 에 들어갈 값이 될 것.

서버측에서는 최종암호화된값을 복호화하여 v5 uuid 를 얻고 timestamp 값을 DB 에 저장된 해싱암호화된고유값과 함께 넣어서 v5 uuid 를 만들어 전달된 것과 동일한지 검증하면 된다. 

 

해싱암호화된고유값 을 꼭 기기나 단말의 고유값이나 맥주소 같은걸 사용하지 않고 그냥 uuid v4 로 생성하여 클라이언트 쪽에 저장해놓고 사용해도 될 것이다. 즉 v4 로 키생성하고 서버에 저장하고 서버는 secret 을 클라이언트에 주고 클라이언트는 v4 키와 timestamp 로 v5 를 만들고 v5 와 timestamp 를 secret 으로 암호화해서 서버에 보내면 서버는 bbb 에서 해당사용자 아이디로 secret 과 v4 키를 가져오고 secret 으로 복호화하여 timestamp 와 v5 를 얻고 timestamp 와 v4 를 통해 v5 를 얻어 동일한지 확인. timestamp 로 해당 키는 30초 내에 만들어진 것인지도 확인.

 

기기고유값일 경우라서 v5 로 만드는걸 고려했는데 그러고 보니 v4 를 클라이언트에 저장하고 사용하는 방식이라면 그냥 secret 암호화 복호화만 해도 될 것 같다. 즉 timestamp-v4 를 secret 으로 암호화해서 서버로 보내주고 서버는 secret 으로 복호화하고 v4 가 동일한지 확인하고  timestamp 가 30초 이내의 것인지만 확인.

 

bbb 의 페이로드에 사용자 아이디 값은 넣어주자. DB 에서 셀렉트할 기준값 정보는 있어야 한다. 어차피 사용자 아이디 값은 공개되는 값이니깐.

 

 

 

zzossig.io/posts/etc/what_is_the_point_of_refresh_token/

blog.ull.im/engineering/2019/02/07/jwt-strategy.html

velog.io/@tlatldms/서버개발캠프-Refresh-JWT-구현

tansfil.tistory.com/58?category=475681

tansfil.tistory.com/59

tansfil.tistory.com/60?category=475681

 

 

 

 

 

[uuid] 128비트 데이터의 조각들이다. 128/4 = 32 hexadecimal digit.
- 를 제외하면 hexadecimal (0 ~ f , 0 ~ 15 , 0000 ~ 1111 , 4bit) 가
총 32개로 되어 있는 것을 볼 수 있다.
UUID v1 : 7b5021d0-ac12-11eb-ad1d-b91e0807627b
UUID v4 : a0b50392-9dad-47f4-a7e2-793ad8db36ae

v1: Uniqueness
호스트 컴퓨터의 MAC 주소와 현재 날짜와 시간의 조합. 그리고 uniqueness 를
위한 다른 요소도 추가된다. 같은 시각에 같은 컴퓨터에서 생성하지 않는 한
완벽히 unique 함을 보장한다. 또한 랜덤 비트로 인해 완전히 같은 시각, 같은 컴퓨터
일지라도 같을 확률은 거의 없다. 이 uniqueness 위해 anonymity (익명성) 을 희생한다.
MAC address 와 생성 시간을 사용하기 때문에 이를 누군가가 유추할 가능성도 있다.
7b5021d0-ac12-11eb-ad1d-b91e0807627b 중에서 맨 앞의 - 저의 값들 (7b5021d0) 은
각 컴퓨터에서 일정하게 증가하는 값이다. 상대적으로 생성된 시각을 추측할 수 있다.
그 뒤의 값들은 생성한 컴퓨터가 같다면 동일하다.

v4: Randomness
좀더 이해하기 쉽다. 그냥 랜덤으로 생성된다. 중복될 확률도 거의 없다고 볼 수 있다.
2 의 128 승의 조합이다. 몇년동안 초당 1조개의 id 를 생성하지 않는다면 중복될 확률은 없다.
하지만 은행거래나 의료시스템 같은 경우라면 유니크한 상수를 추가할 필요는 있다.

v5: Non-Random UUIDs
랜덤이 아닌 unique ID 를 얻고 싶다면 v5 를 사용.
Input string 와 Namespace 를 받아서 SHA1 hashing algorithm 으로 변환된다.
namespace 는 고정된 uuid 값이고 input string 은 다른 애플리케이션들 간에
차이를 주는 임의의 값이다. 이 둘을 조합한 값을 해싱한 것이다.
중요한 포인트는 v5 는 일관성이 있다는 점이다.
input string + namespace 가 같다면 항상 같은 값을 준다.
사용자의 정보를 DB 가 아닌 id 에 넣어주고 싶다면 이것을 사용하면 좋을 것이다.
하지만 id 들은 랜덤이 아니라서 uniqueness 는 알아서 고려해야 한다.

 

반응형

'REACT & NODE' 카테고리의 다른 글

리덕스 스타일링 가이드 요약  (0) 2021.05.10
redux toolkit 관련 참고용 코드들  (0) 2021.05.09
redux-requests  (0) 2021.04.21
package-lock.json vs yarn.lock  (0) 2021.04.20
oauth2  (0) 2021.04.03

댓글

Designed by JB FACTORY