ORY Hydra でマイクロサービスの認証認可を試す

こんにちは、白川です。

今回は、 ORY Hydra を使った OpenID Connect Provider の構築について紹介します。

マイクロサービスにおける認証・認可

モノリシックな Web アプリケーションでは、通常はそのアプリケーション自体がユーザ情報を保持し、ユーザの認証とセッションの生成を行ないます。
マイクロサービスを構成する各サービスが、モノリシックな Web アプリケーションと同じようにユーザ情報を保持すると、利用するユーザはサービス間を移動する際に別々にログインしないといけなくなります。
しかし、単純に各サービス間でユーザ情報やセッションをデータベースで共有する方法をとった場合、ユーザが認証されているかを知るために、すべてのサービスがデータベースにアクセスしないといけなくなってしまいますし、データベーススキーマ更新の際は全サービスに影響が出てしまいます。

そのため、マイクロサービスアーキテクチャでは各サービス共通で一度で認証し、各サービスで都度認証を行なわずにユーザーが認証済であることを確認できる疎結合な仕組みが必要となります。

このとき、従来の単純なセッションベースでの認証を採った場合、認証状態をセッションストアに都度問い合わせするような実装が多いです。
これを回避するためには、 OpenID Connect による JSON Web Token(JWT) 形式の ID トークンによる認証を採用するのが良さそうです。
OpenID Connect トークンベースの認証では、 OpenID Connect Provider によって JWT が生成され、その JWT はクライアントに返されます。
クライアントはリクエストごとに JWT をヘッダに含め、各サービスはリクエストヘッダの JWT の検証を行なうことで認証を行ないます。

ただし、 OpenID Connect の関連仕様は多い(参考リンク)ので、きちんとした OpenID Connect Provider を自前で実装するのはかなり大変そうです。
そこで、openid.netのLibraries, Products, and Toolsにあるライブラリを探したみたところ、 ORY Hydra を見つけました。

ORY Hydra の特徴

ORY Hydra は、 OpenID Foundation 公認の OpenID Connect Provider の Go 言語での実装です。

大きな特徴として、ORY Hydra 自体は独自の Identity Provider (IdP) を持たず、
ユーザ管理やログイン認証の部分に関しては、自前実装のものを API 呼出しする事で認証を行ないます。
これは既存のユーザ情報のデータストアを持っていて、そこに OpenID Connect のフローを組み込みたい場合に、非常に便利です。
以下 ORY Hydra の github にある README からの引用です。

ORY Hydra is not an identity provider (user sign up, user log in, password reset flow), but connects to your existing identity provider through a consent app.
(ORY Hydra は identity provider (ユーザーサインアップ、ログイン、パスワードリセット処理を行なう) ではありません。 しかし既存の identity provider に同意アプリケーションを通して接続します。)

本記事を執筆するにあたり、 ORY Hydra で OpenID Connect Provider を構築し、 kubernetes 上のアプリケーションで認証・認可するところまでを試してみました。
今回試したソースコードはこちらにアップしていますので、構築手順の詳細はそちらをご確認いただければと思います。
ログイン認証の実装に関しては、今回は公式に用意されたサンプルを利用しました。

ORY Hydra での OpenID Connect のフロー

公式ドキュメントから図を引用します。
認可エンドポイントへのアクセス → ログイン画面 → 同意画面表示 → トークン発行 というフローとなります。
図の中の Login ProviderConsent Provider がこちらで実装が必要なものとなります。

※引用: 公式ドキュメント

OpenID Connect Relying Party の登録

まず、OpenID Connect のクライアントである Relying Party を登録する必要があります。(上記シーケンス図の OAuth2 Client に当たる部分です。)
今回はコマンドラインで登録します。
API も用意されていますが、これらは管理用 API を保護する手段は ORY Hydra では用意していないので、本番運用では外部からコールされないように別途保護する必要があります。

Authorization Code Flow の開始

今回は RFC 6749 (The OAuth 2.0 Authorization Framework) で定義されている認可フローのうち, Authorization Code Flow (認可コードフロー) を試します。
Relying Party が ORY Hydra の認可エンドポイントにアクセスすることで、 Authorization Code Flow を開始します。
本来は Relying Party の実装も必要ですが、今回は Hydra が用意している ヘルパーコマンドラインを利用することで代用します。

上記を実行すると、以下のように表示されて 認証完了のコールバックを受け取る Web サーバが立ち上がります。

http://127.0.0.1:4446 にアクセスすると、以下のような画面が表示されます。


Relying Party Example


Authorize Application のリンク先は以下のようになっています。 oauth2/auth は ORY Hydra の認可エンドポイントです。

http://hydra-public-api.synergy-example.com:30080/oauth2/auth?audience=&client_id=test-client&max_age=0&nonce=clhxrbtyijycgfonbxmxflhl&prompt=&redirect_uri=http%3A%2F%2Flocalhost%3A4446%2Fcallback&response_type=code&scope=openid+offline&state=mxamrvdrwnucqjfytkeqzbkw

上記リンクを押下する前に、 シーケンス図における Login Provider と Consent Provider を起動させます。
今回は、公式に用意されている Node.js 製のサンプル実装を利用します。

Authorize Application リンクを押下すると、 ORY Hydra の認可エンドポイントに遷移します。
ORY Hydra は Cookie をチェックしてユーザの認証状態を判断して、 Login Provider にリダイレクトします。リダイレクト先のエンドポイントは通常 https://login-provider/login で表されます。この時、ORY Hydra から LoginProvider へのリダイレクトパスのクエリパラメータにlogin_challenge が付与されます。

ログイン

Login Provider はログイン画面を表示します。


login


サンプルではIDが "foo@bar.com"、パスワードが "foobar" 固定となっています。
ログイン画面にある "remember me" はチェックすることで、ログインリクエスト受理の際、 ORY Hydra に対して認証状態を保持するように依頼します。
これは再度認可エンドポイントにユーザがアクセスした場合に、再度認証をさせないためのものとして働きます。

正しいID/パスワードが入力された場合、Login Provider は login_challenge をURLパスに含めて ORY Hydra のログインリクエスト受理エンドポイント にリクエストを送ります。

レスポンスには、ユーザが次にリダイレクトされるべきURLを含む redirect_toキーが含まれています。
redirect_toキー には以下のようなURLが指定されています。

http://hydra-public-api.synergy-example.com:30080/oauth2/auth?audience=&client_id=test-client&login_verifier=524e532413924f33a479738e9ebd756d&max_age=0&nonce=clhxrbtyijycgfonbxmxflhl&prompt=&redirect_uri=http%3A%2F%2Flocalhost%3A4446%2Fcallback&response_type=code&scope=openid+offline&state=mxamrvdrwnucqjfytkeqzbkw

この URL は ORY Hydra の認可エンドポイントですが、login_verifier というパラメータが追加されていることが分かります。
リダイレクトされた ORY Hydra が、 login_verifier を見てユーザが認証されているので次の同意フローに進んでよい、と判断します。

同意

次の同意フローに進んでよい、と判断した ORY Hydra は Consent Provider にリダイレクトします。
リダイレクト先のエンドポイントは通常 https://consent-provider/consent で表されます。この時クエリパラメータに consent_challenge が付与されてきます。

Consent Provider は 同意画面を表示します。
ユーザが Relying Party に以前に権限を付与したことがない場合に、ユーザにその要求を確認させるために画面を表示します。


consent


サンプルでは、openidoffline という2つのスコープに対する要求を確認しています。
openid は ID トークンを発行するか、offlineリフレッシュトークンを発行するか、という点に関わってきます。

Do not ask me again は、同意した状態を ORY Hydra が保持しておくかどうかに関わります。
チェックした場合は、再度ログインした後に同意画面をスキップすることができます。

仕組みとしては、Consent Provider にリダイレクト後、 Consent Provider は consent_challenge を元に同意リクエストを ORY Hydra に確認する API をコールしますが、
以前に同意した場合は、この時のレスポンスの中に含まれる skip キーがtrue で返ってくるようになります。
Consent Provider は skip キーを元に同意画面を出さずに、次の同意リクエスト受理に進むことができます。

ユーザの同意が得られると、
Consent Provider は consent_challenge をURLパスに含めて ORY Hydra の同意リクエスト受理のエンドポイント にリクエストを送ります。

この同意リクエスト受理のリクエスト送信時に、session キーに ID トークンに追加する任意の claim を指定することができます。
claim はOpenID Connect で定義された ID トークンに含まれるユーザー情報属性群です。
以下は、サンプルの Consent Provider の実装です。

レスポンスには、ユーザが次にリダイレクトされるべきURLを含む redirect_toキーが含まれています。
この URL は ORY Hydra の認可エンドポイントですが、consent_verifier というパラメータが追加されていることが分かります。

http://hydra-public-api.synergy-example.com:30080/oauth2/auth?audience=&client_id=test-client&consent_verifier=8c33536a584d4443aa25ac226167785c&max_age=0&nonce=clhxrbtyijycgfonbxmxflhl&prompt=&redirect_uri=http%3A%2F%2Flocalhost%3A4446%2Fcallback&response_type=code&scope=openid+offline&state=mxamrvdrwnucqjfytkeqzbkw

トークン発行

認可エンドポイントに consent_verifier パラメータ付きでリダイレクトされると、 Authorization Code (認可コード)を発行して、 Relying Party のコールバック URL にリダイレクトされます。
以下はサンプルにおけるコールバックURLです。

http://localhost:4446/callback?code=zRgk-QWsIOUKHsFTU1PREaY6WldH7rvjrtaa39yRQxM.9PcRUjQz437I7sT_2CoFsRnONjx2onZCx-8LKI36M98&scope=openid%20offline&state=ldhfjtraoocpxiyenhkvchxr

Authorization Code を受け取った Relying Party は state の検証を行なったのち、ORY Hydra のトークンエンドポイント に Authorization Code を Post して、 発行された ID トークン等を取得することができます。
( state はどのリクエストに対してどのレスポンスが帰ってきたか正しく対応づけされていることを保証することで CSRF 攻撃を防御するためのランダムな値です。)

サンプルでは以下のように発行されたアクセストークン、リフレッシュトークン、ID トークンが、画面に表示されました。


token


ID トークンを試しに jwt.io でデコードしてみると、ペイロード部分は以下のような感じになっていました。 ID トークンに追加する任意の claim として groups キーも確認することができます。

ログアウト

ORY Hydra のログアウトのエンドポイント に GET でアクセスすることでログアウトできます。

ただし、ログアウトしても以前発行したアクセストークン等々は有効なままなので、トークンを取り消すエンドポイント の API もコールする必要があります。
トークンの無効化は、アクセストークンとリフレッシュトークンのみ有効です。 ID トークンの無効化はできず、 ID トークンの expキーが持つ有効期限までは有効となるため、上記とログアウトと連動しないという点は注意が必要です。

トークンを用いた kubernetes 上のアプリケーションでの認証・認可

kube-apiserver の認証・認可

ここからは 今回のサンプルで利用したコンテナオーケストレーションである kubernetes での OpenID Connect への対応について説明します。

kubernetes(kube-apiserver) の認証は OpenID Connect に対応しており、 RBAC(Role Based Access Control) を設定することでグループによるアクセス制御が可能です。

OpenID Connect による認証・認可に対応するために、 kube-apiserver 起動時にいくつかのパラメータを指定して起動する必要があります。

パラメータ名 指定する内容
--oidc-issuer-url IDトークンのiss ※ https必須
--oidc-client-id Relying Partyのclient_id
--oidc-groups-claim RBACのGroupとして扱うclaimのキー
--oidc-ca-file IdPのサーバ証明書に署名したCA証明書

Docker for Mac の場合、 kube-apiserver は Moby VM 上でコンテナとして動作しているので、
こちらの方法で、 Docker for Mac の tty に接続して、
/etc/kubernetes/manifests/kube-apiserver.yaml を下記のように編集した後に kubernetes を再起動します。

次に、先ほど Hydra から発行された ID トークンを kubernetes に登録します。

次に、 RoleRoleBinding を kubernetes に 登録します。登録内容としては namespace が kube-system の pod に対する get/watch/list の操作を許可する内容となります。

実際に試してみると、namespace 未指定の場合は pod の情報取得ができませんでしたが、 kube-system を指定した場合は pod の情報取得ができるようになりました。

istio による認証・認可

istio はマイクロサービスのサービスメッシュを実現するオープンソースプロジェクトです。
kubernetes の pod 内に Envoy というプロキシをサイドカーとして動かすことで、アプリケーションのコードに手を入れずに、マイクロサービス間の通信に関する課題(リトライ、サーキットブレーカー、負荷分散、分散トレーシング)を解決するための仕組みを提供します。

先ほど見た kube-apiserver の例はあくまで kube-apiserver に対する操作の認証・認可ですが、
istio によって、OpenID Connect による認証・認可を各マイクロサービスのアプリケーションや HTTP メソッド単位で実施することも可能になります。

サンプル では、 httpbin のサービスを立てて、認証・認可のポリシーを設定しています。

認証に関しては、Policy を kubernetes リソースとして登録します。
targets に対象のサービス、origins に OpenID Connect の issuer と JWKsエンドポイントを指定します。

認可については、RbacConfigServiceRoleServiceRoleBinding を kubernetes リソースとして登録します。
以下のサンプルでは、JWT にペイロードに指定された groups クレームが foo のユーザに対して、namespace が foo の全サービス、全メソッドを許可しています。

では、実際にアクセスしてみて試してみます。
Bearer トークンで ID トークン指定なしだと API コールできず、正しい ID トークンを指定した場合は API コールが通ることが確認できました。

おわりに

実運用に入る前には、今回のサンプル実装にくわえて API の保護であったり、 ID トークンの署名に使用する鍵のローテーションの考慮のなどを行なう必要があります。
とはいえ、 ORY Hydra を利用することで、独自のログイン認証を行ないつつ、 OpenID Connect に対応して、 kubernetes や istio での認証・認可に対応できることが分かりました。
実装コストを抑えながら、既存のユーザストアや認証処理を OpenID Connect に対応させる場合などに、非常に魅力的な選択であることが分かりました。

最後に参考文献を載せておきます。

Comments are closed, but you can leave a trackback: Trackback URL.