질문답변

oauth2-restapi-server: 모바일 앱을 OAuth2.0으로 좀 더 안전하게!

작성자 정보

  • 쉬운은유 작성
  • 작성일

컨텐츠 정보

본문

 https://vinebrancho.wordpress.com/2014/05/19/oauth2-restapi-server-%EB%AA%A8%EB%B0%94%EC%9D%BC-%EC%95%B1%EC%9D%84-oauth2-0%EC%9C%BC%EB%A1%9C-%EC%A2%80-%EB%8D%94-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C/

 

oauth2-restapi-server: 모바일 앱을 OAuth2.0으로 좀 더 안전하게!

개발중인 앱을 좀 더 안전하게 서비스하기 위해서 만든 백엔드 서버 프로토타입인 oauth2-restapi-server을 공유하고자 한다. oauth2-restapi-server 은 모두 node.js 위에서 동작하며 OAuth2.0 스펙대로 동작하는 Authorization server와 Authorization server가 발행한 access token으로 리소스 접근을 관리하는 매우 기본적인 RESTful API server로 이루어져 있다.

https://github.com/vinebrancho/oauth2-restapi-server

 당연한 얘기지만, 앱이 사용자의 개인정보와 리소스를 안전하게 보호되는 것을 보장해주지 않는다면 그 앱은 사용자로부터 외면받게 될 것이다. 그래서 앱 개발 시 사용자 인증(Authentication)과 리소스에 대한 권한부여(Authorization)는 반드시 필요하고 제공되어야 한다. 앱 개발시 인증과 권한 부여하는 방법에 관해 OAuth, OpenID, SAML 등 몇가지 스펙이 있는데, 이중에서 가장 많이 사용되고 twitter, facebook, google등 메이저급 업체들이 사용하는 스펙이 OAuth 이다. OAuth는 현재 2.0 까지 나왔으며, 최종 버전(final version)은 RFC 6749 (http://tools.ietf.org/html/rfc6749) 이다. OAuth 2.0은 인증 및 권한부여에 관한 기본적인 흐름(flow)뿐만 아니라 implementor가 자기의 목적에 맞게 확장할 수 있도록 해준다. 이 때문에 OAuth 2.0은 프로토콜인 동시에 프레임워크라고 부르기도 한다. OAuth 2.0 으로 서버와 클라이언트 간 인증을 마치면 서버는 권한부여의 결과로써 access token을 발행해주게 되는데, 클라이언트는 이 access token을 이용해서 서버로 리소스 접근을 요청할 수 있다. 서버는 access token 기반으로 리소스 접근을 허용할지 말지를 결정하고, 결과 데이터를 클라이언트에게 보내줄 수 있다. 서버는 access token 기반으로 클라이언트를 인식하고 서비스하기 때문에, 세션(session)이나 쿠키(cookie)를 이용해 클라이언트의 상태정보를 유지할 필요가 없다. 이것은 REST(Representational State Transfer) 디자인 컨셉의 주요한 요구사항인 ‘stateless’ 특성을 만족시켜준다. 그렇다. OAuth2.0의 access token을 이용해서 서버는 RESTful API를 클라이언트에게 제공할 수 있다. 실제로 OAuth2.0 을 제공하는twitter, facebook, google, yahoo, linkedin등 메이저급 앱의 서버들은 access token 기반의 RESTful API를 제공하고 있다. OAuth2.0에 대한 내용은 스펙문서 자체를 참고하길 바라며, 지금부터는 구체적으로 아래 내용들에 대해 살펴보고자 한다.

  • 앱스토어에 올릴 1st party client 에 OAuth2.0의 어떤 권한부여 타입을 적용할 것인가?
  • 인증 및 권한부여를 하는 authorization server는 어떻게 구현 하지?
  • 발행된 access token 기반으로 동작하는 RESTful API server는 어떻게 구현 하지?
  • oauth2-restapi-server 소스코드

잠깐, 문득 용어 정리가 필요할 듯 하다. 관점에 따라 앱은 다음과 같이 구성된다.

  • 사용자 관점에서 프론트엔드(front-end)와 백엔드(back-end)
  • 시스템 관점에서 클라이언트(client)와 서버(server)

앱 개발시 프론트엔드와 클라이언트, 그리고 백엔드와 서버는 동일한 의미로 사용된다. 클라이언트는 1st party client와 3rd party client 로 나눠진다. 1st party client 는 업체가 서비스를 위해 제공하는 공식 클라이언트이며 직접 사용자의 id와 password를 입력받을 수 있다. 그리고 3rd party client는 서비스에 접근하는 외부 클라이언트이며, 보안상의 이유로 해당 서비스 접근을 위해서 사용자로부터 직접적으로 id와 password 입력을 받지 못한다 (id, password 정보가 다른 앱에 저장될 위험이 있으니깐 말이다). OAuth2.0에서 서버는 authorization server와 resource server로 나눠진다. authorization server는 클라이언트들을 인증하고 access token을 발행해주는 서버이다. resource server는 클라이언트의 API 요청을 access token 기반으로 처리하는 서버이며 흔히 얘기하는 API server 이다. 자세한 것은 OAuth 2.0 스펙을 참고하기 바란다. OAuth를 이야기할 때는 클라이언트와 서버라는 용어를 사용하며, 본 글에서도 이 용어들을 사용할 것이다. 

앱스토어에 올릴 1st party client 에 OAuth2.0의 어떤 인증 타입을 적용할 것인가?


위에서도 언급했듯이  1st party client 는 특정 서비스를 위한 공식 클라이언트이다. 이 때문에 1st party client 는 등록된 사용자로부터 직접 id 와 password를 입력받아서 Authorization server 를 통해 사용자 인증을 하는데 그것들을 쓸 수 있다. 이러한 특성을 고려했을 때, 클라이언트 및 사용자 인증에 가장 적합한 인증 타입은 ‘Resource Owner Password Credentials’ 임을 찾을 수 있었다.

OAuth2.0 스펙에선 아래 4가지 타입의 인증 방식을 소개하고 있다.

  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

간략하게 핵심 내용을 설명하자면, Authorization Code와 Implicit 타입은 OAuth2 스펙을 보면 credential client 즉, 서비스에 접근하는 외부 앱을 위한 타입이다. Authorization Code는 자신만의 백엔드 서버를 갖고 동작하는 앱일 경우에, Implicit는 백엔드 서버 없이 순수 클라이언트 사이드에서만 동작하는 앱일 경우에 사용한다. 이들 외부 앱은 보안상의 이유로 사용자의 id와 password를 직접 입력 받지 못하기 때문에, access token을 얻기 위해서는 먼저 해당 서비스로 이동하여 사용자 확인(로그인) 및 허락을 받아야 한다. 이를 위해 외부 앱은 새로운 window 또는 webview 를 생성해서 해당 서비스의 Authorization server url에 client id와 callback url을 붙여서 리다이렉션 시킨다. Authroization server는 client id와 callback url을 검사하여 요청한 client가 developer site를 통해 등록된 정상적인 client인지 확인하는데 사용한다. 그리고 리다이렉션된 페이지에서 사용자가 id, password 로 로그인(Authentication)을 하고 해당 외부 앱이 자신의 데이터를 엑세스하도록 허용하면 (보통 ‘Allow’ 해당하는 버튼을 클릭함), Authorization server는 해당 외부 앱의 callback url에 인증타입에 따라 code 또는 access token을 붙여서 리다이렉션 시킨다. ‘Authorization Code’ 타입일 경우 callback url에 code가 붙으며, 외부 앱은 이 code 값과 해당 서비스에서 발급해준 자신의 client id와 secret 을 Authorization server로 다시 보내어 access token을 발급 받는다. 그리고 ‘Implicit’ 타입일 경우 바로 callback url에 access token정보가 fragment로 붙어서 넘어가는데 ‘Authorization Code’ 타입보다 간소화된 타입이라 보면 된다. client 는 사용자의 단말 또는 PC 에 설치되기 때문에 하드 코딩된 client id 값이 노출될 수도 있다. 그래서 client id 값만으로 access token을 받아오는 ‘Implicit’ 타입은 보안상 약간 위험할 수 있다. 그러나 ‘Authorization Code’ 의 경우, Authroization server와 통신하는 주체가 client가 아니라 백엔드 서버이고 client id와 secret이 백엔드서버에 저장되어 사용되기 때문에 노출될 여지가 없기 때문에 좀 더 보안상 좋다고 할 수 있다. 아무튼 이 두 타입의 인증방식은 용도상 외부 앱이 내 서비스에 접근하기 위한 것으로써 1st party client 인증용으로는 적절하지 않다. 그리고 마지막의 ‘Client Credentials’ 타입은 어떤 사용자든 상관없이 클라이언트 그 자체만을 인증하기 위한 방식으로써 사용자별로 인증하여 access token을 발행해야 하는 1st party client 인증용으로는 역시 적절하지 않다.

마지막으로 남은 ‘Resource Owner Password Credentials’. 이 타입은 아래의 OAuth2.0 스펙의 설명을 보면 1st party client 으로 적절해 보인다. 아 처음 스펙을 읽을 땐 왜 이렇게 이 문장이 ‘1st party client 를 위한 것이다!’ 라고 느껴지지 않았는지 모르겠다. 그냥 속시원하게 ‘1st party client 을 위해 적절하다’ 라고 명시해놓았으면 좋았을 것을 ㅠㅠ;

The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application (4.3)

1st party client 는 자신의 Authozation server 에 인증 요청을 하는 것이므로, 어떤 페이지로의 리다이렉션 없이 직접 사용자의 id, password를 입력받고 자기의 client id와 secret을 Authorization server로 인증 및 access token을 요청한다. 이 경우, 1st party client 는 사용자 단말에 설치되어 있어서 하드 코딩된 client id 와 secret 이 노출되는 것이 위험할 수도 있으나, 이 타입의 경우 사용자의 id, password 를 함께 보내야만 인증 가능하므로 사용자 id, password 만 단말 어딘가에 저장하지 않는다면 안전하다고 할 수 있겠다. 그래서 OAuth2.0 스펙에도 access token을 얻은후에 client는 반드시 사용자의 credentials 즉, id와 password를 제거해라고 언급되어 있다.

The client MUST discard the credentials once an access token has been obtained. (4.3.1)

결론은 1st party client 에는 ‘Resource Owner Password Credentials’ 타입을 적용하자! 이다.

 

인증 및 권한부여를 하는 authorization server는 어떻게 구현 하지?


우선 1st party client 만 배포하는 나로써는 Authorization server 구현시 ‘Resource Owner Password Credentials’ 타입만 고려하면 되지만, 혹시나 나중에 외부 앱이 나의 서비스에 접근해야 하는 요구사항이 생길 수도 있으니 미리 다른 인증 타입도 고려해서 만들면 낫지 않을까 생각했다. 그리고 어차피 node.js 로 백엔드를 구성하고 있기 때문에 Authorization server 역시 node.js 로 구현했다.

1.  유용한 node.js 사용하기

  • oauth2orize
  • https
  • passport-http
  • passport-oauth2-public-client

oauth2orize 는 OAuth2.0 스펙의 핵심 flow를 node.js 형태로 구현해놓은 모듈이다. 요청된 인증 타입(grant_type)에 따라 개발자가 등록해놓은 callback function을 호출해주고, 해당 function이 리턴한 결과를 적절히 http response로 만들어서 client에게 응답해주는 모듈이다. 나는 이 모듈을 이용해서 oauth2-restapi-server 의 뼈대를 만들 수 있었다. oauth2-restapi-server가 express 모듈을 사용하고 있기 때문에 아래에서 설명할 Authorization API 가 호출될 때,  oauth2orize의 grant, exchange, errorHandler가 실행되도록 oauth2orize 기능들을 모든 Authorization API에 express의 middleware로 지정하였다.

https 는 TLS (Transport Layer Security) 기반으로 서버가 동작할 수 있도록 해주는 모듈로써 client와 Authorization server 간 주고 받는 패킷에 적용하기 위해서 사용했다. 나는 Authorization server에 self signed certificate를 만들어서 client와 주고 받는 패킷을 encryption / decryption 하도록 만들었는데 실제 product 시점엔 공인된 CA로부터 발급받은 certificate 를 사용해야 하지 않을까 싶다. 그리고 http 로 요청이 들어와도 https 로 리다이렉션이 되도록, 리다이렉션 기능을 모든 API 에 express의 middleware로 지정하였다.

passport-http 는 Authorization server가 client id 와 secret으로 client 검증을 해야 하는 경우에 사용한다. ‘Authorization Code’ 타입의 client가 code로 access token 을 요청하는 경우, ‘Resource Owner Password Credentials’ 타입의 client가 access token을 요청하는 경우, (잘 쓰이지는 않지만) ‘Client Credentials’ 타입의 client가 access token을 요청한 경우가 해당된다. 이 모듈은 http header의 ‘Authorization: Basic’ 뒤에 base64로 인코딩된 스트링을 파싱하여 client id와 secret을 개발자가 등록한 callback function으로 넘겨준다. client id와 secret이 유효한지 여부를 검사하는건 개발자의 몫이다.

passport-oauth2-public-client 는 Authorization server가 client id 만으로 client 검증을 하는 경우에 사용한다. ‘Authorization Code’ 타입의 client가 access token 요청 전 code 자체를 얻기 위해서 요청하는 경우, ‘Implicit’ 타입의 클라이언트가 access token을 요청하는 경우가 해당된다. 이 모듈은 http body의 ‘client_id’ 값을 개발자가 등록한 callback function으로 넘겨준다. client id가 유효한지 여부를 검사하는 건 개발자의 몫이다.

2. Authorization을 위한 API 설정

1st party 든 3rd party 든 모든 client들은 인증 및 access token을 요청하기 위해서 url 형태의 API를 통해서 접근한다. 아래의 API로 4가지 인증 타입의 요청을 모두 받아들일 수 있다.

  1. /oauth2/authorize
  2. /oauth2/authorize/decision
  3. /oauth2/token

1, 2 번 API는 ‘Authorization Code’ 와 ‘Implicit’ 타입의 인증방식에서만 사용된다. 즉, 3rd party client 가 사용하게 되며 2번 API의 응답 결과가 해당 client가 등록될 때 지정한 callback url 뒤에 붙어서 넘어가게 된다. 응답 결과는 위에서 설명한대로 인증 타입에 따라 code또는 access token 둘 중 하나이다. 3번 API는 ‘Authorization Code’, ‘Resource Owner Password Credentials’, ‘Client Credentials’ 타입의 인증방식에서 사용된다. API 요청시 client는 OAuth2.0 스펙에서 요구하는 header와 body를 만들어서 요청해야 하며, authorization server는 이 값들을 파싱하여 client 검증 또는 사용자 검증을 하는데 사용해야 한다. API 사용법에 대한 자세한 설명은 oauth2-restapi-server github 페이지를 참고하길 바란다.

authorization의 의미대로 권한부여를 제대로 하려면 client 별로 사용자별로 접근할 수 있는 리소스를 제한하는 기능이 있어야 한다. 이를 위해 OAuth2.0 스펙에서는 ‘scope’ 이라는 필드가 있는데 제대로 authorization server를 만드려면 리소스에 대한 scope를 정의하고 http body의 ‘scope’ 필드를 활용할 필요가 있다. 여기서는 주제를 벗어나므로 자세한 내용은 생략한다. oauth2-restapi-server 는 아직 ‘scope’ 필드를 활용하고 있지 않다. 그래서 발행되는 모든 access token 은 API server 내 특정 사용자의 모든 자원에 대해 read/write 가능하다.

3. 필수 database 설정

OAuth2.0 스펙대로 동작하려면 기본적으로 아래와 같은 데이터를 Authorization server가 갖고 있어야 한다.

  • 등록된 client들의 id 와 secret 정보 (보통 developer site 에서 각 client 개발자가 등록함, 난 테스트를 위해 디폴트로 하나씩 등록함)
  • 등록된 사용자들의 id 와 password 정보
  • access token을 얻기 위해 발행된 code 정보 (‘Authorization Code’ 타입을 위해서만 사용)
  • 발행된 access token 정보

나는 mongoose 모듈을 이용해 authorization server가 mongo db 에 이러한 정보들은 저장하고 검색하고 삭제하도록 구성하였다.

4. 테스트를 위한 4가지 인증 타입별 client 정보를 등록

authorization server는 모든 요청에 대해 client 검증을 하기 때문에 타입별로 client 정보를 등록할 필요가 있다. 등록을 위한 developer site 가 있다면 편리하겠지만 여력이 없어서 단순히 authorization server가 런칭될 때, 디폴트로 client 들이 등록되도록 하였다. 이 때 등록되는 정보들은 다음과 같다.

  • client name *
  • client id *
  • client secret *
  • token refresh 여부 *
  • callback url

‘ * ‘ 붙은 정보는 mandatory 이며, callback url은 ‘Authorization Code’와 ‘Implicit’ 타입에서만 사용된다.

5. 인증 및 access token 발행 테스트

각 타입별로 client 를 만들어서 확인해보면 가장 좋겠지만, Authorization server에게 요청/응답 받는 건 ‘postman’ 같은 http client로도 충분하다. 나는 크롬의 extension인 ‘postman’을 사용해서 위의 4가지 타입의 client들을 API로 검증할 수 있었다. 1st party client를 위한  ‘Resource Owner Password Credentials’ 타입에 대해서는 특별히 angular.js 기반의 client를 만들었다.

6. access token 저장 및 소셜 앱을 위해 확장한 기능

angular.js 기반의 1st party client는 local account로 로그인한 직후, ‘Resource Owner  Password Credentials’ 타입으로 access token을 요청하고 받아온다. 그리고 해당 access token은 local storage에 보관하여 client 종료후 다시 실행시 local storage에서 읽혀 다시 사용된다 (만약 명시적으로 authorization server에 API로 logout을 요청하면 authorization server는 해당 client와 사용자를 위해서 생성했던 access token을 제거하고, client 역시 local storage에 저장된 access token을 제거한다). 개인적으로 고민했던 부분은 access token을 받아온 후 client가 이것을 어떻게 보관하느냐 하는 부분이었다. 현재로써 결론은 client 가 access token을 저장할 필요가 있으니, Android/iOS 플랫폼에서 app isolation을 보장해준다는 가정하에 client가 살아있는 동안 영구적으로 보관하기 위해  local storage를 선택했다.

이 angular.js client는 local account 로 signup 및 login을 지원하며, 외부 social account 로 연결(connect)이 가능하다.  소셜 앱을 개발한다면 사용자들의 편의를 위해서 local account 를 생성하는 것 없이 1st party client 가 twitter, facebook 같은 외부 social account 인증만으로도 signup을 해줄 필요가 생긴다. 이 경우에 사용자의 social account로 1st party client를 로그인 한 후 RESTful API로 내부 리소스를 접근하려면 access token이 필요하다. local account 가 아닌데 ‘Resource Owner Password Credentials’ 타입을 어떻게 이용하지? 고민을 하다 생각해낸 아이디어는 1st party client가 social account로 login 하게 되면 결과값으로 twitter 등 해당 social 서버가 발행해준 access token을 client로 보내주고, client는 http body의 ‘username’과 ‘password’ 필드에 각각 해당 서비스 이름과 해당 서비스의 서버에서 발행해준 access token을 넣어주도록 하는 것이다. 그럼 authorization server는 ‘username’이 local account의 사용자 id가 아닌 서비스 이름일 경우 password에 있는 access token 값으로 내부 database를 검색하여 등록된 social account인지 체크 후, RESTful API 사용을 위해서 access token을 발행해주는 것이다. 테스트 결과 잘 동작하였으며 외부 social account을 지원하는 앱을 만드는 경우라면 참고해도 될 것 같다.

 

 발행된 access token 기반으로 동작하는 RESTful API server는 어떻게 구현 하지?


client가 Authorization server를 통해 access token을 얻었다면, 그 access token을 갖고 특정 리소스에 접근하기 위해서 API를 호출할 것이다. 이 때 API server는 오직 access token 하나만으로 해당 요청을 받아야 할지 말지를 결정하고 적절히 그 요청을 처리해야 한다. OAuth2.0에서는 client로부터의 API 요청시 authorization server와 API server 중 누가 access token의 유효성을 검사해야 하는지 정해놓지 않았다. 그래서 implementor가 성능, 보안을 고려해서 자신의 상황에 맞게 선택해야 한다. 나의 경우 API server가 직접 access token 을 검사하도록 만들었다. API server 역시node.js 로 구현하였고 내용은 다음과 같다.

1. 유용한 node.js 모듈 사용하기

  • https
  • passport-http-bearer

API server 에 역시 안전하게 client와 패킷 교환을 해야하기 때문에 https 를 적용했다.

passport-http-bearer 는 http header에 있는 ‘Authorization Bearer:’ 뒤에 있는 스트링인 access token을 개발자가 등록한 callback function으로 넘겨준다. access token 이 유효한지 여부를 검사하는 건 개발자의 몫이다. API server는 자신에게 들어온 API 요청을 처리하기 전에 전달받은 access token이 유효한지 검증하는데 이 모듈을 사용한다.

관련자료

댓글 0
등록된 댓글이 없습니다.

질문답변

최근글


새댓글


  • 댓글이 없습니다.
알림 0