mod_mrubyでJWTベースの認証proxyを作る

![hello JWT](http://www.mediabistro.com/agencyspy/files/2011/08/jwtpandora.jpg)

 こんにちは．mod_mruby ngx_mruby Advent Calendar 2014 12日目にお邪魔いたします！[@ainoya](https://gihtub.com/ainoya)です．mod_mruby/ngx_mrubyとの出会いはカレンダー8日目の[@mookjp](https://gihtub.com/mookjp)の紹介にもありました[pool](https://github.com/mookjp/pool)がきっかけです.

 mod_mruby/ngx_mrubyの利点は高速であることもさることながら，なじみのあるrubyの文法でproxyレイヤの制御を 柔軟に記述できることも大変すてきな点だと考えています．今日はmod_mrubyを使って，jwtを利用して認証認可制御を行うproxyを作ってみた件について紹介します！


## JWT(JSON With Token)とは

![jwt-io-jpg]

JWT方式でのエンコード/デコード(jwt.ioより引用)

 JSON With Token(JWT)とは，[JSONで署名や認証などを行うために決められた仕様][jwt.io]のことです．
仕様では署名のためのJSONの構造が決められており，"署名方式を定義するヘッダ"と"格納情報を定義"などを
JSONに記述します．

    # JWTに使用する署名方式などの定義
    "header": {
        "typ":"JWT",
        "alg":"HS256"
    },
    # ritouさんの記事より一部引用 http://d.hatena.ne.jp/ritou/20140927/1411811648
    # 格納する情報
    "payload": {
     "sub": "(リソースオーナーのユーザー識別子)",
     "token_id": "(認可情報 or Access Tokenに対して一意な識別子)",
     "aud": "(client_id)",
     "exp": (有効期限のタイムスタンプ),
     "scope": "(このAccess Tokenに関連付けられたScope)"
    }

  リソースアクセスで認証をかけたいと思ったときに，JWTをつかうと次のようなありがたみがあると考えています．

- 認証情報をJSON形式で格納しているので，アプリケーションで扱い易い
- 認証情報はエンコードされたトークンの形で単純な文字列となるので，認証データ管理DBに優しい(KVSに雑にしまっておける)

　ちょうど認証機能のついたリソースサーバを作りたいと思っていたところで，[ritouさんのブログ][jwt]を見て
JWTを使った認証方式について学び，mod_mrubyもちょっと使えるようになってきたので，jwtをベースにした
proxyを作ってみようとしたのが今回の記事にいたった経緯でございます．

## 認証proxyを作る

### 全体構成

![demo-arch]

　まずは今回作ってみた仕組みの全体像です．登場人物は下記の通りで，処理の順序も項目通りになっています．

- 認証サーバ(Sinatraアプリ): クライアントにJWT方式のアクセストークンが入ったクッキーを発行する．
- トークンDB(Redis): 認証サーバで発行したアクセストークンを保持する．
- 認証proxy(mod_mruby): クライアントのアクセス要求でクッキーのアクセストークンとトークンDBを比較し，リソースサーバへのアクセス認可を判断する
- リソースサーバ(任意): 認証をかけたい大事なデータがはいっているサーバ．

### mruby-jwt

  前述のとおり，認証proxyは署名されたJWTのデコードを行う必要があります．rubyには[progrium/ruby-jwt]というJWTを扱うための
gemがあったので，今回のためにこれをmrubyでも動くように修正を加えて[mruby版のmruby-jwt][prevs-io/mruby-jwt]を用意しました．

  mrbgemを用意するにあたっては[matsumoto-rさんの解説記事](http://blog.matsumoto-r.jp/?p=3923)がとても参考になりました．ここに
書かれているチュートリアルの通りにやればmrbgemが簡単にできます.中身は[progrium/ruby-jwt]のほぼコピペとなっていますが，
いつかパフォーマンスを求めたくなったときにc実装に移植したいと思っています...(fork me!)．

### mod_mrubyからmruby-jwtを利用する

　いつものやり方でmruby-jwtを入れてmod_mrubyをビルドしなおしたら，フックスクリプトからruby-jwtを使ってみましょう．

**httpd.conf**


    <IfModule mruby_module>
      mrubyTranslateNameMiddle "/data/hooks/hook.rb"
    </IfModule>


**hook.rb**


    # Redisへの接続設定
    host    = "0.0.0.0"
    port    = 6379
    redis   = Redis.new(host, port)

    upstream_addr = "localhost"
    upstream_port = "9000"

    # スーパーシークレットな署名鍵
    secret_key = 'this is the very secret key!'

    begin
      hin = Apache::Headers_in.new
      # CookieからJWTを取り出す
      jwt = hin["Cookie"].split("; ").select{|s| s=~/p_tkn=/}.first.split("=").last

      # JWTのデコード
      d = JWT.decode(jwt, secret_key).first
      # アクセストークン
      stored = JWT.decode(redis.get('token:' + d['uid']), secret_key).first['token']

      # JWTのpayloadに格納したtokenについてクッキーのものとDBのものを比較
      if d['token'] != stored
        raise Exception.new("Token verification failed, uid: #{d['uid']},token: #{d['token']}")
      else
        r = Apache::Request.new
        r.reverse_proxy "http://#{upstream_addr}:#{upstream_port}" + r.unparsed_uri
        Apache::return(Apache::OK)
      end
    rescue Exception => e
      # JWT符合に失敗したら認証エラー
      Apache.errlogger Apache::APLOG_NOTICE, "something error occured while checking authorization reason: #{e}"
      Apache::return(Apache::HTTP_PROXY_AUTHENTICATION_REQUIRED)
    end


## デモ
　
![demo-detail]

　今回作った[デモ][jwt-auth-proxy]を動かして，認証proxyの様子を見てみましょう．
全体構成を一式格納したDockerfileを用意したので，ビルドして動かしてみます．

**デモ用イメージのビルドとコンテナ起動**

    git clone https://github.com/prevs-io/jwt-auth-proxy.git
    docker build -t jwt-auth-proxy .
    docker run -t --rm -p 8080:8080 -p 80:80 jwt-auth-proxy

　起動出来たら，`http://localhost/`にアクセスしてみましょう．この状態では認証トークン
が発行されていないのでproxyが認証エラーとなり，リソースにアクセスできません．

    $ curl -vI http://0.0.0.0/
    ...
    HTTP/1.1 407 Proxy Authentication Required
    < Date: Fri, 12 Dec 2014 04:39:14 GMT
    Date: Fri, 12 Dec 2014 04:39:14 GMT
    ...
    * Closing connection 0

　それでは，`http://localhost:8080/auth`にアクセスして認証トークンを
クッキーに発行してもらいます．

    $ curl -v http://0.0.0.0:8080/auth
    ...
    < HTTP/1.1 200 OK
    < Date: Fri, 12 Dec 2014 04:49:49 GMT
    < Status: 200 OK
    < Connection: close
    < Content-Type: text/html;charset=utf-8
    < Set-Cookie: p_tkn=<tokenは省略>; path=/; expires=Mon, 15 Dec 2014 04:49:49 -0000

  レスポンスの通り，`Set-Cookie`で`p_tkn`にJWTが格納されているのがわかります．
今度は発行されたクッキーを使ってもう一度認証proxyにアクセスしてみましょう．次は
認証に成功するのでレスポンス200が返ることがわかります．

    $ curl -I --cookie p_tkn='<tokenは省略>; path=/; expires=Mon, 15 Dec 2014 04:42:37 -0000' http://0.0.0.0/
    HTTP/1.1 200 OK
    Date: Fri, 12 Dec 2014 04:45:49 GMT
    Status: 200 OK
    Content-Type: text/html;charset=utf-8
    Content-Length: 1873
    X-XSS-Protection: 1; mode=block
    X-Content-Type-Options: nosniff
    X-Frame-Options: SAMEORIGIN
    Connection: close
　

## proxyレイヤでのプロトタイピングがしやすいmod_mruby

　今回はmod_mrubyを使ってリソースへのアクセスコントロールを行うproxyが簡単に作れる
ことをご紹介できました．mod_mruby/ngx_mrubyを使うとリクエストハンドラをrubyの文法で
とても簡単にスクリプティングできます．ハンドラ書いていると結構面白いことができるなと
思っていて，この強みを活かして他にもいろいろなチャレンジをしてみたいと思っています．
例えば，

- ProxyレイヤでのAPIバージョニングやAPIオーケストレーションの実現
  -
  [The Future of API Design: The Orchestration Layer](http://thenextweb.com/dd/2013/12/17/future-api-design-orchestration-layer/) をやる．
  - mod_mruby/ngx_mruby上でリクエスト/レスポンスをDSLでスクリプティングできる
    フレームワークがあったら面白いかも．(既に誰かやってそうですが)

などなど...引き続き活用の道を探っていきたいなと思っています．
　そんなわけで本日は以上です！つづきまして，明日(12/13)は[matsumotory](http://qiita.com/matsumotory) さんによる「新しいmgemとmod_mruby ngx_mrubyとの組み合わせ」です．楽しみ！！

## 引用元

- [OAuth 2.0のAccess TokenへのJSON Web Token(JSON Web Signature)の適用][jwt]
- [jwt.io][jwt.io]

[jwt-auth-proxy]: https://github.com/prevs-io/jwt-auth-proxy
[progrium/ruby-jwt]: https://github.com/progrium/ruby-jwt
[prevs-io/mruby-jwt]: https://github.com/prevs-io/mruby-jwt
[demo-arch]: https://dl.dropboxusercontent.com/u/10177896/demo-detail.png 
[demo-detail]: https://dl.dropboxusercontent.com/u/10177896/demo-arch.png
[jwt]: http://d.hatena.ne.jp/ritou/20140927/1411811648
[jwt.io]: http://jwt.io/
[Understanding JWT]: https://developer.atlassian.com/static/connect/docs/concepts/understanding-jwt.html
[jwt-io-jpg]: https://dl.dropboxusercontent.com/u/10177896/jwt-io.png