(はてなダイアリー版: http://d.hatena.ne.jp/mjt/20180923/p1 )
★ 注: Lasso自体はあんまりプロダクションに向いていない印象を受けた。全く同じことはoauth2_proxyでも可能で、oauth2_proxyのforkもいくつかあるので、そちらを検討する方が良い気がしている。
基本的にはサイト側でOpenID Connectのサポートをするのが望ましいが、nginxにはリクエストヘッダだけを一旦別のHTTPサーバに送信し、その結果によってアクセス可否を決める auth_request
モジュールがあるため、HTTP認証サーバと適当なブラウザベースの認証機構を用意すれば任意のサイトを任意の認証フレームワークで保護することができる。
従来は oauth2_proxy というbit.ly製の認証プロクシがこの手のユースケースには(おもにk8s界隈で e.g. https://qiita.com/nirasan/items/7d5d9321fcf3e5c6c82a )よく使われていたが、oauth2_proxyはbit.ly側でのメンテナ不在により今月末でのプロジェクト終了が宣言( https://github.com/bitly/oauth2_proxy/issues/628#issuecomment-417121636 )されている。
認証プロクシは、バックエンドHTTPサーバへのリクエストも行う。それに対して、 auth_request
による認証の移譲ではバックエンドサーバへのリクエストはnginxが行うことになる。このため、auth_request
で使う認証サーバはnginxの十分近くに配置する必要がある -- 認証プロキシに比べて、TCPなりなんなりの接続コストがそのままリクエスト遅延に上乗せされる。もっとも、認証プロキシでは事実上フロントエンド側のリバースプロキシによって2重のコンテンツバッファリングが発生してしまうので、どちらも一長一短な気はする。
- https://github.com/LassoProject/Lasso
- LassoのGitHubページ
- http://nginx.org/en/docs/http/ngx_http_auth_request_module.html
- nginxの
auth_request
モジュールのドキュメント - https://developer.okta.com/blog/2018/08/28/nginx-auth-request
- Oktaのブログ: Use nginx to Add Authentication to Any Application
今回の方法はメジャーなSSOベンダである Okta のブログに有るが、OktaでなくAuth0( https://auth0.com/ )を使ってみた。どちらも開発者向けには無料アカウントが有り、1000人までのユーザベースなら無料となっている。
今回は、stripe.local/auth/*
をLassoのための認証に使用し、stripe.local/testing/*
というWebリソースの保護を考える。
nginxの設定
nginxの設定はほぼOktaのブログに掲載されている通りだが、リライトを使ってrootを移動している。
http {
server {
listen 443 ssl;
server_name stripe.local;
ssl_protocols TLSv1.2;
ssl_certificate f:/serv/keys/out.cer;
ssl_certificate_key f:/serv/keys/key.pem;
location /testing/ { ★ ここが保護したいリソース
auth_request /lasso-validate;
error_page 401 = @error401;
root f:/serv;
}
location @error401 { ★ Lassoから401エラーが帰ってきた場合のリダイレクト先
return 302 https://stripe.local/auth/login?url=https://$http_host$request_uri&lasso-failcount=$auth_resp_failcount&X-Lasso-Token=$auth_resp_jwt&error=$auth_resp_err;
}
location /auth { ★ ブラウザからLassoにアクセスできるようにする
rewrite /auth/(.*) /$1 break;
proxy_set_header Host login.stripe.local;
proxy_set_header X-Forwarded-Proto https;
proxy_pass http://127.0.0.1:9090;
}
location /lasso-validate { ★ 認証の移譲先
proxy_pass http://127.0.0.1:9090/validate;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
auth_request_set $auth_resp_jwt $upstream_http_x_lasso_jwt; ★ リダイレクト先指定で参照できる変数のセット
auth_request_set $auth_resp_err $upstream_http_x_lasso_err;
auth_request_set $auth_resp_failcount $upstream_http_x_lasso_failcount;
}
nginx は 常に ユーザのリクエストヘッダをLassoに転送し、LassoはCookieの内容をチェックして認証済かどうかを確認する。認証済のCookieを持っていなかったら、Lassoは401
を返してnginxに上で宣言したような @error401
を実行させ、結果的にユーザは302
でLassoの /login
に転送される。正当なCookieを持っていたら、Lassoは200
を返し、nginxはそのままコンテンツの提供を継続する。
Lassoは保護されたリソースへのリクエストを全て検証することになるので、パフォーマンスに対する要求はそれなりに高いことになる。
ここでは認証したユーザ名の処理は特に実装していない。つまり OpenID Connect認証に成功したユーザは無条件でリソースへのアクセスが許可される 。 Oktaのブログのセクション Bonus: Who logged in? やLassoのREADME.md
に、HTTPヘッダを経由してユーザ名を渡す方法が有る。
Auth0の設定
Auth0では適当にテナントを作成した上で Regular Web Application を作成し、Allowed callback urlsをLassoの /auth
を指すように設定する。今回の場合は、 https://stripe.local/auth/auth
になる(本来は /auth
だけだがrootを移動しているため)。
LassoにはOpenID Connectの自動設定(discovery)機能は無いので、LassoのYAMLには諸々の設定を手動で実施する必要がある。これらのデータは Advanced settings >> Endpoints 以下にある。
実際のユーザについては、Auth0の無料アカウントでは2種類までのソーシャルログインを有効化することができる。ソーシャルログインを使うと、ユーザの持っているGoogleとかTwitterのアカウント認証で認証を代替できる。ただし、複数のソーシャルアカウントを1つのAuth0上のユーザに結びつける機能は無料アカウントでは提供されない。(non-profitなFOSSについてはJIRA/Confluenceのように有料版をコストなしで提供している。)
Lassoの設定
Lassoの設定は、Lassoのexecutableがあるディレクトリ上の config/config.yml で行う。
lasso:
logLevel: debug # ★ 機密情報もガンガン出るので注意
listen: 127.0.0.1
port: 9090
# !!!! FIXME !!!! allowAllUsers for now.
# Lassoは元々Googleアカウント等への統合を想定しているため、ユーザ名中のドメインを追加で検証できるようになっている
allowAllUsers: true
publicAccess: false
jwt:
issuer: Lasso
maxAge: 240 # 240分 = 4時間は再認証しない
secret: lZlMPLfBjcIQCjbxjEoKep5dWn6xDjyW # これは乱数で良い
compress: true
cookie:
name: Lasso-cookie
secure: true
domain: stripe.local
httpOnly: true
session:
name: lasso-session
headers:
jwt: X-Lasso-Token
querystring: access_token
redirect: X-Lasso-Requested-URI
db:
file: data/lasso_bolt.db # data/ ディレクトリを事前に用意しておくこと
oauth:
provider: oidc
# ★★ 以下のclient_idとclient_secretは本来開示してはいけない。
client_id: TfMpW26Hah9HI5sQw0WpVoCIhCS2V5qN
client_secret: Q9eBFIKMRkhSGjjfrSxDITHDNgV2UwnMO4KqKR5jTBCqNbSNQBfRC50fZ5ozX1fc
auth_url: https://yuni.auth0.com/authorize
token_url: https://yuni.auth0.com/oauth/token
user_info_url: https://yuni.auth0.com/userinfo
scopes:
- openid
- email
callback_url: https://stripe.local/auth/auth # Auth0に設定したCallback urlに一致すること
OpenID Connect OPのなかには、callback_url
としてローカル端末を許可していないことがある。Auth0の場合は問題なく設定できるが、プロバイダ側で許可されていない場合は/etc/hosts
を書くとか何らかの方法でワークアラウンドが必要になる。
YAMLにはいくつかの機密情報を設定する。client_id
とclient_secret
は認証を提供するOpenID OP(= Auth0)がRP(= ここで設定したLasso)の真正性を確認するのに必要となる(プロトコル上機密情報なのはclient_secret
の方)。jwtのsecretはLassoが発行するJWTの認証鍵として使われる(これをランダムにしないと、別のLassoインスタンスが同様にJWTを発行できてしまう)。
この手法はOpenID Connectをユニバーサル認証手段にするか?
というわけで、auth_request
を使うと任意のブラウザ認証手法をWebサイトに統合できることがわかった。もっとも、これがどの程度役に立つかは何とも言えない。
明確に役に立つのは、本来認証の無いWebサイト -- 例えば内部で作っているダッシュボードとか -- にそのまま認証を付加できるポイントだろう。ただ、他の認証手段(BASIC認証やアプリケーション組込の認証)に比べると:
- Deployが大変 。通常の ID/パスワード認証であればBasic認証で良いし、LDAPを認証基盤としていて、かつOktaのようなOpenID Connectプロバイダを採用していないなら(あるいは自由にアプリケーションを追加できる環境でないなら)dex https://github.com/dexidp/dex とかKeyclockのようなOpenID Connect実装を別途用意しておく必要がある。
- アクセス権の扱いが微妙 。グループ等のclaimを伝播させる良い方法が無い。有料版のnginxにはJWTのパース処理 http://nginx.org/en/docs/http/ngx_http_auth_jwt_module.html が存在するのでnginx内での処理も原理的には可能だが。。
- ユーザサービスを提供する余地が無い 。そもそもアプリケーション側で明示的に対応しないと "ログアウト" みたいなリンクを置く場所が無い。
-
複数サービスの統合が難しそう 。今回のセットアップでは、Lassoは
/
に認証クッキーを設定する。このため複数の異なる認証機構を単一ドメインでサービスするためには追加の考察が必要になる。
個人的には、HTTP経由のWindows統合認証が上手く動いた試しが無いので、ID/パスワードを前提とした実装よりは、OpenID Connectベースの認証に寄せた方がSSOや2FAの実装の上では有利な気はしている。...でも自作アプリの割合が多い現状を考えるとアプリ側に認証を用意した方がUI的には便利というか何というか。。