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


hello JWT

こんにちは.mod_mruby ngx_mruby Advent Calendar 2014 12日目にお邪魔いたします!@ainoyaです.mod_mruby/ngx_mrubyとの出会いはカレンダー8日目の@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で署名や認証などを行うために決められた仕様のことです.
仕様では署名のための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を使った認証方式について学び,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を用意しました.

mrbgemを用意するにあたってはmatsumoto-rさんの解説記事がとても参考になりました.ここに
書かれているチュートリアルの通りにやれば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

 今回作ったデモを動かして,認証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-Cookiep_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 をやる.
    • mod_mruby/ngx_mruby上でリクエスト/レスポンスをDSLでスクリプティングできる フレームワークがあったら面白いかも.(既に誰かやってそうですが)

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

引用元