요즘은 HTTP API를 사용할 때 엑세스 토큰(Access Token)을 사용하는 것이 일반적이다. 이러한 인증방식은 클라이언트가 무엇이냐에 따라 약간 다른데 페이스북의 경우는 앱을 생성할 때 시크릿 토큰을 제공하고 시크릿 토큰을 이용해서 오픈 그래프 API로 /oauth/access_token
에 요청하면 인증토큰을 주고 Github의 경우에는 앱 생성 시 인증 토큰을 주고 이를 API 요청 시에 사용하게 한다.
여기서 말하려는 인증 토큰은 OAuth에 대한 것은 아니다. OAuth는 보통 사용자의 인증과정을 얘기하는데 어떤 서비스를 이용할 때 페이스북으로 로그인해서 페이스북의 정보를 해당 서비스에서 사용하게 하는 것이 OAuth인데 이건 사용자가 직접 하는 행동이다. OAuth는 사용자의 인증과정에 관여할 뿐이고 이후의 API 호출에는 마찬가지로 어떤 형태의 엑세스 토큰이 필요하다. 사용자가 관여하지 않더라도 앱이나 서버에서 직접 페이스북 등의 OpenAPI를 사용하는 경우 해당 서비스에 등록하고 받은(혹은 인증을 통해 받은) 엑세스 토큰으로 API를 사용한다.
아주 간략하게 설명하면 서버가 사용자(실제 사람일 수도 있고 앱이나 서버)가 인증했을 때 엑세스 토큰을 알려주고 다음에 API 요청할 때 이 토큰을 가져오면 아까 인증한 너라는 걸 인증해줄게 라고 하는 것이다.
JSON Web Token(JWT)
이 인증 토큰을 어떻게 구현할지를 알아보다가 발견한 게 JWT이다. 해당 구현에 대해서 검색하는 중에 여러 번 눈에 뜨였지만 처음에는 Java 관련 무슨 라이브러리인 줄 알고 자세히 보지 않았다.(GWT와 유사하게 보여서 무의식중에 그렇게 생각한 것 같다.)
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JavaScript Object Notation (JSON) object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or MACed and/or encrypted.
JWT 문서에 따르면 위처럼 정의되어 있다. 간단히 말하면 JSON Web Signature (JWS)와 평문 JSON Web Encryption (JWE)를 페이로드로 사용하는 JSON객체로 인코딩해서 양쪽에서 주고받을 수 있는 토큰을 의미한다고 나와 있다. 스펙을 자세히 읽어보지는 못해서 완전히 이해는 못 했고 스펙이 아직 Draft상태라서 언제 달라질지도 명확지 않다.
사용방법
위에서 본 대로 JWT외에도 JWS와 JWE도 있는데 이 스펙을 발견한 지도 얼마 안 되었기 때문에 깊은 부분까지는 잘 모른다. JWT 구현체를 만들려고 하는 것은 아니므로 어떻게 사용하는지 살펴보자. 예제를 위해서 jwt-simple라는 Node.js 모듈을 사용해서 테스트해보자.
var jwt = require('jwt-simple');
var payload = { foo: 'bar' },
secret = 'secret_token'
var token = jwt.encode(payload, secret);
// 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.FoQJBkvwOkFZ9RZ7vMDw0Tz4RlpR3_Be63jHVpNpnJ0'
사용방법은 간단하다. 필요한 데이터를 담은 JSON 객체로 페이로드를 만들고 시크릿 토큰으로 인코딩하면 토큰을 만들 수 있다.
var jwt = require('jwt-simple');
var payload = { foo: 'bar' },
secret = 'secret_token'
var decoded = jwt.decode(token, secret);
// { foo: 'bar' }
JWT는 인코딩한 토큰으로 원래의 페이로드를 복구할 수 있다.
인코딩할 때는 여러 가지 알고리즘을 사용할 수 있는데 현재 jwt-simple
는 HS256
, HS384
, HS512
, RS256
4가지 알고리즘을 지원하고 있다.
jwt.encode(payload, secret, 'HS256')
// 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIifQ.FoQJBkvwOkFZ9RZ7vMDw0Tz4RlpR3_Be63jHVpNpnJ0'
jwt.encode(payload, secret, 'HS384')
// 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJmb28iOiJiYXIifQ.ZVajb4bjQ5Vb0ZtVmiGcyAhViZ2jxgQzsFHtWsGUkkehhlrITdrsGynC4SzfiTYb'
jwt.encode(payload, secret, 'HS512')
// 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJmb28iOiJiYXIifQ.qFCfJj5QGgGAD6994rOQJl1vYKobD5TpiieF7615ybmIvdJtyaN8OXYCdshdvUCvzeDmuXyUUuA5QaSMYc0_Xg'
JWT로 엑세스 토큰을 구현할 때
추측이지만 JWT 스펙을 봤을 때 API 인증에 사용하는 엑세스 토큰에 대한 어떤 명세화가 필요해서 생겨난 것으로 보인다. 보안에 대해 깊은 지식이 없기에 이 부분에 대해서 언급하는 게 불안하지만, API 서버에서 엑세스 토큰으로 JWT를 사용할 때 고민되는 부분을 생각해 봤다.(보안을 뚫는 방법은 아주 다양하므로 기본적인 부분만 얘기한다.)
JWT 자체의 취약점이 있을 수도 있지만 신뢰한다고 가정했을 때 시크릿 토큰이 있어야 디코딩을 할 수 있으므로 시크릿 토큰을 안전하게 보관한다면 데이터를 다른 사람이 열어볼 수 없다. 대부분의 OpenAPI 서비스들이 하듯이 앱을 등록할 때 AppId와 Secret을 제공해서 앱별로 다른 시크릿을 사용하게 할 수도 있다. 시크릿을 빼앗긴다면 당연히 누가 열어볼 수 있지만 이건 어디서나 같은 얘기로 다른 OpenAPI들도 시크릿은 공개하지 말라고 경고하고 있다.
{userId:1}
처럼 페이로드를 만들고 API 요청 시에userId
를 같이 보내게 해서 이 둘을 비교해 볼 수도 있지만 실제로 유효한지는 잘 모르겠다. 토큰을 빼내 가거나 임의로 생성할 수 있다면userId
를 같이 보내는 정도는 별일 아닐 것이므로 어차피 이러한 공격은 막을 수가 없다. 기본적으로 토큰을 다른 사람이 볼 수 없어야 하므로 HTTPS를 사용하면 기본적인 안정성을 가져갈 수 있고 타임 만료를 주어서 자주 토큰을 교체해 줄 수도 있다.데이터베이스에 해당 토큰을 저장해두고 이를 요청으로 받은 토큰과 비교해서 유효성을 확인할 수 있다. 페이로드에 비교할 수 있는 값들이 추가로 더 들어있다면 같이 비교해 볼 수 있고 이를 통해서 기존 토큰은 만료 처리하고 새로운 토큰을 발급할 수 있다. 어떤 상황에서 새로운 토큰을 발급할지(타임 만료 시간 등)는 서비스 정책에 따라 다를 것이다.
위에서 인코딩/디코딩할 때 봤듯이 같은 페이로드를 같은 시크릿으로 인코딩하면 항상 같은 토큰이 나온다. 그러므로 만들 때마다 timestamp나 만료시간이나 uuid 등 계속 바뀌는 값이 있어야 항상 새로운 토큰을 만들 수 있다.
엑세스 토큰을 구현하는 방법이 한 가지만 있는 것도 아닐 테고 다양한 접근이 있겠지만 스펙화가 진행되고 있고 페이로드에 정보를 담을 수 있다는 점이 꽤 괜찮아 보인다.(대신 스펙이 진행된 지 오래되지 않아서 언제 취약점 같은 게 발견돼서 확 바뀔지도 모를 일이다.)
JWT 에서 검증 방법좀 바꾸면 어떨까싶은데요...
지금은 Hash ( secret + header + body) 방식이어서 다른데서 검증하려면 secret 도 같이줘야 되지만
public key encryption 쓰면
키 생성은 Crypt ( PrivateKey, Hash(header + body) ) 로 하고
검증은 Decrypt ( PublicKey, Crypt ( PrivateKey, Hash(header+body)) 하는 방식으로 하면 더 좋을꺼같아요ㅎㅎ
현재에서 알고리즘은 RSA 방식으로 하면 비슷하게 사용할 수도 있어 보입니다. 이런 건 요구사항이 다양해서 어떨지 모르겠지만 RSA 같은 경우는 양쪽에서 복호화를 하기 위함인데 토큰 같은 경우 서버외에 다른쪽에서도 복호화를 할 방법을 제공해야 하는지는 잘 모르겠습니다.