Blog

RESTful, Stateless, HATEOAS 그리고 Passport

June 10, 2014

RESTful, Stateless, HATEOAS 그리고 Passport

RESTful, Stateless, HATEOAS

환영합니다! Passport 세계에 온 낯선이여. 이제 당신은 곧 무력감 을 느끼게 될 것입니다.

이 시리즈의 목적은 Node.js 에서 RESTful 하게 세션 없이 오오 인증을 하기 위함입니다. 목적이 거창한 만큼 여정도 험난한 법! 다음과 같은 내용들을 차례로 다루겠습니다.

1. RESTful
2. OAuth and Passport
3. Passport Strategy

먼저 RESTful 한 웹서비스가 무엇인지 개략적으로 다룰겁니다. 인증과 관련된 조건인 Stateless 와 아직 까지 대다수 서비스가 구현하지 않고 있는 HATEOAS 를 개념적으로 다루겠습니다.

2부에서는 1부에서 논의한 인증 방법 중 API Key 방식과 OAuth 을 구현하기 위해 passport.js 를 사용하겠습니다. Stateless 하게요! 구현에 앞서 OAuth 의 개념과 Node.js 의 OAuth 와 그 프레임워크인 passport 에 대해 알아야 합니다.

3부에서는 passport-local 의 사용을 통해 passport 의 strategy 를 어떻게 사용하는지 알아보고, 2부에서 언급한 API key 방식과 OAuth 를 구현하기 위해 passport strategy 를 선택하여 간단한 웹 애플리케이션을 구현해봅시다

1. RESTful

REST, Representational State Transfer 은 이름 그대로 모든 요청에서 Client 의 상태(State) Representational 하게 전송합니다. Representational 하다는건, Client 의 요청이 URI 상에서 의미가 잘 드러난다는 뜻입니다.

;; Non-RESTful URI
http://example.com/cotnents?lists=3&id=1

;; RESTful URI
http://example/com/contents/lists/3/id/1

* Stateless

Stateless 하다는 것은 서버가 어떠한 Client 의 Status 도 저장하지 않는다는 뜻입니다. 따라서 Client 는 매 요청마다 자신을 인증할 수 있는 Token 이나 Key 등을 Request 에 포함시켜 전송합니다. 만약 클라이언트의 요청이 Stateful 하다면 서버가 클라이언트의 현재 상태를 저장해야하고, 클라이언트의 상태는 해당 서버에 종속이 됩니다. 만약 대규모 환경에서 동일한 웹서버를 다수 배치해 로드밸런싱을 하는 경우에 각 서버가 클라이언트의 상태, 세션을 공유할 수 있는 Redis 같은 별도의 시스템이 필요합니다. 그러나 Stateless 하다면 클라이언트의 요청은 어느 서버가 처리하던지 동일하게 처리할 수 있습니다. 클라이언트의 상태는 클라이언트의 요청 안에 모두 들어있으니까요.

* Authentication

서버에 Session 과 같은 정보가 저장되지 않으면, 그래서 Stateless 하다면 매번 인증을 위해 아이디와 패스워드를 보내야 하는건가요? 원래는 그래야 합니다. 하지만 여기 POST 로 ID 와 Password 를 Plain 하게 매번 보내는 대신 사용할 수 있는 몇 가지 기술이 있습니다.

1. HTTP Basic Auth

HTTP Basic Authentication (BA) 은 사용자의 ID 와 패스워드를 Base64 인코딩 (!= 암호화) 하여 HTTP Header 로 보냅니다. 따라서 암호화 하지 않기 때문에 SSL 과 같이 쓰시거나 암호화 할 수 있는 다른 방법과 같이 사용해야 합니다. BA 과정은 요약하면 아래와 같습니다.

  1. 서버가 클라이언트에게 인증을 원한다면, Authentication Request 를 보내야 합니다. HTTP 401 Not Authorized 코드와 함께 WWW-Authenticate HTTP Header 가 같이 보내져야 합니다.
  2. 클라이언트는 username:password 형식의 Plain Text 를 Base64 로 인코딩해서 Authentication Header 로 보냅니다. 이렇게 생겼습니다.
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

그런데, HTTP Basic Authentication 은 단점이 하나 있습니다. 로그아웃을 지원하지 않습니다. 그러나 브라우저마다 BA 에 대해 로그아웃을 지원하기 위한 방법이 다양한데, 가장 기본적인 방법은 부정확한 패스워드로 다시 BA Header 를 보내는것입니다.

2. HTTP Digest Authentication

HTTP Digest Authentication 은 Http Basic Authentication 의 단점을 극복하기 위해 나온 기술입니다.

  • 해쉬 함수를 통해 사용자의 ID 와 Password 를 암호화 합니다. MD5(username:realm:password) 와 같은 식으로
  • 클라이언트가 무작위로 생성한 nonce 를 통해 Rainbow Table 과 같은 해시 함수 자체에 대한 공격을 방지할 수 있습니다.
  • 서버 nonce 는 타임스탬프를 포함할 수 있어 Replay Attack 을 방지할 수 있습니다.

그러나 HTTP Digest Auth 는 몇 가지 문제점이 있습니다.

  • Man-in-the-middle Attack 에 취약합니다. Digest Auth 는 클라이언트가 서버를 ‘확인’ 할 수 있는 방법을 제공하지 않기 때문에 공격자가 클라이언트에게 ‘HTTP Basic Auth 를 이용하라’ 고 말할 수 있습니다.
  • bcrypt 와 같은 강력한 해시 방법을 사용할 수 없습니다. 클라이언트로부터 오는 것은 ID, Password 가 아니라 nonce, realm 까지 뭉텅이로 Digested 되어 오는 데이터입니다.

HTTP Basic Auth 와 HTTP Digest Auth 에 대해 더 알고싶으시다면, Understanding HTTP Authentication In Depth 를 참조하세요.

3. API Key, Access Token

아무리 HTTPS 를 쓰고 있다고 하더라도! 매번 Plain Text 로 ID, Password (Credential) 을 보내기는 조금 꺼림칙합니다. 그래서

  • 처음 로그인시에만 Credential 을 보내고,
  • 정상적으로 로그인이 되면 Access Token 을 받아
  • 매 요청마다 Access Token 을 보내면서 리소스에 접근

이렇게 Credential 대신 보낼 무언갈 만들어 요청을 보낼 수 있습니다.

4. OAuth

OAuth 는 사용자(Resource Owner) 가 웹서비스(Client) 에게 로그인을 시도 했을때, 웹 서비스(Client) 가 페이스북(Auth Server) 에게 인증을 받아 적절한 권한이 부여된 Token 을 얻은 뒤, 필요한 사용자의 정보(프로필 이미지, 출신 학교) 등등을 페이스북(Resource Server) 에서 권한 확인 후 얻어오는걸 말합니다. 이미지로 보시는게 훠어어어얼씬 이해가 빠르겠습니다. OAuth 에 대해선 다음 챕터에서 다룰 것이므로 이만 줄이겠습니다. 더 자세한 내용은 OAUTH 2.0 – OPEN API 인증을 위한 만능 도구상자

(출처 – http://wiki.scn.sap.com/wiki/display/Security/Using+OAuth+2.0+from+a+Web+Application+with+Authorization+Code+Flow)

* Stateless?

Roy Fielding 씨가 어떤 생각을 처음에 했을진 모르겠으나, REST 에서 Stateless 의 개념은 난해하고 논란이 좀 많습니다. 그러나 이 개념을 구현함으서 얻을 수 있는 Visibility, Reliability, Scalability 에 비해서 매 요청마다 진행되는 인증때문에 잃을 CPU, Network Resource 등이 더 클 수도 있습니다.

여기 Stackoverlfow 에서 RESTful Authentication 으로 가장 많은 추천을 받고, 제가 읽어 본 수십개의 질문 중 가장 유용하다고 생각하는 몇 가지 답변들을 링크 해 놓았습니다. 흔히 Stateless 대해 오해하는 것들과, RESTful 하게 구현하는데 도움이 될만한 답변들이 있습니다.

  1. http://stackoverflow.com/questions/319530/restful-authentication
  2. http://stackoverflow.com/questions/6068113/do-sessions-really-violate-restfulness
  3. http://stackoverflow.com/questions/15051712/how-to-do-authentication-with-a-rest-api-right-browser-native-clients

그리고 Roy Fielding 이 작성한 논문에 근거해 Stateless 제약 조건에 대해 논한 아티클이 하나 있습니다.

THE REST Statelessness Constraint

* HTTP

RESTful 한 웹 서비스에서는 클라이언트가 요청하는 Action 을 URI 상에 포함시키는 것이 아니라 HTTP 메소드로 대신해야 합니다. 예를 들어서 클라이언트가 요청하는 데이터베이스의 CRUD 연산은 다음 HTTP 메소드로 표현되어야 합니다.

Database HTTP
Create POST
Read GET
Update PUT
Delete DELETE

너무 간단하게 REST 를 짚고 넘어가는 것 같습니다. 사실 REST 는 단순히 아키텍쳐 일 뿐입니다. REST, HTTP, 그리고 HTTP 를 RESTful 하게 이용하는 것은 모두 다 다른것임을 비롯해서 HATEOAS, Self-descriptive message 등 여러분들이 REST 에 대해서 꼭 알아야 할 내용들이 더 있습니다. 그러기에 REST 와 관련된 포스트들을 링크합니다.

  1. RESTful Web Services: The Basics (1/2) 번역
  2. RESTful Web Services: The Basics (2/2) 번역
  3. 실용적인 REST 이야기 (1/2)
  4. 실용적인 REST 이야기 (2/2)
  5. 당신의 API 가 RESTful 하지 않은 5가지 증거 (번역)

* HATEOAS

이야기가 나온김에 HATEOAS 에 대해 조금 더 이야기 해 보려고 합니다. 마틴 파울러가 2010년 3월에 작성한 Richardson Maturity Model (원문) (번역) 에 웹서비스들이 RESTful 을 향해 나아가는 단계를 다룬 이미지가 있습니다.

0 단계는 End-point 그 자체가 하나의 서비스입니다. 오오

POST /appointmentServie

1 단계는 URI 를 Representational 하게 작성하는 단계입니다. 그러나 POST와 GET의 의미도 없고, 단순히 HTTP 요청을 보내기 위해 사용합니다.

POST /doctors/mjones
POST /slots/1234

2단계는 HTTP Method 에 의미를 부여해 Verbs, 즉 Action 처럼 활용하는 단계입니다. 각 HTTP 메소드 POST, GET, PUT, DELETE 가 CRUD 에 대응됩니다.

마지막 3단계는 클라이언트의 State 를 link 로 컨트롤 하는 단계입니다. 무슨 말인고 하니, RESTful 에서 클라이언트의 상태(State) 는 URI 로 표현됩니다. 즉 클라이언트가 어디 있는가(Where the user is) 가 상태(State) 입니다. 이런 State 를 변화시킬 수 있는 새로운 URI 를 서버로부터 받아, 다시 State 를 변화시키는 것이 HATEOAS, Hypermedia as the engine of application state 의 개념입니다.

마틴 파울러의 글에 나와 있는 그림입니다. 먼저 doctors/mjones/slots 로 열려있는 슬롯을 찾고, 비어있는 슬롯으로의 link 가 포함된 Response 가 돌아옵니다. 일반적인 웹 서비스라면, 슬롯의 id 가 Response 로 날아왔을겁니다. 그리고 이 id 값을 이용해 다시 /slots/:id 로 POST 등의 요청을 날려 새로운 약속을 만들 수 있을겁니다.

그러나 link 가 돌아오는 것(HATEOAS, LEVEL 3)과, id 가 돌아오는 것(LEVEL 2)에는 큰 차이가 있습니다. id 를 돌려주는 일반적인(지금까지) 방식은 클라이언트가 id 를 받아 /slots/:id 로 다시 요청d을 보낼때, 자신이 요청 보낼 URI 에 리소스가 있을것이라 가정 하고 요청을 보냅니다. 코드는 아마 이렇게 생겼을겁니다.

$.get('/doctors/mjones/slots?date=20140607&status=open', funtion(id) {
    $.post('/slots/' + id);
}

프로그래머가 이렇게 코딩했을 당시에는 /slots/:id 란 리소스가 있었을지 모르겠지만, 나중에는 없을 수도 있습니다. API 가 바뀌어 /username/slots/:id 로 바뀔 수도 있습니다. 물론 그때 다시 클라이언트 사이드를 다시 코딩할 수도 있겠지만요. 중요한건 클라이언트가 리소스에 대해 가정 하고 요청을 보낸다는 것입니다.

하지만 HATEOAS 하게 우리 애플리케이션을 만든다면, 클라이언트가 요청하는 모든 URI 는 서버로 부터 받은 Response 내에 있는 것이고, 이것은 URI 가 유효함을 말해줍니다. 가정이 아니라 서버로 부터 온 것이니까요.

여기 HATEOAS 적인 응답의 예제가 하나 더 있습니다. 한번 눈으로 훑어보세요.

{
    "content": [ {
        "price": 499.00,
        "description": "Apple tablet device",
        "name": "iPad",
        "links": [ {
            "rel": "self",
            "href": "http://localhost:8080/product/1"
        } ],
        "attributes": {
            "connector": "socket"
        }
    }, {
        "price": 49.00,
        "description": "Dock for iPhone/iPad",
        "name": "Dock",
        "links": [ {
            "rel": "self",
            "href": "http://localhost:8080/product/3"
        } ],
        "attributes": {
            "connector": "plug"
        }
    } ],
    "links": [ {
        "rel": "product.search",
        "href": "http://localhost:8080/product/search"
    } ]
}   

아래는 HATEOAS 에 대한 여러분의 궁금증을 해소해 줄 수 있는 링크입니다.

  1. Why HATEOAS
  2. From REST to HATEOAS