참고 : https://jwt.io/
참고 : Basic 인증과 Bearer 인증의 모든 것
참고 : Refresh Token
JWT( Json Web Token )는 당사자 간의 정보에 디지털 서명을 하고, JSON 객체로 안전하게 전송하기 위한 개방형 표준입니다.
JWT 는 주로 인가 혹은 인증 정보를 서버와 클라이언트 사이에서 안전하게 주고 받기 위해 사용합니다.
사용자 인증 정보를 넣어서 토큰을 발급해주면 클라이언트가 인증이 필요한 리소스에 접근할 때 토큰을 포함해서 요청을 전송하고, 서버에서는 복잡한 인증 과정 없이 토큰만으로 사용자를 인증 및 인가할 수 있게 됩니다.
디지털 서명을 할 때는 통신 당사자 간의 비밀키를 이용한 HMAC 혹은 RSA 와 같은 공개키, 개인키 암호화 방식을 사용할 수 있으며 메세지 무결성을 확인할 수 있어 신뢰할 수 있습니다.
디지털 서명, 비밀키, 공캐키, 개인키와 같은 내용에 익숙하지 않으신 분들은 네트워크 보안 게시글에 간단하게 정리해두었으니 참고하시면 좋을 것 같습니다.
JWT 는 Header, Payload, Signature 로 이루어져 있습니다. 각 부분은 JSON 형태로 이루어져 있으며, Base64Url 로 인코딩 되어 표현됩니다. 이렇게 나누어진 부분은 .
으로 연결됩니다.
디지털 서명된 토큰의 경우 변조로부터 보호되지만, 누구나 읽을 수 있기 때문에 암호화 되지 않은 경우, JWT 의 header 나 payload 에 중요한 정보를 넣어서는 안됩니다.
{ "alg": "HS256", "typ": JWT }
헤더는 일반적으로 토큰의 유형( typ ) 과 서명 알고리즘( alg ) 으로 구성됩니다.
tye : 토큰의 유형에서는 JWT 와 같은 토큰의 타입을 지정합니다
alg : 서명 알고리즘에는 HMAC SHA-256 혹은 RSA 를 사용할 수 있습니다.
Payload 는 Claim 을 포함하는데 Claim 이란 Entity( ex> 사용자 ) 및 추가 데이터에 대한 설명입니다. 즉, 토큰에서 사용될 정보들의 조각이라고 생각하시면 됩니다.
Claim 은 총 3가지로 이루어지며, JSON 형태( Key, Value )로 사용자 정보와 같은 여러 정보들을 넣을 수 있습니다.
Registered claims ( 등록된 클레임 ) : 토큰 정보를 표현하기 위해 미리 정해진 종류의 데이터로, key 의 길이는 모두 3글자이며, 아래와 같은 key 들을 사용할 수 있습니다.
iss : 토큰 발행자
exp : 토큰 만료 시간
sub : 토큰 제목이며 unique 한 값을 사용해야합니다.
aud : 토큰 대상자
Public claims ( 공개 클레임 ) : 사용자 정의 클레임으로, 공개용 정보를 위해 사용됩니다. 충돌을 방지하기 위해 충돌 방지 네임스페이스를 포함하는 URI로 정의해야 합니다.
Private claims ( 비공개 클레임 ) : 서버와 클라이언트 사이에 임의로 지정한 정보를 저장하는 클레임입니다.
payload 의 예시는 아래와 같으며 이곳에 userID 가 무엇인지, userName 이 무엇인지에 대한 정보를 담게 됩니다.
{ "sub": "1234567890", "name": "John Doe", "admin": true }
서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용되는 고유한 암호화 코드입니다.
서명 부분을 생성하기 위해 인코딩된 헤더와 페이로드가 필요하며, 인코딩한 값을 키를 이용해 헤더의 alg 에 정의한 알고리즘으로 서명한 후 이 값을 다시 Base64Url 로 인코딩합니다.
HMAC SHA256 알고리즘을 사용하는 경우 통신 당사자 간의 비밀키를 이용하여 서명하기 때문에 메세지가 도중에 변경되지 않았는지에 대한 메세지 무결성만을 확인할 수 있습니다.
참고로 HMAC 은 해시함수에 넣을 때 비밀키 + 메세지 본문을 넣게됩니다. 여기서 메세지 본문이란 header 와 payload 을 의미합니다.
반면에 RSA 의 경우, 메세지 본문을 해시 함수를 통해 해시값을 구하고, 이 해시값을 개인키로 암호화하여 전달합니다.
즉, header 와 payload 에 대한 해시값을 구하고, 이를 암호화하여 전달합니다.
RSA 방식은 혼자만 갖고 있는 개인키로 서명을 하기 때문에 메세지 무결성에 더해 송신자를 확인할 수 있습니다.
서버는 클라이언트가 전달한 JWT 를 디코딩하여 header, payload, signature 세 부분으로 분할하고, 유효한 토큰인지 검증을 진행합니다.
이 떄 header 의 alg 에 명시된 방식에 따라 다른 검증 방식을 사용합니다.
HMAC : header 와 payload 를 자신이 가진 비밀키와 함께 해시함수에 넣어 서명을 생성하고, 전달받아 분할한 signature 와 비교합니다.
RSA : 개인키로 암호화된 header + payload 를 자신의 공개키로 복호화해서 검증합니다.
전달된 JWT 를 분해할 수 있는 이유는 JWT 는 암호화 하는 것이 아닌 Base64Url 로 인코딩하여 전달되기 때문입니다.
인증( 검증 )이 완료된 후, 클라이언트가 요청한 정보를 다시 payload 에 담아서 전달합니다.
Http 통신에서는 보호된 서버 리소스를 접근하는 클라이언트의 인증 정보( Credentials ) 를 확인하게 되며, 아래와 같은 인증 헤더를 요청에 사용하게 됩니다.
Authorization : <type> <credentials>
올바른 인증 정보를 넣으면 HTTP 상태 코드 200 OK
, 인증 헤더를 누락하면 401 Unauthorized
, 인증 정보를 넣었지만 접근 권한이 없다면 403 Forbidden
이 반환됩니다.
이러한 인증 정보는 인증 방식을 나타내는 type 에 따라 달라지게 되는데 Basic 과 Bearer 에 대해 알아보도록 하겠습니다.
Basic 은 가장 기본적인 인증 방식으로 사용자 ID 와 PW 를 Base64Url 로 인코딩한 값을 인증 정보( Credentials )로 사용하는 방식입니다.
즉, Authorization 에 ID 와 PW 를 담는 것인데 이렇게 되면 매요청마다 ID, PW 를 달고 요청하게 되고, 클라이언트와 서버가 요청을 보낼 때마다 사용자 정보를 함께 전달할 필요가 없게 됩니다.
Basic 인증 방식은 간단하지만 몇 개의 단점이 존재하는데 Basic 인증 방식은 서버에 사용자 목록을 저장하게 되는데 요청한 리소스가 많거나 사용자가 많으면 목록에서 권한을 확인하는 시간이 길어지게 됩니다.
또한 ID, PW 가 인코딩 되었을 뿐 암호화되지 않기 때문에 Basic 토큰 값이 노출이 되면 ID, PW 가 그대로 노출되는 것이기 때문에 보안에 취약합니다.
ID, PW 노출을 막기 위해 사용할 수 있는 것이 https 인데 Basic 인증을 사용하는 요청은 반드시 Http, SSL/TLS 로 통신해야 안전합니다.
Bearer 방식은 OAuth 2.0 프레임워크에서 사용하는 토큰 인증 방식입니다. 즉, 토큰을 인증 정보로 사용한다는 의미입니다.
Bearer 토큰의 형태는 인증 서버에 따라 다르지만 16 진수의 문자열을 사용하기도 하고 JWT 를 사용하기도 합니다. 중요한건 Bearer 토큰은 클라이언트가 해석할 수 없는 형태여야 하고, 사용자의 정보를 전달하면 안됩니다.
Bearer 토큰은 쉽게 복호화 할 수 없고 OAuth 는 프레임워크의 인증 및 리소스 서버는 SSL/TLS를 필수로 사용하기 때문에 Basic 에 비해 안전합니다.
만약에 토큰이 외부로 노출되면 다른 서비스도 토큰으로 바로 리소스에 접근할 수 있지만 서버에서 토큰의 리소스 접근 권한을 쉽게 철회할 수 있으며, ID 와 PW 가 노출되는 것이 아니기 때문에 Basic 에 비해 노출되어도 훨씬 피해가 적습니다.
지금까지 설명한 토큰은 서버의 리소스에 접근할 때 사용자를 인증할 수 있는 Access Token 으로 동작합니다. 하지만 JWT 는 Stateless 하기 때문에 서버는 토큰을 가진 클라이언트가 진짜 본인이 맞는지 알 수 없습니다.
만약 이를 해결하기 위해 Access Token 의 유효 기간을 짧게 설정한다면 로그인을 자주 해야 하기 때문에 불편합니다. 반대로 유효 기간을 길게 할 경우, 서버는 한 번 발급한 토큰에 대한 제어권이 없기 때문에 보안에 문제가 발생합니다.
이런 문제를 해결하기 위해 등장한 것이 바로 Refresh Token 입니다.
Access Token 의 보안을 강화하기 위해 토큰의 유효 시간을 짧게 설정하여 자주 재발급하되, 사용자가 자주 로그인 해야하는 불편함을 없애기 위해 사용합니다. 즉, 클라이언트가 가진 Access Token 이 만료 되었을 때 Access Token 을 새로 발급받기 위해 사용합니다.
간단하게 말하면 Access Token 이 만료되었을 때 새로 발급해줄 수 있는 열쇠의 역할을 합니다. 이 Refresh Token 이 만료되기 전까지 Access Token 을 새롭게 발급받을 수 있습니다.
( A ) 클라이언트가 인증 서버에 인증을 요청합니다
( B ) 서버는 클라이언트의 인증이 성공하면 Access Token 과 Refresh Token 을 발급합니다
( C ) 클라이언트는 Access Token 을 제시하며 보호된 리소스를 요청합니다
( D ) 리소스 서버가 액세스 토큰의 유효성을 확인하고 유효한 경우 요청을 제공합니다
( E ) 클라이언트는 Access Token 을 제시하며 보호된 리소스를 요청합니다
( F ) 클라이언트가 제시한 Access Token 이 유효하지 않으면 Invalid Token Error 를 반환합니다
( G ) 클라이언트는 인증 서버에 Refresh Token 을 제시하며 새로운 Access Token 을 요청합니다
( H ) 서버는 클라이언트를 인증하고, Refresh Token 의 유효성을 검사하여, 유효한 경우 새로운 Access Token 을 발급합니다
JWT 형태의 Refresh Token 이라면 JWT 의 특징처럼 stateless 하고, 토큰 자체에 데이터를 담을 수 있습니다. 또한 Refresh Token 의 유효성을 검증하기 위해 DB 에 접근하지 않아도 됩니다.
하지만 Access Token과 마찬가지로 Refresh Token 을 서버에서 제어할 수 없기 때문에 Refresh Token 을 탈취 당한다면, 토큰을 무효화 시킬 수 있는 방법이 없어 유효 시간이 만료될 때까지 기다려야합니다.
Refresh Token 으로 random String 이나 UUID 를 사용한다면, 해당 토큰을 사용자와 매핑되도록 DB 에 저장해야합니다.
이 경우, Refresh Token 이 전달되는 경우에 유효성 검증을 위해 DB에 접근해야 합니다.
하지만 DB에 저장이 되어 있기 때문에 탈취 당했을 때 사용자를 강제로 로그아웃 시키거나, 토큰을 무효화시킬 수 있다는 장점이 있습니다.