로딩...
jwt
JSON Web Token
질문
jwt 를 사용할때 refresh token 이 필요한지?
- 아래와 같은 형태의 사용도 가능하지 않은가?
- jwt 토큰은 expire 이후에도 decode 자체는 가능하다
- 만료된 token 을 decode 해서 expire 된 시간을 확인한다
- 발급했어야 할 refresh token 의 expire 기간내라면 access token 을 갱신한다
- 위와같은 시나리오는 access token 의 expire 기간을 refresh token 과 같게한것과 같다
- refresh token 은 서버가 아니라 신뢰 클라이언트에 저장되어야한다
- [oauth#신뢰, 비신뢰 클라이언트 인증](oauth#신뢰, 비신뢰 클라이언트 인증)
- 안전하게, 만료된 access token 에 만료시에 해당 access token에 대해서 한번만 갱신이 가능한 refresh token 을 만들기 위해서는 jwt 로 만들어서는 안될 것으로 생각된다
- jwt 는 revoke 가 불가능하기 때문
- 때문에 이를 성취하기 위해서는 jwt 를 사용하더라도 db에 저장후에 사용(아무 해시와 다를게 없이 사용)되면 파기하는 방법으로 사용되는게 안정성이 높을 것으로 생각된다
- 이를 분리하는건 탈취에 대한 방어인데 access_token 과 refresh_token 을 같은 비신뢰 저장소에 저장해 둔다면 동일하게 탈취되므로 의미가 없다
- 구현이슈는 복잡하고 생각할게 많으므로 mvp 를 구현하는데 방해가 될 수 있으므로 보안 요구사항을 생각하고 진행하는 것이 좋겠다
refresh token 구현 아이디어
- 클라이언트에 저장 하지 않고 access token 에 대해서 한번만 발생할 수 있는 refresh token 발급 api 를 생성한다
sequenceDiagram
actor app as app or web
participant service as service server
participant auth as auth server
participant db as database
app ->> auth: 인증 요구
auth ->>+ app: access_token
app --> app: expired
app ->>- service: request with expired auth
service ->> app: 401
app ->> auth: /auth/refresh/token=[access_token]
auth --> auth: if 만료 && 만료시간이 특정 기간이내
auth -->> db: find access_token
db -->> auth: db에 없음
note right of db: db에 있는 경우 이미 발행된 케이스
auth -->> db: save access_token
auth --> auth: renew access_token
auth ->> app: access_token
- 이런 구현인 경우 만료된 토큰을 가지고 /auth/refresh 를 지속적으로 요구하면 db 가 지속되어 기존 스펙보다 나은게 없는 것 같다
- refresh token 을 함께 발급하고 이를 안전한 저장소에 클라이언트에 저장하는게 요구사항인만큼 이것이 관철되는것이 좋을것으로 생각한다
- 최종적으로 구현을 정리하여 단순화하면 아래와 같다
sequenceDiagram
title: 리프레시 토큰교환 한번만 진행
actor app as app or web
participant be as backend
participant db as database
app ->> be: 인증 요구
be ->> db: save refresh_token with user
be ->>+ app: access_token, refresh_token
app --> app: expired
app ->>- be: expired token
be ->> app: 401
app ->> be: /auth/refresh?access_token&refresh_token
be --> be: if refresh token 만료되지 않은 경우
be ->> db: try to delete refresh_token
db --> app: 401: db에 이미 없는 경우 이미 교환한 케이스
db ->> be: 삭제 성공: 토큰이 있는 경우
be --> be: renew access_token, refresh_token
be ->> db: save refresh_token with user
be ->> app: access_token, refresh_token
- access token 을 salt 로 써도 좋을 듯
하이브리드 웹
- 토큰 갱신을 어디서할 것인지에 따라서 보안레벨이 달라진다
- 인증된 SSR 사용시 에는 401 화면 노출을 피할 수 없다
- 웹뷰에서 갱신을 하게 되면 중복 구현이 발생한다
갱신시 webview 를 새로 고침
sequenceDiagram
actor app as app
actor wv as webview
participant be as backend
participant db as database
app ->> be: 인증 요구
be ->> db: save refresh_token with user
be ->> app: access_token, refresh_token
app ->> wv: open with access_token
wv ->> be: expired token
be ->> wv: 401
wv ->> app: 401
app ->> be: /auth/refresh?access_token&refresh_token
be --> be: if refresh token 만료되지 않은 경우
be ->> db: try to delete refresh_token
db --> app: 401: db에 이미 없는 경우 이미 교환한 케이스
db ->> be: 삭제 성공: 토큰이 있는 경우
be --> be: renew access_token, refresh_token
be ->> db: save refresh_token with user
be ->> app: access_token, refresh_token
app ->> wv: open with access_token
- weview 를 열때 access_token 을 전달
- 만료시에는 app 에서 토큰 리프레시를 담당하고 웹뷰를 새로고침한다
- 구조가 단순한 대신 새로고침 되므로 경험이 떨어진다, 스크롤 위치 이슈
갱신시 webview 에서 갱신후 후 앱에 전달
sequenceDiagram
actor app as app
actor wv as webview
participant be as backend
participant db as database
app ->> be: 인증 요구
be ->> db: save refresh_token with user
be ->> app: access_token, refresh_token
app ->> wv: open with access_token
wv ->> be: expired token
be ->> wv: 401
wv ->> app: get refresh_token
app ->> wv: refresh_token
wv ->> be: /auth/refresh?access_token&refresh_token
be --> be: if refresh token 만료되지 않은 경우
be ->> db: try to delete refresh_token
db --> app: 401: db에 이미 없는 경우 이미 교환한 케이스
db ->> be: 삭제 성공: 토큰이 있는 경우
be --> be: renew access_token, refresh_token
be ->> db: save refresh_token with user
be ->> wv: access_token, refresh_token
wv ->> app: store access_token & refresh_token
wv ->> be: retry failed api
- webview 의 새로고침은 발생하지 않는다, 무한 스크롤 등 구현이 다소 유리
- app 에서도 로직이 존할 것이므로 full webview 기반이 아니라면 refresh token 로직은 중복구현된다
- 좀더 seamless 한 경험을 줄 수 있지만 인증된 SSR 사용이 안되는건 마찬가지
- ssr 은 포기해야지 싶다
앱에서 갱신 + 웹뷰에 통신으로 브로드 캐스팅
flowchart LR
app --"5: injectJavascript(access_token)"--> webview0
app --"5: injectJavascript(access_token)"--> webview1
app --"5: injectJavascript(access_token)"--> webview2
app --"5: injectJavascript(access_token)"--> webview3
webview3 -- 2: 401 --> app
webview3 -.-> service
service --1: 401--> webview3
app --3: /refresh/token --> service
service --4: access token --> app
webview0 --6: 갱신--> webview0
webview1 --6: 갱신--> webview1
webview2 --6: 갱신--> webview2
webview3 --6: 갱신--> webview3
- ssr 을 포기하면 가장 심리스한 구현이 아닐가 싶