auth0とALBを連携させてOpenID Connectを理解したい
はじめに
こんにちは。コンサルティング部の津谷です。
今回のテーマはOpenID Connect(以下OIDCと略称します)です。前職では、外部のIdPからAWSサービスへのアクセス権限を委任する設定等をやりましたが、解像度はあまり高くなかったです。今回はWebアクセスをする際にApplication Load BalancerでOIDC認証を行い、アクセスを制御してみて解像度上げてみようと思います。
そもそもOIDCとは
「OpenID Connect または OIDC は、OAuth 2.0の認証・認可メカニズムを採用したアイデンティティプロトコル」を指しています。「OIDCとは」の前に「認証・認可」の概念が根本にあることを理解しておきます。
認証とは「本人確認」を行うといえばすっきりしますね。パスワード認証や、PIN認証、最近だと生体認証(指紋や顔など)でいろいろなサービスにログインしたりすると思います。
認可はある特定のサービス・システムにアクセスしたい場合の権限制御を行う仕組みです。AWSでいうとIAMでロールを付与してAWSリソースへの権限を制御したりしますよね。AWSだけでなく、例えばWindowsのファイルシステムにアクセスできるユーザを絞ったりとか、会社ごとに持っているシステムへの役割別の操作権限を与えたりとかも認可の仕組みです。
auth0で利用する通信規格Oauth2.0は認可を行うものになります。このOauth2.0の規格を認証もできるように拡張したものがOpenID Connectという新しい規格になります。クライアント(アプリケーションやユーザ)が何かのリソースにアクセスしたい場合に、一時的なアクセストークンを払い出してくれます。それと同時に、システムにアクセスしているクライアントが本人確認された情報一式をもっていることを証明してくれるのがIDトークンと呼ばれるものです。これらのトークンを両方払い出してくれるのがauth0のような認証認可を兼任できるサービスになります。
AWSサービスだとCognitoでも実装が可能です。もちろん、IdP(認証管理)はActive Directoryなどを使って、Authorizationサーバ(認可管理)はauth0でやるとかも可能です。
「OpenID Connectとは」を一から解説してくれているブログもあるので、とても参考にしました。ありがとうございます。下記を参考にしつつ、AWSのWeb構成でどういう風に利用するのか1つ1つあてはめながら確認していきます。
また、今回の検証をするにあたり下記の構築ブログも非常に参考にしました。ありがとうございます。
構成
上記を踏まえて、今回の構成に当てはめてみました。
ブラウザから、ALB⇔Lambdaにアクセスする場合を考えております。Lambdaはただ、ログイン成功のHTMLを返すだけの静的ページを実装します。普通にすればインターネット上の誰でもアクセスできるような構成なのですが、ALBでOIDC認証を挟むことでauth0で管理しているユーザでないとアクセスできないように制限をすることが可能です。
1度認証すると、クライアント側が保持するCookies情報を送付するので、以降はTTLに基づいて認証なしでアクセス可能です。
今回はクライアント(アプリケーション)がALBになります。このクライアントから、特定のリソース(Lambda)にアクセスしたいのでauth0で認証・認可を行う必要があります。ユーザがALBにアクセスすると、認証認可済みでないアクセスはauth0へのログイン画面(認可用のエンドポイント)にリダイレクトされるようになっております。
認証情報を入力すると、auth0が情報をIDプールと照合し、問題なければALBに認可コード(リダイレクトURLにcodeパラメータを付与している)を付与して、リダイレクトします。
ALBは認可コードをもらうと、それをauth0のトークンエンドポイントに渡します。
これを提示することでauth0はアクセストークンと一緒にIDトークンをJWT形式で発行してくれます。
ALBは受領したアクセストークンをユーザ情報エンドポイントに送信します。
実際にアクセス要求側のユーザ情報(ユーザ名やEmail情報等)を付与してALBに返します。
ユーザ要求の情報をALBがLambdaにHTTPリクエストにヘッダー(x-amazn-oidc-~)付与した形で転送します。
認証が済んだので、リクエストをそのままLambdaに転送します。問題なければ、ブラウザでアクセス成功の文字列が出力されるはずです。
実装してみた
⓪何よりもまずauth0のライセンスを取得する必要があります。
auth0は22日間の無料トライアルが実施できますので、開始してみましょう。
自分はgithubアカウントとの連携でテナントを作成しました。
①auto0での事前設定を行います。
まずは、ユーザを作成します。
ALBからauth0への認証に使う情報になります。
[ユーザー管理]-[ユーザ]から追加します。
認証方法は「ユーザ名-パスワード」にします。
メールはお使いのもので問題ございません。パスワードも設定しておきます。
次にアプリケーションを作成しましょう。こちらは、auth0に認証認可を行うクライアントであり、ALBのことを指しています。後続で、生成されたアプリケーションのクライアントIDやシークレット情報をALBで設定しますので事前に作成を済ませておきましょう。
②AWS側でネットワーク周りの設定を済ませておきます。
- VPC
サービス名 | リソース名 | CIDR | 備考 |
---|---|---|---|
VPC | OIDC-test-vpc | 10.0.0.0/24 |
- サブネット
リソース名 | CIDR | 備考 |
---|---|---|
OIDC-test-alb-subnet-1a | 10.0.0.0/27 | ALB用のパブリックサブネット |
OIDC-test-alb-subnet-1c | 10.0.0.64/27 | ALB用の空サブネット |
OIDC-test-lambda-subnet | 10.0.0.32/28 | VPC Lambda用のサブネット |
- ルートテーブル
リソース名 | 送信先 | ターゲット | サブネット関連付け |
---|---|---|---|
OIDC-test-alb-rt | 10.0.0.0/24 | local | OIDC-test-alb-subnet-1a、OIDC-test-alb-subnet-1c |
OIDC-test-alb-rt | 0.0.0.0/0 | IGW | 同上 |
OIDC-test-lambda-rt | 10.0.0.0/24 | local | OIDC-test-lambda-subnet |
- セキュリティグループ
■ALB(OIDC-test-alb-sg)
インバウンド
タイプ | プロトコル | ポート範囲 | ソース | 備考 |
---|---|---|---|---|
HTTPS | TCP | 443 | 0.0.0.0/0 | インターネットからのアクセス |
アウトバウンド
タイプ | プロトコル | ポート範囲 | ソース | 備考 |
---|---|---|---|---|
HTTP | TCP | 80 | OIDC-test-lambda-sg | バックエンド(Lambda)へのアクセス |
HTTPS | TCP | 443 | 0.0.0.0/0 | インターネットへのアクセス |
Lambda(OIDC-test-lambda-sg)側は特に設定していません。
デフォルトのまま、アウトバウンドはインターネット向けに0.0.0.0/0が空いています。
③Lambdaを作成していきます。
関数名は「OIDC-test-lambda」としました。ランタイムは最新の「Python 3.12」にしています。
実行ロールもデフォルトで付与されるものを利用すれば問題ないです。
VPCは「OIDC-test-vpc」を指定しました。VPCLambdaなので、ENIを「OIDC-test-lambda-subnet」に配置します。セキュリティグループは「OIDC-test-lambda-sg」を指定します。
作成後、コードを貼り付けします。
[コード]タブから、記載します。Webアクセスできることだけ確かめればよいので簡単な応答メッセージのみです。
def lambda_handler(event, context):
return {
'statusCode': 200,
'statusDescription': "200 OK",
'isBase64Encoded': False,
'headers': {
'Content-Type': 'text/html; charset=utf-8'
},
'body': '<h1>ログイン成功!</h1><p>認証されました。</p>'
}
「deploy」を押下し、反映しましょう。
④ALBを作成していきます。
ターゲットグループから行きましょう。
ターゲットタイプは「Lambda関数」を指定しましょう。
ターゲットグループ名は「OIDC-test-targetgroup」にしています。
Lambda関数「OIDC-test-lambda」を指定します。バージョンは最新のもので問題ありません。
本体作っていきます。
名前は「OIDC-test-alb」にしました。スキームは「インターネット向け」にしましょう。
VPCは「OIDC-test-vpc」、サブネットは「OIDC-test-alb-subnet-1a」「OIDC-test-alb-subnet-1c」を指定しています。ALBはマルチAZで構成するので、空サブネットを指定しておきました。
リスナーの設定をしましょう。OIDC認証の設定はリスナーで行うのですが、一旦ポート80でターゲットグループに転送するルールを入れておきます。
⑥HTTPSリスナーを作成するための自己証明書(オレオレ証明書)を作成しておきます。
auth0との認証時の通信プロトコルは全てHTTPSになるため、必要になります。
ACMなどで証明書を発行してもよいのですが、あくまでOIDCの認証が目的なので、ホストゾーンでのドメイン管理までする必要はありません。(本番利用の際はACMでパブリック証明書を発行するか、認証局で発行した正規の証明書をインポートするようにしましょう)
ローカルで発行するので、下記で証明書用のディレクトリを作っておきます。今回Ubuntuを使っていますがopensslコマンドが利用できるOSであればなんでもよいかと思います。
mkdir certification
ディレクトリに移動して、秘密鍵を作成します。
openssl genrsa -out ./server.key 2048
自己署名用のCSRを作成しておきます。
openssl req -new -key ./server.key -out ./server.csr
ここの所有者設定は、任意のもので問題ないので省略します。(一連の流れは後ほど添付します)CN(Common Name)には、ALBのデフォルトDNS名を設定しておきました。(自己署名なのでここも任意で問題ないです)
以下のコマンドで、証明書を発行しましょう。
openssl x509 -in ./server.csr -days 365 -req -signkey ./server.key -out ./server.crt
ディレクトリ配下に証明書と秘密鍵があるので、catコマンドで中身を控えておきます。
証明書:server.crt
秘密鍵:server.key
ACMから証明書をインポートしましょう。
sever.crtの内容を「証明書本文」に貼り付けます。
server.keyの内容を「証明書プライベートキー」に貼り付けます。
証明書チェーンは必要ないので空欄のままで問題ないです。「証明書をインポート」を押下して作成しましょう。
⑦リスナーの設定に行く前に、auth0で設定したクライアント情報を抑えておきましょう。
「OIDC-test]-[設定]タブから基本情報を抑えておきます。
- ドメイン
- クライアントID
- クライアントシークレット
【補足】
ドメインはALBがauth0にリダイレクトするURLになります。
クライアントIDはアプリケーションを識別する公開情報で、クライアントシークレットはアプリケーションの正当性を担保するパスワードのようなものになります。
⑧ALBのリスナールールの設定を行います。
先ほどインポートした証明書を指定します。
リスナーのポートは443(HTTPS)を指定します。OIDC認証にチェックを入れ、以下必要な情報を入力します。
デフォルトアクション | 項目 |
---|---|
アイデンティティプロバイダー | OIDC(OpenID Connect) |
発行者 | auth0で確認した[ドメイン名] |
認証エンドポイント | [ドメイン名]/authorize |
トークンエンドポイント | [ドメイン名]/oauth/token |
ユーザ情報エンドポイント | [ドメイン名]/userinfo |
クライアントID | auth0で確認したクライアントID |
クライアントのシークレット | auth0で確認したクライアントシークレット |
これらで取得した、トークン情報はセッションCookieに保存されます。
タイムアウトは5分で設定しました。そのほかはデフォルト設定で問題ありません。
スコープ設定は任意です。ただし、ユーザ情報を詳細に取得したい場合などは「profile」を指定したり、今回は短期検証なのでrefresh tokenの取得は行いませんが、トークン更新をする場合は「offline_access」を指定したりする必要があります。デフォルトでopenidは指定されているのでIDトークンの取得は可能です。
⑨構成フローでも説明した通り、ALBはauth0への認証の際にリダイレクトを行いますのでauth0側で認証認可の検証をした後、コールバックするALBのドメインを指定する必要があります。
[OIDC-test]-[設定]タブでアプリケーションのURIから「許可するCallbackURL」を指定しましょう。
「https://(ALBのドメイン)/oauth2/idresponse」と入力します。
Idpアプリでリダイレクトするドメインが公式ドキュメントで指定されています。
疎通確認
実際にALBのDNS名でアクセスしてみますと、
auth0の認証画面にリダイレクトされました。設定済みのユーザ情報(ユーザIDとパスワード)を入力していきます。
入力後、ALBのDNSにCallbackされ、ブラウザにログイン成功の文字が出力されました。
疎通の中身
ここまでで、いったん検証は完了なのですが裏でどういう通信が走っているのか確認しておきます。
ブラウザのディベロッパーツールで認証時の通信を見ると、
callbackURLでALBにリダイレクトが成功しています。
パラメータに付与している「code」はLambdaにアクセスするためにALBに認可コードを送付しています。これを引き受けるとクライアントはアクセストークンを取得することができます。アクセストークンはブラウザ側で確認することはできません。
正常にALBのHTTPヘッダーにアクセストークン、IDトークンの情報が渡されているか確認したいので、ブラウザには表示させずCloudWatchに出力させてみました。
x-amzn-oidc-accesstokenはアクセストークンを表しております。こちらはプレーンテキストで渡されているようです。
x-amzn-oidc-identityはIDP側での認証ユーザの識別子を指しています。logsで確認すると、auth0で登録したユーザの「USER ID」と一致しています。こちらもプレーンテキストで渡されています。
x-amzn-oidc-dataはJWT形式で渡されます。確かにユーザ情報なのですが正式にはIDトークンそのものではないようです。公式にも記載がありました。IDトークンがユーザ情報を表す箱とするとクレームは実際の中身とイメージすると腑に落ちました。
下記はCognitoで認証をしていますが、x-amzn-oidc-dataの情報をデコードまでしてくれていますので是非参照ください。
最後に
今回はALBでOIDC認証を試してみました。認証認可の仕組みを知ること、そのうえでOIDC規格がどういうやり取りをしているのかをつかんだうえで、実践できるとより解像度が上がりますね。ALBは直接OIDC認証ができますが、Cloud Frontなどのエッジサービスを噛ませる場合などは直接の連携ができないので、Lambda@edgeなどを使って認証認可のフローを作りこんだりする必要があります。AWSサービスの特性を理解したうえで、認証設計できるようになりたいと思いました。