ORACLE
HOME > DB > ORACLE
2018.12.07 / 17:36

JWT(Json Web Token)을 이용한 모바일 인증

hanulbit
추천 수 39

토큰 방식의 모바일 유저 인증에 대해 잘 정리되어 있는 글이 있어 번역하며 정리해본다.

출처: The Ultimate Guide to Mobile API Security

모바일 API 보안의 문제점

가장 기초적인 API 보안의 형태는 HTTP Basic Authentication(HTTP 기본 인증)이다. 이 방식은 매우 간결하게 작동하기 때문에 API 서버를 만드는 사람이나 API를 활용할 개발자 모두에게 편리함을 제공해준다.
기본적인 작동방식은 다음과 같다.

  • 개발자에게 API키(보통 ID와 PW)가 주어진다. 이 API키는 다음과 같은 형태를 보인다.
    • 3bb743bbd45d4eb8ae31e16b9f83c9ba:ffb7d6369eb84580ad2e52ca3fc06c9d.
  • 개발자는 발급받은 API키를 자신의 서버 내 안전한 곳에 보관해야 할 책임이 있다.
  • 개발자는 API키를 HTTP 인증 헤더에 담아 API 서버에 request를 날린다.
  • Request를 cURL로 나타내면 다음과 같다.
$ curl --user 3bb743bbd45d4eb8ae31e16b9f83c9ba:ffb7d6369eb84580ad2e52ca3fc06c9d https://api.example.com/v1/test
  • cURL툴은 API인증값을 받아와 base64 인코딩을 거쳐 다음과 같은 형태의 HTTP 인증 헤더를 만들어 낸다.
    • Basic M2JiNzQzYmJkNDVkNGViOGFlMzFlMTZiOWY4M2M5YmE6Zm…
  • API 서버에서는 이 프로세스를 역순으로 행한다. 서버에서 HTTP헤더를 찾아내면, base64 디코딩을 하여 API키로부터 ID와 PW를 뽑아내고 검증을 한 뒤에 응답을 계속 진행할 것인지 결정한다.

HTTP 기본 인증 방식은 간결하면서 훌륭한 인증방식이다. 개발자는 API키를 발급받는 것 만으로 API서비스를 간편하게 사용할 수 있다.

HTTP 기본 인증 방식이 모바일 앱에 적합하지 않은 이유는 API키를 모바일 기기 내에 안전하게 저장하는 이슈 때문이다. 또한 HTTP 기본 인증 방식은 Raw한 API키를 매 요청때마다 사용해야 하므로, 시간이 갈수록 취약점 공격에 노출될 가능성이 높아진다. 또한 API키가 내장된 채로 배포된 앱을 리버스 엔지니어링 하면 API키를 알아내는 것이 가능해지기 때문에 API 서비스가 악용될 소지가 있다. 대부분의 경우 다수에게 배포되는 모바일 앱 내에 안전하게 API키를 보관하는 방법은 없다고 볼 수 있다. ??문에 HTTP 기본 인증 방식은 웹 브라우저나 모바일 앱과 같은 신뢰하기 힘든 환경에 적합한 인증 방식은 아니다.

모바일 API보안에 OAuth2 도입하기

OAuth2는 신뢰하기 어려운 기기(모바일)에서 사용되는 API 서비스의 보안을 위한 훌륭한 프로토콜이다. 또한 모바일 사용자를 인증할 수 있는 수단으로 토큰 인증방식을 제공한다. 사용자의 관점에서 OAuth2 토큰 인증은 다음과 같이 진행된다.(OAuth2는 이 프로세스를 Password Grant Flow라고 부른다.)

  1. 사용자가 모바일 앱을 실행하고, ID혹은 메일 주소와 비밀번호를 입력하는 창이 뜬다.
  2. 모바일 앱에서 입력 받은 사용자의 정보를 POST 방식으로 API 서버로 보낸다.(SSL)
  3. 유저 인증값을 검증하고 일정 시간이 지나면 만료될 액세스 토큰을 생성한다.
  4. 발급?쩜? 액세스 토큰을 모바일 기기에 저장한다. API토큰과 마찬가지로 보안이 적용되는 안전한 장소에 보관해야 하며, API 서비스에 접근할 때 이 토큰을 사용한다.
  5. 액세스 토큰이 만료되면 더이상 작동하지 않으며, ID 혹은 메일 주소와 비밀번호를 입력하는 창을 다시 띄운다.

OAuth2가 API 보안에 좋은 이유는 API키 자체를 안전하지 않은 환경에 저장할 필요가 없기 때문이다. 대신에 액세스 토큰을 생성하여 안전하지 않은 환경에 임시로 저장해놓기만 하면 된다. 공격자가 액세스 토큰에 접근할 수 있게 되더라도 일정 시간이 지나면 만료되기 ??문에 잠재적인 피해를 줄일 수 있다. 
OAuth2에서 API에 접근할 때 필요한 액세스 토큰을 발급 받았다면, 모바일 기기 어딘가에 저장을 해야 한다. 토큰 저장소는 개발을 진행하는 플랫폼에 따라 정해진다. 예를 들어 안드로이드 앱을 개발하고 있다면 모든 액세스 토큰을 SharedPreference에 저장할 것이다. iOS의 경우 액세스 토큰을 Keychain에 저장하게 된다. 이에 관해 StackOverflow에 잘 정리된 내용이 있다.

액세스 토큰

액세스 토큰은 JWT(JSON Web Token)를 주로 사용하는데, JWT는 다음과 같은 특징을 갖는다.

  • 클라이언트로 발행될 수 있다.
  • 당신이 생성했다는 사실을 증명할 수 있다.(서명)
  • 일정 시간 뒤에 자동으로 만료된다.
  • JSON타입으로 변수 정보를 저장할 수 있다.
  • 서버에서 쿼리하지 않아도 사용자를 로컬에서 검증할 수 있기 때문에 API 호출의 횟수를 줄일 수 있다.

JWT는 다음과 같은 방식으로 항상 암호화 서명을 사용한다(Cryptographically signed).

  • 보안을 위해 랜덤 문자열을 생성하고, API 서버에 저장한다. 40자 정도면 적합하다.
  • 새 JWT를 만들 때 이 문자열을 JWT라이브러리로 전달하여 저장할 JSON데이터(ID, 권한 등)과 함께 토큰에 서명한다.
  • 토큰이 생성되면 다음과 같이 나타난다.
    • header.claims.signature
    • 헤더와 클레임 그리고 서명은 base64로 인코딩 된 긴 문자열 형태로 나타난다.

모바일 클라이언트에서도 JWT에 저장된 내용을 볼 수 있다. JWT라이브러리가 있으면 JWT내부의 JSON데이터를 쉽게 확인할 수 있기 때문이다. 저장된 데이터는 일반적으로 다음과 같은 형태로 나타난다.

{
  "user_id": "e3457285-b604-4990-b902-960bcadb0693",
  "scope": "can-read can-write"
}

JWT는 토큰 만료 기능을 기본적으로 지원하기 때문에 토큰만을 가지고 토큰의 유효성을 확인할 수 있다. 따라서 JWT 라이브러리를 사용한다면 API호출 없이도 로컬에서 JWT의 유효성을 검사할 수 있다. JWT인증 시스템은 서버단에서도 훌륭한 작업을 수행한다. 예를 들어 다음과 같은 JWT를 모바일 앱에 발급했다고 가정해보자.

{
  "user_id": "e3457285-b604-4990-b902-960bcadb0693",
  "scope": "can-read can-write"
}

그리고 모바일 멀웨어나 해커가 토큰을 다음과 같이 조작했다.

{
  "user_id": "e3457285-b604-4990-b902-960bcadb0693",
  "scope": "can-read can-write can-delete"
}

위와 같이 삭제 권한을 조작하여 부여했을 경우 API 서버에서도 동작할 것인가? 답은 아니다. API 서버가 JWT를 수신하고 검증할 때 다음과 같은 작업을 수행하기 때문이다.

  • 서버만 알고 있는 비밀 문자열을 사용해 토큰이 훼손되지 않았는지 체크한다(앞서 보았던 암호서명을 이용).
    • JWT가 조작되었다면 이 단계를 통과하지 못한다.
  • JWT의 만료 시간을 체크하여 아직 유효한 JWT인지 검증한다.
    • 오래 된 JWT를 이용해 API 요청을 한다면 거부된다.

JWT의 이러한 기능은 인증 / 만료 / 보안을 매우 간결하게 만들어 준다. JWT를 이용해 서비스를 개발할 때 명심해야 할 점은 오직 하나이다. 유출되어도 상관없는 정보만 저장한다.

작동방식

JWT process flow

  1. 유저가 앱을 실행
  2. 앱이 인증 정보를 요구함
    • OAuth2의 암호 부여 타입 스키마를 사용해 사용자 인증을 하기 때문에 ID와 PW가 필요하다.
  3. 사용자가 인증 정보를 입력함
  4. 앱이 API 서버로 POST request를 보냄
    • OAuth2 프로세스가 시작되는 부분
    • 단순히 API 서버로 HTTP Post 요청만 전송
  5. API 서버에서 유저 인증
    • 사용자의 계정과 PW가 일치하는지 확인
  6. API 서버에서 모바일 앱에 저장할 JWT를 생성
    • OAuth2 인증을 통과했기 때문에 액세스 토큰을 발급해 모바일 앱으로 전달
    • ID와 사용자 권한 그리고 앱에서 바로 접근할 필요가 있는 어떤 정보든 JSON타입으로 담는다.
    • JWT를 생성했으면, 앱으로 아래와 같은 형태의 JSON Response를 보낸다.
    • {  
        "access_token":     "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEUExSSTVUTEVNMjFTQzNER0xHUjBJOFpYIiwiaXNzIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvNWpvQVVKdFZONHNkT3dUVVJEc0VDNSIsImlhdCI6MTQwNjY1OTkxMCwiZXhwIjoxNDA2NjYzNTEwLCJzY29wZSI6IiJ9.ypDMDMMCRCtDhWPMMc9l_Q-O-rj5LATalHYa3droYkY",
        "token_type": "bearer",
        "expires_in": 3600
      }
      
    • JSON Response는 세 개의 필드를 갖는다. 첫 번째 필드는 액세스 토큰이다. 이 토큰은 API 요청 시 인증을 할 때 사용하게 된다.
    • 두 번째 필드는 토큰 타입이다. 이것은 앱에 어떤 종류의 토큰을 제공했는지 알려주는 역할을 한다. 여기서는 OAuth2의 Bearer Token을 제공했다.
    • 세 번째 필드는 만료기한 필드이다. 이 필드는 초 단위로 표시되기 때문에 위 토큰은 3600초(1시간)뒤에 만료될 것이다.
    • 클라이언트 단에서는 JSON Response를 받아와 액세스 토큰을 파싱 한 뒤에 안전한 기기 내부의 공간에 저장한다.
  7. 앱에서 서버로 인증된 요청을 보낸다.
    • 저장된 JWT를 사용해 HTTP 인증 헤더를 만들고 API 요청을 보내 사용자를 인증할 수 있도록 한다.
    • 예를 들어 cURL을 이용해 Bearer 타입의 토큰을 보낸다면 다음과 같은 형태를 보일 것이다.
    • $ curl -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEUExSSTVUTEVNMjFTQzNER0xHUjBJOFpYIiwiaXNzIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvNWpvQVVKdFZONHNkT3dUVVJEc0VDNSIsImlhdCI6MTQwNjY1OTkxMCwiZXhwIjoxNDA2NjYzNTEwLCJzY29wZSI6IiJ9.ypDMDMMCRCtDhWPMMc9l_Q-O-rj5LATalHYa3droYkY" https://api.example.com/v1/test
      
    • 여기에서 인증 헤더 부분은 다음과 같다.
    • Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJEUExSSTVUTEVNMjFTQzNER0xHUjBJOFpYIiwiaXNzIjoiaHR0cHM6Ly9hcGkuc3Rvcm1wYXRoLmNvbS92MS9hcHBsaWNhdGlvbnMvNWpvQVVKdFZONHNkT3dUVVJEc0VDNSIsImlhdCI6MTQwNjY1OTkxMCwiZXhwIjoxNDA2NjYzNTEwLCJzY29wZSI6IiJ9.ypDMDMMCRCtDhWPMMc9l_Q-O-rj5LATalHYa3droYkY
      
    • 이 요청을 받은 서버단에서는 다음과 같은 활동을 수행하게 된다.
      • HTTP 인증 헤더 값을 검사하여 Bearer가 들어있음을 확인한다.
      • 뒤에 따라오는 문자열 값을 가져와 액세스 토큰으로 인식한다.
      • JWT라이브러리를 사용해 액세스토큰이 유효하며, 서명되어있고, 만료되지 않았는지 검사하고 검증한다.
      • 사용자의 ID와 권한을 확인한다.
      • 사용자의 계정을 데이터베이스로부터 가져온다.
      • 모든 것이 통과된다면 API 요청에 맞는 응답을 해준다.