작년에 JSON Web Token(JWT)에 대한 글을 올렸다. 당시에는 JWT 처음 사용해 보면서 적은 글이라 그제야 약간 이해한 상황이었지만 시간이 지나면서 더 알게 된 부분도 있고 스펙상 달라진 부분도 꽤 생긴 관계로 유지보수 차원에서 추가로 글을 쓰게 됐다.
JSON Web Token에 대해서...
이전 글을 쓴 지 1년도 채 지나지 않았지만, 그 사이에 많은 내용이 달라졌을 정도로 당시의 JWT는 초기 상태였다. 작년에 사용할 때는 스펙의 버전이 25였지만 버전이 32까지 오른 뒤 현재는 RFC 7519가 되었다. (버전 32는 읽어보았지만, RFC 7519는 아직 못 읽어봤다.)
JWT는 토큰 기반의 인증이 많아지면서 HTTP Authorization 헤더나 URI 쿼리 파리미터 등 공백을 사용할 수 없는 곳에서 가볍게 사용할 수 있는 토큰으로 설계되었다.(발음은 "jot"이다.) 이름에서도 알 수 있듯이 JWT는 JSON 객체를 사용하고 여기에 서명하거나 암호화를 할 수 있다.
JWT의 구조
JWT는 API 인증 등에 사용하는 토큰이므로 스펙에 인증하는 방법에 대한 내용은 적혀있지 않고 토큰을 어떻게 만들고 검증하는지에 대해서 나와 있다. 뒤에서 인증에 대해서도 약간 얘기를 하겠지만 우선 JWT 자체에 대해서 알아보자. JWT에 대해서 알고자 한다면 Auth0에서 만든 JWT 사이트가 참고하기 제일 좋다. 이 사이트에서 JWT 토큰을 테스트해보거나 구조도 파악해 볼 수 있고 언어별로 추천 라이브러리와 지원상태를 한눈에 볼 수 있어서 믿고 쓸 수 있고 JWT에 대한 보안 취약점도 잘 보고되고 있다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJKb2huIERvZSIsImV4cCI6MTQzNDI5MDQwMDAwMCwidXNlcm5hbWUiOiJqb2huIiwiYWdlIjoyNSwiaWF0IjoxNDM0Mjg2ODQyNjU0fQ.jzvwdy5mQuzkEenNEFeRlSytvB7-X7NVAvtTDr1jP0Q
JWT 토큰은 위와 같은 모습을 하고 있다. 자세히 보면 마침표(.
)를 구분자로 세 부분으로 나누어져 있는데 이 3가지 부분을 각각 JOSE 헤더(JSON Object Signing and Encryption), JWT Claim Set, Signature라고 부른다.
jwt.io 사이트의 디버거로 JWT 토큰을 디코딩한 모습니다. 이해하기 쉽게 각 부분의 색을 다르게 표시했는데 빨간색 부분이 JOSE 헤더이고 분홍색 부분이 JWT Claim Set이고 하늘색 부분이 Signature이다. 즉, 오른쪽의 값을 인코딩해서 왼쪽의 JWT를 만들고 왼쪽의 JWT를 디코딩하면 우측의 JSON이 나오게 된다. 각 값의 의미는 나중에 살펴보기로 하고 node.js로 직접 만드는 방법을 먼저 살펴보자.
JOSE 헤더
new Buffer('{"alg":"HS256","typ":"JWT"}').toString("base64")
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
JOSE 헤더는 JWT 토큰을 어떻게 해석해야 하는지를 명시한 부분이다. 이 헤더를 열어보고 이어서 설명할 JWT Claim Set이나 Signature를 어떻게 해석할지를 알 수 있다. JOSE 헤더의 값이 '{"typ":"JWT","alg":"HS256"}'
라고 한다면 이를 base64로 인코딩해서 JOSE 헤더를 생성한다. 이 예제에서는 JWT 타입이고(옵션 파라미터로 MIME 미디어 타입을 의미하지만 JWT인 경우 JWT
라고 적기를 추천하고 있다.) 토큰에 사용한 알고리즘은 HS256 즉, HMAC SHA-256이라는 의미이다. HS256 외에도 HS512, RS256(RSASSA SHA-256), ES256(ECDSA P-256 curve SHA-256) 등의 알고리즘을 사용할 수 있다.
JWT Claim Set
new Buffer('{"iss":"John Doe","exp":1434290400000,"username":"john","age":25,"iat":1434286842654}').toString("base64")
// eyJpc3MiOiJKb2huIERvZSIsImV4cCI6MTQzNDI5MDQwMDAwMCwidXNlcm5hbWUiOiJqb2huIiwiYWdlIjoyNSwiaWF0IjoxNDM0Mjg2ODQyNjU0fQ==
JWT Claim Set은 실제 토큰의 바디로 토큰에 포함할 내용을 넣는다. JWT는 토큰을 디코딩해서 바로 값을 확인할 수 있는 구조로 되어 있어서 데이터베이스 조회할 필요없이 사용자 아이디 등을 여기에 담아서 바로 사용할 수 있다. 여기서 JWT Claim Set의 내용이 '{"iss":"John Doe","exp":1434290400000,"username":"john","age":25,"iat":1434286842654}'
라고 한다면 JOSE 헤더와 같게 base64 인코딩을 한다. 마지막에 붙은 ==
은 padding인데 여기서는 패딩을 제거하고 ...yNjU0fQ
까지만 토큰으로 사용한다.
Signature
앞에서 보았듯이 JOSE 헤더와 JWT Claim Set은 암호화를 한 것이 아니라 단순히 JSON문자열을 base64로 인코딩한 것뿐이다. 그래서 누구나 이 값을 다시 디코딩하면 JSON에 어떤 내용이 들어있는지 확인할 수 있다. 토큰을 사용하는 경우 이 토큰을 다른 사람이 위변조할 수 없어야 하므로 JOSE 헤더와 JWT Claim Set가 위변조되었는지를 검증하기 위한 부분이 Signature 부분이다. JOSE 헤더와 JWT Claim Set는 JOSE 헤더와 JWT Claim Set를 base64로 인코딩해서 만든 두 값을 마침표(.
)로 이어 붙이고 JOSE 헤더에서 alg
로 지정한 알고리즘 HS256
즉, HMAC SHA-256으로 인코딩하면 JWT 토큰의 세 번째 부분인 Signature를 만든다.
var crypto = require('crypto');
var secretKey = 'secret';
var headerAndClaimSet = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJKb2huIERvZSIsImV4cCI6MTQzNDI5MDQwMDAwMCwidXNlcm5hbWUiOiJqb2huIiwiYWdlIjoyNSwiaWF0IjoxNDM0Mjg2ODQyNjU0fQ';
crypto.createHmac('sha256', secretKey).update(headerAndClaimSet).digest('base64')
// jzvwdy5mQuzkEenNEFeRlSytvB7+X7NVAvtTDr1jP0Q=
headerAndClaimSet
값이 앞에서 만든 JOSE 헤더와 Claim Set을 마침표로 이어 붙인 것이고 이를 HMAC SHA-256으로 암호화해서 base64로 표현하자 앞에서 본 Signature와 같은 문자열이 나왔다.(여기서도 padding은 제거한다.) 공격자가 서명을 위변조할 수 없도록 비밀키를 사용해서 서명하고 당연히 이 비밀키는 외부에 노출되면 안 된다.(여기서는 비밀키로 secret
이라는 값을 사용했다.)
이렇게 만든 3부분을 마침표(.
)로 이어 붙이면 JWT 토큰이 완성된다.
JWT Claim Set
JWT의 구조를 설명하기 위해서 앞에서는 직접 토큰을 만들었지만 실제로 사용하게 되면 언어별로 있는 JWT 라이브러리를 사용해서 토큰을 만들 것이므로 이 과정을 직접 수행하는 경우는 별로 없다. JWT 사이트에 언어별 추천 라이브러리가 있고 현재 지원상황까지 표시되어 있으므로 비교해서 사용하면 된다. 이런 라이브러리는 취약점에 대한 대처가 중요하므로 세부 내용을 자세히 아는 것이 아니라면 그냥 여기 나와 있는 라이브러리를 사용하기를 추천한다. 이전에 작성한 글에서는 Node.js JWT 라이브러리로 jwt-simple을 사용했지만, 개발이 거의 진행되고 있지 않아서 지금은 jsonwebtoken으로 갈아탔다.
JWT에서 토큰의 정보를 클레임이라고 부르기 때문에 이 정보를 모두 가지고 있는 바디 부분을 Claim Set이라고 부르고 Claim Set은 키 부분인 Claim Name과 값 부분인 Claim Value의 여러 쌍으로 이루어져 있다. Claim Name으로 사용 가능한 값에는 3가지 분류가 있는데 등록된 클레임 이름(Registered), 공개 클레임 이름(Public), 비밀 클레임 이름(Private)이다. 등록된 클레임 이름은 IANA JSON Web Token Claims에 등록된 이름이고 필수값은 아니지만 공통으로 사용하기 위한 기본값이 정해져 있다. 아래 목록이 등록된 클레임 이름인데 모두 선택사항이다.
iss
: 토큰을 발급한 발급자(Issuer)sub
: Claim의 주제(Subject)로 토큰이 갖는 문맥을 의미한다.aud
: 이 토큰을 사용할 수신자(Audience)exp
: 만료시간(Expiration Time)은 만료시간이 지난 토큰은 거절해야 한다.nbf
: Not Before의 의미로 이 시간 이전에는 토큰을 처리하지 않아야 함을 의미한다.iat
: 토큰이 발급된 시간(Issued At)jti
: JWT ID로 토큰에 대한 식별자이다.
공개된 클레임이름은 토큰에서 사용하기 위해서 정의했지만, 충돌을 방지하기 위해서 공개된 이름이고 비밀 클레임이름은 서버와 클라이언트가 협의로 사용하는 이름을 의미한다.
JWT의 사용
JWT가 다른 토큰하고 가장 다른 부분은 토큰 자체가 데이터를 가지고 있다는 점이다. API 서버를 직접 구현한 적이 많지는 않지만, 일반적으로 토큰 기반의 인증을 구현한다면 API 요청 시 헤더나 파라미터에 엑세스토큰을 가져오도록 하고 이 토큰을 보고 인증한다.(서비스에 따라서 앱 ID나 비밀키를 같이 사용하기도 하지만 여기서는 JWT 범주가 아니므로 여기서는 엑세스토큰만 얘기한다.)
일반적인 토큰의 흐름을 생각한다면 API 요청 시에 들어온 토큰을 보고 이 토큰이 유효한지 확인하게 된다. 보통은 데이터베이스에 토큰을 저장해 놓고 만료시간이나 토큰의 사용자 등을 저장해 놓고 유효한 토큰인지 등을 검사하고 유효한 경우 해당 사용자라고 인식하고 이 사용자의 권한으로 사용할 수 있는 정보를 조회하게 된다. 요청마다 데이터베이스를 조회하는 것은 비용이 꽤 크므로 캐시서버 등을 두어 성능을 높이기도 한다.
JWT의 경우는 토큰을 받아서 서명으로 유효한 토큰인지 검증을 한 뒤에 유효하다고 판단하면 클레임셋을 디코딩해서 토큰에 담긴 데이터를 열어본다. 앞에서 보았듯이 클레임셋의 JSON에 만료시간 등이 담겨있으므로 토큰이 사용가능한지 검사를 하고 이상이 없으면 바로 사용한다. 토큰의 사용자 아이디 등이 클레임셋에 담겨있다면 데이터베이스나 캐시를 조회할 필요없이 바로 애플레이케션이서 사용자를 확인하고 정보를 조회할 수 있다. 대신 다른 토큰보다 길이가 좀 길다는 문제는 있다.
이 차이는 서버 개발 쪽에서는 상당히 큰 차이를 만들어내는데 토큰만 가지고 유효성 검사나 사용자 식별까지 할 수 있으므로 서버를 무상태로 유지할 수 있다는 점이다. 데이터베이스나 캐시 없이 로직만으로 인증이 가능하므로 애플리케이션 서버를 필요에 따라 늘릴 수 있다.
JWT를 사용하다 보니 몇 가지 주의할 점이 있다.
- 앞에서 보았듯이 클레임셋은 암호화하지 않는다. 그래서 서명 없이도 누구나 열어볼 수 있기 때문에 여기에는 보안이 중요한 데이터는 넣으면 안 된다. base64로 인코딩해서 사용하다 보면 이 부분을 간과하기 쉬운데 필요한 최소한의 정보만 클레임셋에 담아야 한다.
- 인코딩 특성상 클레임셋의 내용이 많아지면 토큰의 길이도 같이 길어진다. 그래서 편하다고 너무 많은 정보를 담으면 안 된다. 앞에서 정의된 클레임네임이 전부 약자를 사용하는 이유도 JWT를 최대한 짧게 만들기 위함이다.
- 토큰을 강제로 만료시킬 방법이 없다. 서버가 토큰의 상태를 가지고 있지 않고 토큰 발급 시 해당 토큰이 유효한 조건이 결정되므로 클라이언트가 로그아웃하더라도 해당 토큰을 클라이언트가 제거하는 것뿐이지 토큰 자체가 만료되는 것은 아니다. 만약 이때 누군가 토큰을 탈취한다면 해당 토큰을 만료시간까지는 유효하게 된다. 많은 고민을 해보았지만 무상태를 유지하면서 이 부분을 같이 해결할 방법은 존재하지 않으므로 서비스에서 이 부분이 문제가 된다면 선택을 해야 한다고 본다.
궁금한게 있어서 질문 드립니다~^^
토큰 만료 시간을 같이 넣어서 주는데 시간을 넣어서 주는 부분이 JWT Claim Set 부분이고, 그러면 저 시간은 한번 정하면 고정이 되는건가여? 저 시간이 클라이언트와 서버와 접속할때마다 시간을 연장해 주면, 최종적으로 JWT token 자체가 바뀌것이 아닌가해서여? 그렇게 되면 클라이언트가 매번 접속때마다 다른 JWT 토큰을 받는건지요? 이 부분은 어떤 로직인지 잘 이해가 가지 않아서 질문드립니다~~^^ 제가 이번에 JWT를 적용하려다 보니 이해가 잘 안되는 부분이 많아서...
JWT 자체는 토큰에 대한 부분만 있습니다. 말씀하신 부분은 토큰 리프레시 부분에 관한 것인데 사용할때마다 토큰을 리프레시 해야한다면 만료시간을 아주 짧게주고 만료시간을 갱신하면서 계속 토큰을 새로 발급해야 합니다.
리프레시 관련해서 정확한 내용이 정해져 있는 것은 아닙니다만 일반적인 API를 보면 최초 발급시 access token과 refresh token 2개를 발급하고 access token으로 API를 사용하다가 만료시간이 지나면 만료시간을 길게 준 refresh token을 이용해서 access token을 다시 발급합니다. 여기서 refresh token에 대한 재발급 이슈도 남게 되고 토큰 만료시간 길이에 대한 이슈등이 있는데 이는 서비스의 민감도에 따라 다른것 같습니다.
좋은 글 감사합니다.
보안관련 프로젝트 진행중 자료를 찾다가 이 곳에 방문하게 되었습니다.
먼저 상세한 좋은 강좌 감사히 잘 읽었습니다.
아직 jwt 에 대해 공부중이라 잘 몰라서 하는 질문이니 미리 양해 말씀 드립니다.
1. 서버에서 발급한 키를 클라이언트가 다음 요청을 위해서 키를 보관후 보내야 하는데 이 경우 키는 쿠키 외에 어디에 저장해야 하는지요?
2. 사용자의 pc내 쿠키나 네트웍에서 해당키가 전송되는걸 도청해서 동일키를 다른 곳이서 보낸다면 서버는 역시나 같은 클라이언트로 인식할것입니다. 쿠키조회나 네트웍 도청이 가능하다는 가정에서는 공격자가 "정보 수집후 재전송" 에는 동일한 문제점으로 보입니다.
3. 그냥 클라이언트가 아이디 암호 보내고 서버가 해당 아이디의 db에 저장된 암호 비교 하는 방식에 비해 다를게 없어 보입니다. 공격자가 암호 부분 수정 없이 아이디 영역에 다른 아이디 넣어 보낸들 의미가 없으니..
2 3 내용으로 보면 아이디암호 읽기쉽게 보내나 읽기어렵게 뭉쳐서 보내나의 차이인데 어차피 카피해 보낼 수 있다면 이는 보안에 별 도움이 안 될듯 느껴집니다.
4. 서버는 아이디, 암호, 임시암호, 임시암호 유효기간을 db에 저장하고 클라이언트는 아이디, 암호로 발급받은 임시암호, 유효기간을 쿠키로 저장후 상황따라 일반 텍스트로 보내고 서버가 db 조회후 처리하는 것과 비교해 보안적으로 어떤 장점이 있는지 궁금합니다. 이 임시암호가 123 인지 복잡한 영문자 100 자리인지는 중요치 않다고 봅니다.
4번 방식은 jwt 에 비해 한 작업당 사용자 검증을 위한 db조회 1회 더 하는 모양이지만 이후 서비스를 위해 어차피 db 작업을 해야하는 상황에선 큰 잇점으로 느껴지지 않습니다.
(더욱이 jwt에는 서버에 등록된 적합한 아이디라는 증빙만 있을뿐 해당 작업을 수행 할 수 있는 권한을 알기 위해선 결국 서버 db를 조회해야 하니까요.. 작업당 jwt를 발급하는 것은 현실적으로 불가능)
jwt 는 토큰만 보고는 사용자 아이디를 알 수 없다는 장점은 있어 보입니다만 현실적으로 토큰을 이용해서 서비스 요청하다면 제공되는 화면을 보고 결국 사용자 아이디를 알게 될 확율은 높을듯 합니다.
다만 저장된 사용자 정보가 굉장히 많으면 아이디 암호 임시암호 조회에 db 부하가 걸리니 jwt가 잇점이 될듯합니다.
반대로 jwt는 헤더가 길어지고 각 작업당 jwt 디코딩을 위한 cpu 부하가 생길듯 합니다.
성능을 떠나 제 좁은 소견으론 jwt가 암호+임시암호의 db방식에 비해서 보안적으로 장점을 못 느끼겠습니다.
프로젝트 중에 급히 고민하는 중이고 강좌의 jwt 내용에 대한 이해도 부족해서 제가 잘못 이해하고 있는 부분이 많습니다.
미리 사과 말씀 드리며 고견을 기다리겠습니다.
어렵게 공부하신 지식을 공유해 주심에 감사 말씀 드립니다
저도 보안쪽에 지식이 많은 편은 아닙니다만 질문을 보면서 약간 제가 가진 개념과 혼동되는 부분이 있어서 정리드리자면...
JWT를 얘기하기 이전에 이건 인증에 세션-쿠키 방식을 쓸 것이냐 API 토큰을 쓸것이냐의 문제입니다. 최초 로그인할 때는 어느쪽이든 아이디/패스워드를 보내고 이를 디비에서 조회해서 비교해봐야 합니다. 이는 무조건 실행되는 과정이므로 비교가 의미가 없고 이 이후에도 항상 아이디/패스워드를 주고 받는 것은 보안에 문제가 있으니 이후에는 세션-쿠키를 쓰거나 토큰을 써야 합니다.
여기서 세션을 쓰면 각 서버가 세션을 관리해서 sticky로 구성하거나 세션스토어를 따로 두어서 처리해야 합니다. API 토큰을 쓰는 경우에는 이를 디비에 넣어놓고 관리하게 됩니다.(아마 임시암호라는 것을 이 API 토큰의 의미로 쓰신게 아닌가 싶습니다.) 여기서는 API 토큰을 쓴다는 가정에서 그 토큰을 JWT로 쓰는 부분에 대한 얘기입니다.
토큰이란 것은 만료시간을 가진 임의의 문자열이므로 어떻게 생겼나는 크게 문제가 되지 않습니다. 같은 토큰이 오면 같은 사람으로 인식한다는 것은 같은 아이디/패스워드가 오면 로그인 시켜준다와 같은 얘기이므로 보안 문제라고 보진 않고 JWT든 일반 토큰이든 말씀하신대로 보안이 더 좋고 나쁘고의 문제가 아니라 둘이 똑같다고 생각합니다. JWT를 쓰는건 일반 토큰보다 보안이 더 좋기 때문이 아닙니다.
말씀하신 부분에서 어느 정도 아시는 것 같지만 JWT는 디비를 조회하지 않아도 된다는 것이 제일 큰 장점입니다. CPU 부하가 생길수는 있지만 현재 API 인증 서버에서 CPU가 문제가 되는 경우는 거의 없다고 생각합니다. 이보다는 디비 조회와 캐시서버 관리가 훨씬 많은 문제를 일으킵니다. JWT를 쓰면 토큰과 마찬가지로 위변조를 방지할 수 있으면서 디비를 조회하지 않고 JWT를 검사할 수 있으므로 서버를 stateless로 유지할 수 있습니다. 똑같은 보안을 유지하면서 서버가 stateles가 된다는 부분은 아주 큰 장점이라고 생각합니다.
그리고 권한 확인에 대해서는 말씀하신대로 들어온뒤에 재 검사를 할 수 도 있지만 JWT에 payload를 담을 수 있으므로 이 안에 권한이름이나 등급까지 담아도 됩니다. 이건 시스템에 따라 다르므로 담을지 말지는 직접 선택해야 합니다.
그리고 토큰의 보안과 관련해서는 JWT든 일반 토큰이든 토큰을 공격자가 가로채서 똑같이 보냈을 때 이를 막을 수 있는 방법은 없습니다. 동일한 요청이므로 같은 사용자가 보냈는지 공격자가 보냈는지 알수 없기 때문이죠. 이는 당연히 어느쪽이든 동일한 문제이고 JWT가 가진 문제는 아닙니다. 보안 면에서 신경써야 하는 것은 일단 위변조입니다. 공격자가 다른 사용자의 요청을 가로챈뒤에 다른 사용자인척 할 수 있냐가 문제가 됩니다. 일반 토큰은 stirng을 디비에서 직접 비교하므로 위변조가 안될테고 JWT는 사인이 있기 때문에 위변조가 안됩니다.(이를 변조하는 공격등도 당연히 존재하지만요..)
그러면 공격자가 가로챈 요청의 사용자인척 그 정보를 보는 문제가 남습니다. 이는 간단히 보면 아이디/패스워드를 뺏긴것과 동일하기 때문에 원천적으로 막지는 못합니다. 공격자가 아이디/패스워드를 넣으면 로그인할 수 있다는 문제를 막을 수 있는 방법은 없기 때문이죠. 이는 보통 토큰을 엑세스토큰과 리프레시 토큰으로 나누어서 관리하는 방식으로 처리합니다.(JWT냐 아니냐는 상관없습니다.) 엑세스토큰은 만료시간을 10분 주고 리프레시 토큰은 더 길게 준 뒤에 엑세스토큰으로 API를 사용하다가 만료되면 리프레시토큰을 이용해서 다시 엑세스토큰을 발급받는 방식이고 평소 API 요청에는 리프레시토큰을 같이 보내지 않습니다. 이 경우 네트워크에서 토큰을 뺏기더라도 만료시간 만큼만 사용할 수 있고 이 후에는 재발급받지 못하므로 더이상 사용하지 못하게 됩니다.
마지막으로 1번 질문에 대해서는 네이티브 앱은 안전한 저장소가 있지만 웹 브라우저는 솔직히 안전한 저장소가 존재하지 않습니다. 보안에 덜 취약한 내부 툴이 아닌 경우 저는 웹에서는 JWT를 포함해서 토큰 방식을 사용하지 않습니다.
친절하신 답변 감사 드립니다.
프로젝트에 적용하기 위한 검토 단계라 아직 해당 기술에 지식이 부족한차에 많은 도움이 되었습니다.
언제나 행복한 날들 되시기를 빌겠습니다..
감사의 의미로 제가 정리한 부분을 함께 올리겠습니다.
가정
--------
유효한 사용자인가
사용자가 유효한 권한을 가지고 있는가
이 두 질문이 엄격히 구분되어야 함.
jwt 와 임시암호(세션아이디나 db 시스템등) 비교
공통
--------
둘다 단기 억세스키, 장기 리프레쉬키 구현 가능
공통단점
-------------
a.클라이언트 웹에서는 단기키, 장기키, 임시암호 등 어느것이든 안전히 보관함을 보장받기 어려움
b.네트웍 노출에 무방비
jwt 억세스 토큰 도난 문제로 리프레쉬 키가 있다지만 리프레쉬 키 도난 역시 같은 문제임.
물론 최초 아이디 암호 도난 경우도 있기때문에 근원적 해결 불가
- https 적용하면 b.항목 해결
- 억세스키(단기키) 리프레쉬키(장기키) 는 약하게나마 보안 강화는 되지만 번거로움에 비해 큰 이익이 없어 보임.
다만 권한 변경시 적용 시간에는 의미가 있음
jwt리프레쉬키 발급시 어차피 db 조회해야함.
(임시암호 세션아이디 등에 비해 db조회를 수십(백)분의 일 정로도 줄일 수 있슴)
jwt 장점
------------
유효한 사용자라는 자체 인증권한을 가짐.
억세스키(단기키) 사용시 db 조회없이 인증 정보 획득 가능
유효한 권한을 가진다는 부분을 payload 에 넣을 수 있음 (헤더 크기증가, 적용 딜레이, 결국 권한 db조회를 감수한다면)
jwt 단점
-----------
유효한 사용자라는 자체인증의 적용은 쉽지만 해당 작업에 유효한 권한을 가진다는 자체인증을 구현하기엔 난항이 예상
인증정보만으로 서비스가 어려운 경우.. 즉 바른 사용자인가아닌가 보다는 그 사용자가 이 작업에서 어떤 권한을 가지고 있나가 중요한 경우
직업수가 많을 경우 즉 사용자 권한이 각 작업별로 규칙성 없이 복잡하거나 수시로 바뀔경우 문제 발생
1. 서버db 사용자 권한이 바뀐 경우 즉시 적용되지 않고 리프레쉬키 (억세스키 재발급) 적용시점까지 시간 걸림
(리프레쉬키 적용시는 필히 권한 db 조회해야 함)
2.jwt payload 내에 사용 권한 부분을 넣고 db내 권한 정보를 전혀 조회하지 않게 하려면 헤더가 심하게 길어질 수 있으며 보안 문제도 생김
(권한이 일반 어드민 혹은 1 2 3 4 5 이런 식이라면 간단하지만
어느 화면에서 A부서 과장은 B부서 대리보다 권한이 축소되어야 할 경우 생김,
예) 화면 101번에서
타부서 대리 이하는 조회 불가
타부서 과장이라도 조회가 최근 3개월 가능
해당 부서는 대리부터 최근5년 조회가능
타부서 대리에게 임시로 7일간만 조회가능
타부서 주임에게 임시로 해당 정보 1건에 대해 1회만 6개월간 자료 조회가능
)
3.jwt 단기키에서 해당 사용자의 아이디를 받은후 작업처리를 위해 권한 db를 조회해야할 상황이라면 중복 작업으로 jwt적용 의미가 퇴색
결론
--------
jwt 사용 최적 환경은
작업수가 작고
사용자 권한 범위가 간단하고 변경이 잦지 않으며
사용자수가 많은 경우
유리함
로그인 이후 수십개의 db 테이블를 조회하는 작업 처리의 경우 위의 jwt단점을 감수하고 사용자 권한 조회 1회를 줄이게 위해 jwt토큰(db독립 권한토큰)을 적용하기엔 설득력이 떨어지는 환경도 존재함.
결과적으로 payload 내에 사용자 권한 필드를 어떻게 잘 구성할 수 있느냐가 적용 가능 불가의 갈림길이 될 것으로 예상됨.
꽤 정확히 파악하신거 같습니다. 저도 JWT를 여기저기 사용하고 있지만 아직도 괜찮은지 헷갈리는 부분(보안이란게 취약하다고 모든 시스템이 다 바로 뚫리는 것은 아니기 때문에요.)도 있는 편이지만 각 부분의 특징과 주의해야 할 부분을 상당히 정확히 파악하신 것으로 보입니다.
이런 부분, JWT를 포함한 토큰이나 인증 등은 논의해야 할 내용이 꽤 많은데 국내에서는 이런 논의를 할 곳이 많지 않아서 영어 자료를 보고 공부해봐야 하는게 아쉽네요. 모든게 그렇지만 말씀하신대로 JWT가 만능은 아니고 토큰 시스템의 하나의 선택권이라고 생각하고 있습니다. 저라도 제가 뱅킹서비스를 만들거나 주식 관련 등의 서비스를 만들다면 실시간성이나 보안이 훨씬 중요하므로 JWT를 쓰기보다는 디비에 부담을 주더라도 디비에 토큰을 다 넣을 것 같습니다. 보안보다는 사용성이 중요한 애플리케이션에서는 꽤 좋은 장점을 준다고 보고 있습니다.
아 참고로 JWT의 가장 큰 단점중 하나는 이미 발급한 토큰을 revoke하지 못한다는 문제가 있습니다. 쓰신 글에서 파악하고 계신듯 합니다만 보안이 중요한 부분이나 권한등이 수시로 바뀐다면 이럴때는 JWT를 쓰기 어렵죠. 제가 본 글에서는 그래서 revoke 토큰을 관리하는 blacklist를 디비에 넣고 조회해서 쓰는 곳도 본적 있습니다. 이렇거면 디비를 조회하게 되므로 이렇게 할거면 왜 JWT를 쓰나 싶기도 하지만 전체를 stateful로 가져가는 것보다는 훨씬 가볍게 운영할 수도 있으므로 상황에 따라서는 고려해볼 수도 있을듯 합니다.
정말 많은 도움이 되었습니다.
감사합니다..~^^
현재 업무가
기존의 시스템이 관리자용 애플리케이션과 일반(낮은권한용 관계사용 일반고객용) 애플리케이션으로 구분되어 서버(db)에 접속하는 구성인데..
일부 로직들이 클라이언트에 분리되어 있어서 관리에 문제가 있었습니다.
이에 모든 로직을 rpc server로 구성해서 로직파트를 단일화하고 클라이언트는 프레젠테이션만 하는 어찌보면 예전부터 있었던 원칙적인 구성으로 변경중입니다
그렇다면 이 클라이언트가 애플리케이션이 아닌 웹 인터페이스여도 가능하지 않나해서 고민중이었습니다.
많은 도움이 되었습니다.
감사합니다~
네 저도 JWT 사용에 대해서 다 완전히 고민이 해결된건 아니라서 다시 여러방향으로 생각해 볼수 있었습니다. ㅎㅎ
안녕하세요. JWT를 사용해보려고 하는 1인 입니다. 작성해주신 글이 큰 도움이 되었는데요~ 질문이 하나 생겨 코멘트 남겨봅니다..
혹시 jwt option중 하나인 issuer (iss)를 변경하면 어떻게 되는지 아시는지요?
일단 토큰이 expired되면 다시 signin하도록 구현해두었는데, issuer 변경 시에도 기존 issuer로 signin했던 token들이 한꺼번에 무효처리가 되고 (마치 expired된 것 처럼..) 접근시 재signin을 시도하게 될지요?
아직 이 분야는 초보라 제대로 이해한 것인지 모르겠네요...
조언 주시면 감사드리겠습니다ㅠㅠ!
스펙은 복잡해서 저도 모든 기능이 스펙에서 어떻게 동작해야 하는지 정확히 알지는 못합니다. 스펙이 모든 상세를 다 설명하는 것도 아니긴 하고요.
정확히 질문하신 시나리오는 모르지만 iss에 대해서 스펙에서 그런 시나리오를 제약하진 않는다로 생각합니다. iss는 토큰의 발행자이므로 발행자를 기준으로 권한등을 체크하거나 발행자가 아닌 권한을 사용하려고 할때 막거나 하는 용도로 사용해야 할것 같습니다. JWT를 쓰면 보통 stateless를 사용하려는 것이므로 들어온 토큰의 유효성과 iss만 확인하면 될것 같습니다.
기대하신 내용이 아니라면 issuer가 변경되는 상황이 어떤 상황인지 좀더 설명해 주시면 고민해 보겠습니다
안녕하세요. 답변 감사드립니다!
시나리오를 조금 더 설명드리자면
서버측에서 A issuer 로 발행된 클라이언트측 JWT가 만료되기 전에
서버측에서 A issuer를 B issuer로 변경하게 된 경우 입니다.
이 경우, 클라이언트가 토큰의 유효성 체크를 시도할 때, expired 로 반환되는 것일지 궁금했습니다 :-)
답변주신 내용으로 미루어 봤을 때, 토큰이 발행자의 권한에 영향을 받을 수 있는 것 같아서 위와 같은 시나리오에서도 문제가 되지 않을까 싶어서요!
안녕하세요. 지식이 부족해 확실치는 않지만.. 자답합니다!
토큰 검증 시, `jwt.verify(token, secretOrPublicKey, [options, callback])` 스펙에서 options에 issuer를 넣어주면 해당 issuer와 동일한지 체크하는 듯 합니다.
issuer는 Optional이구요. verify시 issuer를 넣어주지 않으면 말씀해주신 것 처럼 발급 당시에 issuer 유효성이 이미 체크되었다고 보고 넘어가는 듯 합니다.
늘 좋은 글로 지식 공유해주셔서 감사드립니다 :-D
안녕하세요.
유익하고 좋은 글 잘 봤습니다.
질문이 하나 있습니다.
저도 JWT를 한번 적용시켜 보려합니다 .
먼저 저는 API서버를 구성하였고,
클라이언트(웹,앱)과 통신할 준비가 되어있습니다. 실제로 하고있습니다.
이때, 인증방식이나 권한등을 체크하기 위해 JWT를 적용하려합니다.
플로우는 아래와 같습니다 .
1) 클라이언트측 - ID와 PW 로그인요청
2) 서버측 - 로그인 요청에대해 DB조회 후, 정상이면 유저정보와 권한,시크릿키 발급응답
3) 클라이언트측 - 유저정보,시크릿키 를 쿠키 및 클라이언트측의 sessionStorge 에 저장 (웹일경우)
그 이후 api 호출 시 마다 미리 서버와 규약된 JWT 암호방식,JWT토큰사용 (헤더)와 유저정보 및 파기시간설정(페이로드) 등을 시크릿키로 암호화 한 후 JWT에 맞게끔 암호화한걸 다시 서버측에 헤더에 담아 전송
4) 서버측 - JWT토큰이 없다면 error ,
JWT토큰에 담긴 페이로드를 규약한 방식의 암호화로 풀어 서버에서 페이로드 부분의 권한을 읽어 처리할 수 있도록 함 (이부분을 검증이 필요할 것 같은데 마냥 보내온 유저로만 검증 하기엔 무언가 위험이 따를 것 같습니다. 혹은 제가 잘 이해못한것일수도..)
이런 식으로 구현 하려고 하는데 제가 하는것이 맞는지 궁금합니다.
의문점이 드는것은 api를 매 호출시 검증을 어디까지 해야하는건지 궁금합니다.
저도 JWT에 대해서 다 아는 것도 아니고 토큰인증 시나리오는 다양한 경우가 있으므로 설명드리는 내용이 다 맞는 것은 아닐 수 있다는 전제하에 제가 아는 선에서 설명드립니다.
일단 2번 과정에서 secret을 클라이언트에 전달하시는데 이렇게 하시면 안될것 같습니다. secret이 유출되면 누구나 똑같은 JWT 토큰을 만들 수 있으므로 secret는 안전하게 보관해야 하고 secret이 유출되면 기존 모든 토큰을 무효처리 해야 합니다. 2번 과정에서 로그인이 정상처리되었으면 서버(혹은 인증서버)에서 JWT 토큰을 클라이언트에서 내려주고 클라이언트는 이를 조작할 필요없이 다음 API 요청에 그대로 가져와야 합니다. 클라이언트가 JWT 토큰을 생성하는 것이 아닙니다.
4번에서 JWT의 페이로드를 해석하는 것은 맞습니다. 기본적으로 JWT의 secret이 유출되거나 JWT 자체의 보안이 뚫리지 않는 이상 JWT를 verify했을때 정상이라면 해당 페이로드는 조작되지 않고 정상이라고 생각하는게 맞는것 같습니다. 이후에는 애플리케이션에 따라 다를 수 있는데 제 생각에 JWT를 쓰는 주 이유는 DB를 조회하지 않고 stateless로 사용하려 함이기 때문에 간단한 앱이라면 JWT 토큰을 그대로 읽어도 큰 문제는 없을 것 같고 금융등 보안이 너무 중요하다면 JWT에는 인증정보만 담고 있고 이 인증정보로 DB에 조회해서 권한을 다시 확인한 후에 사용하는게 나을것 같습니다.
추가적인 내용으로 JWT를 포함해서 토큰을 사용하는 경우 iOS나 Android에서는 안전한 저장소가 앱 내부에 존재하지만 Web 같은 경우는 완전히 안전한 스토리지라는 것이 존재하지 않습니다. 이경우는 앱에서 JWT를 사용하는 것에 여러 보안 요소를 고려해 보거나 JWT의 토큰 만료시간을 짧게 주어 리프레시하는 등의 사용방식을 살펴보시기를 권해드립니다.
답변감사합니다.
그럼 서버측에서 로그인 이후 JWT토큰을 (expiration time넣어) 발급 후,
단 1번 내려주게 되고, 리프레시토큰을 이용하지 않는다면, 이 한번 내려준 만료시간이후에
로그인이 되어있다면 다른 동작하면 토큰인증실패로 로그인을 다시 하도록 하여 계속 로그인시 새로운 JWT토큰을 생성하게끔 하여야 하겠네요..
아니면, api성공 호출 시 서버측에서는 다시 JWT토큰을 담아서 넘겨야 하는 상황이 벌어지는데
그경우, 탈취당하면 .........
그래서 첫 로그인할땐 시크릿키만 넘겨주고 클라이언트측에서 JWT토큰을 생성하는것으로 생각하고 있었습니다.
즉, 클라이언트에선 매 API마다 호출 할 때에 , http 헤더값에 Authorization 해서 새로운 JWT토큰담아서 서버측에 넘긴걸 검증하려 했었는데
제가 틀린것이겠죠?
PS) 추신으로.. 시크릿키는 각 유저마다 DB에 고유한 암호화된 값으로 넣어두고 클라이언트측에 그 시크릿키,유저정보만 보내어 클라이언트측에서 JWT를 생성할 생각입니다.
제가 완전히 이해한 것인지 모르지만(물론 JWT를 완전히 다 이해하고 있는 것도 아닙니다.) 클라이언트가 JWT를 생성하는 것은 잘 이해되지 않습니다. 여러 탈취경로가 있지만 가장 주의해야할 부분은 네트워크나 클라이언트에서 탈취당하는 경우라고 생각합니다. 서버가 털릴 수도 있지만 앞의 두경우에 비해서는 막기가 훨씬 쉽고 사용자는 불편하겠지만 시크릿을 바꾸어서 모든 키를 revoke 시키는 방법도 있습니다.
JWT를 이용하는 모든 시나리오를 찾아본 것은 아니지만 제가 생각하는 시나리오는 최초 로그인시에 access token/ refresh token을 주고 평소엔 aceess로 API를 이용하다가 만료되면 refresh로 다시 access를 발급 받는 구조입니다. 이 상황에서 전제되어야 하는 것이 refresh를 안전하게 클라이언트가 보관하는 것인데 웹브라우저에서는 사실상 이 토큰을 안전하게 보관할 곳은 없습니다. 실제로 저는 아직 웹에서는 JWT를 사용하지 않습니다.
시크릿으로 클라이언트가 토큰을 만드는 부분에서 제가 이상하게 느끼는 부분은 JWT를 사용하는 가장 큰 이유는 stateless로 서버를 구성해서 디비를 조회하기 위함이라고 생각하는데 사용자별로 시크릿이 다르다면 API 요청마다 검증하기 위해서 디비를 조회해야 하므로 이럴 바에는 사용자마다 토큰을 임의로 발급해서 서버에서 관리하는게 더 편하다고 생각하기 때문입니다.
감사합니다 아웃사이더님
말씀해주신대로 사실상 access token의 만료기간을 짧게갖고
만료시 refresh token으로 access token 만료되었을때 재발급해주는 형태로 하는것이 가장 좋겠네요.
사실 앱에선 웹보다 비교적 안전한 저장소라고 들었는데,
웹에서는 좀 취약한것 같습니다.
웹에선 쿠키나 세션스토리지 뭐 이런걸 사용해야할 것 같은데, 찾아보니 그것도 민감한 데이터는 담지말기를 권장하더군요.
아무튼, api서버와 vue로된 프레임워크로 개인적인 공부를 하고있는데,
각기 띄우는 서버가 달라 CORS허용해줘야 하더라고여.
그런것 등등을 다 고려했을때, 어떻게 가야할지 막막하네요 ㅠㅠ
여기까진... 제 푸념이였습니다 ㅠ
이제까지 답변해주셔서 정말 감사합니다. 많은것을 배워갑니다! ! !
네 저도 인증 구현할때는 한참 고민하다가 요즘은 못보고 있어서... 좋은 방법 찾으시면 나중에라도 공유해 주시면 감사하겠습니다.
좋은 글 잘 보았습니다.
댓글 내용을 읽던 중 아웃사이더님께서는 웹은 저장소가 앱에 비하여 취약하기 때문에 jwt를 웹에서는 이용하지 않는다라고 하셨는데, jwt나 cookie에 session을 담아서 세션키를 레디스 같은 저장소에 둬서 세션서버를 운용하는 것이나 둘다 어쨌든 클라이언트에 캐시를 해야한다고 생각해서, jwt가 다른 인증 방식보다 웹에서 취약하다라고 볼수 있는지 잘 모르겠습니다.
제가 이해를 잘 못하고 있는 부분이 있는 것 같아 여쭤봅니다 ^^
(저의 개인적인 생각으로는 개별 유저마다 디비에 salt같은 것을 두고 암호화를 적용해서 단방향 암호화 토큰을 관리하는 것이 jwt보다 상대적으로 보안상 더 이점이 있다라고 생각합니다. jwt는 복호화가 가능해서 secret이 노출되는 순간 모두 털리는 구조이기 때문입니다. 개별 토큰 방식은 물론 디비를 매번 조회해야하는 오버헤드가 있지만, 제가 jwt를 쓴다면 클레임셋에 아이디나 그외 유니크 키값과 같은 정말 최소한의 정보만 넣을 것이고 결국 유저디비를 매번 조회해서 전체값을 얻어올 것 같기 때문입니다.)
저도 JWT나 보안관련해서 다 아는것은 아니라 조심스럽긴 하고 이런 얘기는 관심있는 분들이랑 항상 얘기해보고 제가 모르는 부분을 배우고 싶은 영역입니다.
저런 얘기를 한 이유는 제가 생각하는 JWT의 가장 큰 장점은 stateless로 토큰을 검증할 수 있기 때문입니다. 말씀하신대로 사용자마다 디비를 조회하거나 레디스같은 세션 스토어에 키를 저장해 놓고 비교하면 revoke도 편하고 보안측면에서도 훨씬 강화할 수 있습니다. 이런 면에서 디비를 조회할 것이라면 굳이 JWT를 사용할 이유가 없다고 생각합니다.
블랙리스트나 화이트리스트로 JWT를 관리할 수도 있지만 그렇지 않은 경우에는 이미 발행한 JWT 토큰을 revoke할 수 있는 방법이 없습니다. 제가 이해한 내용에서는(그리고 이글 이후에 변경사항이 있어도 좇아가진 못했습니다.) JWT에서 로그아웃이라는 것은 클라이언트에서 토큰을 제거할 뿐이고 서버에서 해당 토큰을 revoke하는 것은 아닙니다. 그런면에서 다른 방법보다 JWT는 클라이언트에서 토큰을 어떻게 관리하는지가 중요하다고 생각했고 웹에서는 그런 부분에 취약하다고 생각했습니다.
좋은글 감사합니다.
많이 배우고갑니다.
댓글 죽 보았는데요 ㅠ 질문이 있습니다 ㅠ jwt를 쿠키에 저장보다는 일반적인 세션키를 서버에 저장하는게 보안에 좋다고 이해했는데요(세션키는 쿠키에 동시저장 정보는 redis이용 서버에 {세션키,세션정보} 저장 한다고 가정), 가령 세션키를 RandomStringUtils.randomAlphanumeric(40) 이런식으로 생성했을때 누군가 brute force 로 계속 키를 변경하여 쿠키에 저장해서 서버로 요청하면 다른 사람꺼로 언젠가는 한번 로그인 되게 될꺼 같아서요;; 이런경우는 따로 brute force 탐지를 부분을 준비해야 하는지 아니면 변조 불가능하게 의미없는 값으로 jwt키로 만들어 쿠키에 저장하고 서버에는 verfiy한 키, 세션정보를 저장하여 세션정보를 가져오는게 좋을지요 일단 이 두개밖에 생각 안나네요 도움 의견 부탁드립니다
말씀하신 시나리오는 이론상 가능하긴 합니다.
OWASP를 보시면 https://www.owasp.org/index.php/Insufficient_Session-ID_Length 그런 문제때문에 최소 128비트 길이의 세션키를 권장하고 있습니다. 정말 우연찮게 존재하는 세션키를 맞출수도 있겠지만 현실에서는 발생할 가능성은 없어보입니다.
와우~ 저도 그렇게 생각은 했는데 확신이 없었는데요 명확한 근거로 답해주셔서 감사합니다.
위에 128비트 길이의 세션키라고 함은 2^128개의 경우의 수이상으로 만들면 된다고 가정할수있겠는데요. 2^128 은 대략 64^21 이고 RandomStringUtils.randomAlphanumeric 으로 생성하는 문자는 한자리에 62개(알파벳대소문자 52개 + 숫자 10개) 이므로 대략 21자리 이상이면 말씀주신 가이드에서 말한 기준치를 넘게 되겠네요 다시한번 감사드립니다!
좋은 글 감사합니다. 그런데 궁금한 점이 있습니다. jwt payload에 "scope"과 "authorities"는 registed claim도 아닌것 같은데, spring의 HttpSecurity에서 저렇게 사용하는 이유가 궁금합니다.
아웃사이더님 안녕하세요. jwt 재생 공격 방지를 위해 위에 말씀하신 blacklist 방법이나 jti 비교 방법과 같이 non-stateless한 방법들이 있는 것으로 알고 있습니다.
현재 곧 진행할 프로젝트에 jwt을 사용하려고 하는 중인데, 궁금한 사항이 생겨 댓글 남기게 되었습니다. https 상에서 통신하는 모바일(네이티브) 어플리케이션에서도 토큰 재생공격이 가능할까요?? (정확히는 jwt 관련 질문은 아닙니다만 질문 드려봅니다!)
저도 보안전문가는 아니라 최근 일어나는 이러한 공격패턴에 대해서 아주 자세히 알지는 못합니다. 자료는 이것저것 찾아봤지만 많은 공격을 당해볼 정도의 서비스를 운영해 보지도 못했고요.
기본적으로 HTTPS를 사용하면 HTTP에 비해 토큰이 탈취될 수 있는 경우를 대부분 피해갈 수 있습니다. 그래서 아주 개인적인 의견으로(조심스럽지만!) 돈을 직접 다루는 아주 민감한 서비스가 아니고 JWT 토큰 유효기간에도 이런 공격을 당하면 아주 민감한 서비스가 아니라면 HTTPS 정도로 어느정도 만족할 수 있지 않나 생각합니다. 하지만 모바일애플리케이션 내에서 토큰이 탈취당한다거나 여러 취약점을 통해서 공격하는 방법도 계속 존재하므로 공격이 불가능하다고 말할 수는 없다고 생각합니다.
답변 감사합니다