こんにちは.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方式でのエンコード/デコード(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を作る
全体構成
まずは今回作ってみた仕組みの全体像です.登場人物は下記の通りで,処理の順序も項目通りになっています.
- 認証サーバ(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
デモ
今回作ったデモを動かして,認証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 をやる.- mod_mruby/ngx_mruby上でリクエスト/レスポンスをDSLでスクリプティングできる
フレームワークがあったら面白いかも.(既に誰かやってそうですが)
- mod_mruby/ngx_mruby上でリクエスト/レスポンスをDSLでスクリプティングできる
などなど...引き続き活用の道を探っていきたいなと思っています.
そんなわけで本日は以上です!つづきまして,明日(12/13)はmatsumotory さんによる「新しいmgemとmod_mruby ngx_mrubyとの組み合わせ」です.楽しみ!!