Azure Functions に上げた Quarkus の Rest API を AD B2C でセキュアにしたい
最近のお気に入り Quarkus ですが、今回は Azure Functions に上げてみたいと思いまして。コンテナデプロイできるのもいいけど、やっぱりサーバレスだよね?ってことで挑戦してみましたが、かなりあっさり出来てしまいました。
これじゃ記事にならないじゃん。。。ということで、Keycloak でも簡単に SSO できたし、"AD B2C"ってので OpenID Connect がさっくりできるらしいからやってみようか、と挑戦してみましたが。。。
…想像以上の地獄でしたw
本記事の概要
今回は以下の流れでチャレンジを行います。
- Quarkus のサンプルをAzure Functionsにデプロイして動作確認
- AD B2C リソースの作成と設定
- Postmanを使用してAD B2CからOIDCのトークンを取得
- 取得したトークンを使ってFunctionsにデプロイしたQuarkusにアクセス
ちょっとSSO してみようという試みでしたが意外と壮大な計画になってしまいました。。。
対象環境
- Java: OpenJDK 8.x
- Maven: 3.6.2
- surefire-plugin: 2.22.2
- Quarkus: 1.0.1.Final
- Azure CLI: 2.0.77
- Postman: 7.13.0
今回は AD B2C など Azure の様々な要素が色濃く現れてまいります。Azure の全体的なサービスアーキテクチャについては以下のわかりやすい記事がありますので、これを眺めながらの方が理解できるかと思います。
ディレクトリとかテナントとかリソースグループとかサブスクリプションとか、わかりにくさ抜群のAzureですが、B2B2Cとしては本物です。(というかちゃんとマルチテナントに対応できているのはAzureぐらい?)
頑張って、ついていきましょう。。。
そして Quarkus のプロジェクトを Azure Fuctions にデプロイする際に az
コマンドを使用しますので、"Azure CLI" が必要です。
以下のページを参考に Azure CLI をインストールしてください。
Quarkus は 1.0.1.Final です。遂に、正式版ですね。。。初めて触ってからここ数ヶ月のお付き合いですが、感慨深いです。
続いてOIDCトークンの確認に、"Postman" を使用します。もしなければ以下のサイトからインストールをお願いします。
また surefire-plugin のバージョンについてですが、quakusが生成したプロジェクトのビルドの途中で以下のようなエラーに遭遇したのでpom.xmlを手で修正してバージョンアップさせました。(前はうまく言っていたんだけどね・・・?)
それでは Azure Functions の AD B2C での SSO、チャレンジしてみましょう!
1. Quarkus のサンプルを Azure Functions に上げてみる。
まずはサクッと、上げてみたいと思います。以下のページの手順で、ほぼ大丈夫です。
1-1. プロジェクトの作成
以下のコマンドでプロジェクトを生成します。
$ mvn archetype:generate \
-DarchetypeGroupId=io.quarkus \
-DarchetypeArtifactId=quarkus-azure-functions-http-archetype \
-DarchetypeVersion=1.0.1.Final
で、Azure上にデプロイするプロジェクトの情報を色々聞かれますので、チュートリアルにしたがって以下のように答えておきます。
- groupId: org.acme
- artifactId: quarkus-demo
- version: デフォルトでOK
- package: デフォルトでOK
- appName: デフォルトでOK
- appRegion: デフォルトでの"westus"でもOKというかお好みですが、近い方が良いので "japaneast" を指定してみます。
- function: デフォルトでOK
- resourceGroup: Functinosのインスタンスを生成する先のリソースグループを指定します。
前半は Maven プロジェクトのお決まりのやつですが、appName
と appRegion
、resourceGroup
はAzure Portal で作成する/されたものと一致してある必要あります。
また、ここの設定値は pom.xml に反映されますので、リージョンやリソースグループなどの設定値を変更したい場合は pom.xml の定義を修正すればOKです。
1-2. az のログイン
以下のコマンドで AZ CLI をログインさせ、操作対象のプロジェクトを決めておきます。
$ az login
ブラウザが立ち上がるのでAzureにログインをお願いします。
コンテナの中などブラウザ立ち上がらない状況では、"次のコード入れてね〜"という案内が出るので大丈夫です。
1-3. 関数アプリ、Functions App の作成
チュートリアルでは mvn clean install azure-functions:deploy
でOKとありますが、自分の環境では何度やってもFunctionsの作成に失敗してしまいました。
先に Azure Portal から Functions Appを作成しておかないとデプロイができませんでした。
というわけで、上記で指定した設定通りの Functions App を作成します。
まずは Azure ポータルの"リソースの作成"をクリックして、"functions"を検索します。
"Function App"がヒットしました。Classic でない方を選択します。
はい、"関数アプリ"が出ました。"作成"をクリックします。(Azure FunctionsだったりFunction Appだったり関数アプリだったり、そのあたりの統一感がないのがね。。。)
リソースグループに "resourceGroup"で指定した値、関数アプリ名に"appName"で指定した値、ランタイムスタックに "Java"を指定します。地域では "appRegion" で指定した地域を使用します。
"確認および作成"をクリックすると、設定に問題がないか確認画面が表示され、確認画面から"作成"ボタンクリックで初期状態のFunctionが作成されます。
この作成作業は数分かかります。しばらく待つと"リソースへ移動"ボタンが表示されるのでクリックすると作成された関数アプリの詳細画面に行けます。
そこで表示されているURLをクリックすると、以下の画面になると思います。
はい、スタート画面が無事に表示されました!
1-4. デプロイ&確認
デプロイそのものは以下のコマンドで一発です。簡単。
$ mvn clean install azure-functions:deploy
"deploy" コマンドでは 'target' 以下のお掃除はしてくれませんので、ソースコードや構成に変更があった場合は毎度、clean install azure-functions:deploy
の3点セットは実行した方が良いです。
単にネットワークエラーやログインエラーなどの"デプロイに失敗しただけ"の時は"azure-functions:deploy"でOKです。
ブラウザから関数アプリのアドレスに"/api/hello"を付けたURL、https://xxxxxxxxx.azurewebsites.net/api/hello
のようなURLを開いてみましょう。
・・・はい無事に hello jaxrs
が表示され、動作確認完了です。
あっさりですが、これで REST APIやServlet、vert.x使ってリバプロ、なんてこともできてしまいます。あらイージー。。。
それでは、いよいよ AD B2C の世界へ踏み込んで参りましょう。。。
2. AD B2C の作成
上記でデプロイした関数アプリと同じリソースグループに "AD B2C"のリソースを作成します。このあたりの手順はやや複雑なので、公式ガイドをご覧ください。(これもわかりにくいけど…)
この手順は非常にわかりにくいので、ざっくり言いますと "一旦、新規にディレクトリ作成してから、サブスクリプションに割り当てる"の2段階の作業が必要です。
で、この2つの作業はどちらもリソースの新規作成→AD B2Cで実施します。つまり…
- リソースの新規作成→AD B2C作成→新しいテナントの作成
- リソースの新規作成→AD B2C作成→既存テナントのサブスクリプションにリンク
という手順を踏む必要があります。まじすか。。。
3. AD B2C の設定、の準備
続いては ローカルで quarkus の開発サーバーを動かしておき、そこへのアクセストークンをPostman で取得してみる、ということをゴールとします。
つまり、"サービスアプリ"は "ローカル"、"クライアントアプリ"は "Postman" という構成を AD B2C にて設定します。
AD B2C、というか AD はこのように設定作業の前に何のために何をするかをはっきりさせておかないと、設定の沼にハマります。ついつい目の前の設定値にのめり込んで闇雲なトライ&エラーに陥りがちです。
さて、以下の公式チュートリアルを参考に設定を行っていきたいと思いますが、初見では正直、何を言っているのか何をやっているのか初めはさっぱり掴めませんでした。。。
が、要するに "ロールじゃなくてスコープでアクセス制限するからね〜"ということです。サービス側では"スコープの定義"、クライアントでは"使用するスコープを許可してもらう"、という設定が必要です。(振り返って初めて理解したけどね。。。というか途中でクライアントアプリと言ったりユーザーと言ったりするから混乱する。)
それでは地獄の1丁目、いってみます!
4. アプリの登録 その1: "サービスアプリ"の登録
まずは "サービス用アプリ"としてhttp://localhost:8080
を登録します。
4-1. 作成
AD B2Cリソースを開きます。が、 AD B2C はリソースグループ内に新規にディレクトリを作成します。
まずは新規に作成したADB2Cリソースと同じ名前のディレクトリに移動しましょう。
なんとかして AD B2C サービスの画面にたどり着いてください。そこで"アプリの登録(プレビュー)"をクリックします。
アプリ名は適当でOKですが、下の方にあるオプションで "Web API"を指定してください。
登録が完了したら、URLの追加をします。左のメニューから"認証"をクリック、表示されたページから"プラットフォームを追加"をクリックします。
以下は、続いて表示されるダイアログの"構成の選択"から"Web"を選択した後の画面です。ここで保護するサイトのURLを入力します。
いったん、http://localhost:8080
とローカルの開発サーバーを指しておきましょう。
"構成"をクリックで保存されます。
4-2. "スコープ"の追加
サービスを提供する側のアプリは"自分がどのようなスコープを使用するのか"を定義する必要があります。
左のメニューから"APIの公開"をクリックし、 "Scopeの追加"をクリックします。
初回は"アプリID URI の設定"ダイアログが立ち上がります。
とりあえずサンプルの通りに設定してみます。
そして、ここで表示されている "スコープ名" 下のURI、これが後で指定するスコープ名となります。これ、まじで地雷。
すでにお腹いっぱいな感じがしますが、これでようやく"サービス側アプリ"の登録が完了です。
5. アプリの登録 その2: "クライアントアプリ"の登録
続いてpostmanを登録します。
5-1. 作成
再び"アプリケーションの登録(プレビュー)"からPostman用のアプリケーション登録を行います。
前回と違い、"URLを入力する欄"が表示されています。どうした? ここも http://localhost:8080
としておきます。
登録ができると概要画面に行きます。ここでクライアントのIDをコピーしておきましょう。
5-2. "アクセス許可"の追加
"アクセス許可"とは、サービスアプリで提供される"スコープ"のうち、どのスコープでのアクセスをこのクライアントで許可するのか?というわけです。サービス側のアプリケーション登録で公開したスコープをクライアントに割り当てます。
左側のメニューから"APIのアクセス許可"をクリックし、アクセス許可の構成画面を表示します
"アクセス許可の追加"ボタンをクリックします。
"自分のAPI"から先ほど登録したサービス側のアプリを選択します。
"委任されたアクセスの許可"を選択し、上記のSample App のアクセス許可から File.Read
を選択して"アクセス許可の追加"をクリックします。
で、これに"管理者の同意を与えます"ボタンクリックです。
ログイン画面と同意のダイアログが表示されるので"承諾"ボタンをクリックしておきます。
しばらく待つと一覧画面にチェックがついて委任の同意が得られたことがわかります。
クライアント側アプリの設定は以上です。
6. ユーザーフロー(ポリシー)の追加
続いてログインするユーザーの処理フローの追加を行います。
今回はユーザーの新規登録とログインが行える "SignUp_SignIn" のフローを使用します。(というかだいたいこれでOK)
6-1. 作成
AD B2Cのトップに戻り、左側のメニューから "ユーザーフロー(ポリシー)"をクリックします。
"新しいユーザーフロー"をクリックします。
ユーザーフローから"サインアップとサインイン"をクリックします。
名前を入力し、"Email signUp" にチェックを入れて "作成ボタン"をクリックします。名前は "SignUp_SignIn"の略で susi
としておきます。
6-2. エンドポイントの確認方法
作成後の 一覧から"B2C_susi" を選んで詳細画面を表示します。
ユーザーフローを実行します
ボタンをクリックします。
..../v2.0/.well-known/openid-configuration
をクリックしてブラウザで開きます。
issuerの設定をみてみましょう。
最後についている ?p=b2c_1_susi
は大抵のクライアントじゃ処理できないと思います。(クエリで終わるURLを想定していない)
というわけで、次の"トークンの互換性の設定" が必要です。
6-3. トークンの互換性の設定
ページの中盤の"トークンの互換性設定"から tfpが含まれるhttps://<domain>/tfp/xxxxxxxxxxx/B2C_1_susi/v2.0
を選択し、"保存"をクリックする。
これによって issuer で表示される URL がパスにポリシー名が含まれた形式に変わります。これで様々なクライアントからアクセスできるようになります。
先ほどの.well-known/openid-configuration
のアドレスをブラウザで開いてみましょう。issuer のアドレスがちょっと変わっていると思います。これ、後でも述べますが非常に重要なのです。
いや、ほんと、これはよく見つけたよね、と自分でも思う。
ローカルを対象とするAzureの設定は一旦、以上です。
7. Postman からトークンの取得
いよいよ Postman の登場です!Postmanの起動〜ログインの流れは省略いたします。各位、よろしくお願いします。
7-1. トークンの取得
新規のリクエストを作成してから"Authrization"をクリック、Typeに"OAuth 2.0"を選択して次のような画面になっているというところからスタートします。
"Get New Access Token" をクリックしてください。以下のようなダイアログが表示されると思います。
各パラメータに以下の値を設定します。
- Token Name: 適当でOKです。
- Callback URL : http://localhost:8080
- Auth URL : 先ほどの
.well-known/openid-configuration
のjsonから得られる "authorization_endpoint" の URL。"b2c_1_susi/oauth2/v2.0/authorize" で終わるもの。 - Client ID: Postman用に追加した AD B2CのアプリケーションのID
- Scope: 例のScopeのURIをここで直打ちします。それと
openid
が必要です。https://xxxxxxxxxxx.onmicrosoft.com/xxxxxxxxx/File.Read openid
のような目を疑う設定値となります。 - State: 空白でOK
- Client Authentication:
Send client credentials in body
を選択
で、"Request Token"をクリックします。
sute.jp
などで適当なメールアドレスをゲットしておきましょう。Azureのログイン画面が表示されるので"Sign Up"からユーザー登録を行います。
無事にトークンがゲットできたでしょうか?
みんな、無事じゃないようですけど。。。
特にScopeの設定がびっくりです。また、authのアドレスも気をつけてください。?p=B2C_1_susi
で終わるURLはPostmanでもエラーが出ますよ!
7-2. jwt.io で確認
ブラウザで jwt.io にアクセスして少しスクロールすると表示される "Debugger" の箇所を表示します。
左側の"Encoded"エリアに Postman で取得できたトークンをベタっと貼り付けると右側の "Decoded" エリアに中身の JSON が復元されます。
...
"scp":"File.Read"
...
というのが確認できたでしょうか?この "scp" の値を Quarkus (というかJavaEE)的に "Role"として扱えばOKということになります。
8. Quarkus OIDC の追加
それでは Quarkus に OIDC 対応を追加していきます。以下のコマンドで Quarkus OIDC プライグインを追加します。
$ mvn quarkus:add-extension -Dextensions="oidc"
続いてsrc/main/resource/application.properties
を以下の内容で作成します。
quarkus.oidc.auth-server-url=https://xxxxxxxxxx.b2clogin.com/tfp/xxxxxxxxxxxxx/b2c_1_susi/v2.0/
quarkus.oidc.authentication.scopes=https://xxxxxxx.onmicrosoft.com/xxxxxxxxxxxxx/File.Read openid
quarkus.oidc.client-id=xxxxxxxxxx
quarkus.oidc.roles.role-claim-path=scp
この設定もかなり手こずりましたが、注意点は以下の通りです。
- auth-server-urlは issuer のURLを厳密にコピペする。最後の"/"も欠けてはいけない
- scopes の scope は例のURI。
- role-claim-path はjwt.ioで表示されたjsonのトップを起点とするパス。今回は"scp"だけで行けたので複雑にならず幸いだった。
で、メソッドの方に制約をつけます。
...
@Path("/api/hello")
@RolesAllowed("File.Read")
public class GreetingResource {
...
Quarkusの作業は一旦、以上とします。"/api/hello"がアクセス保護できればOKとします。
で、以下のコマンドでデプロイを行います。
$ mvn clean install azure-functions:deploy
9. Postman での確認
さて、実際の Azure Functions に対して Postman からのトークン付きアクセスを行ってみましょう。
9-1. AD B2C の設定変更
サービスアプリ、クライアントアプリそれぞれのリダイレクトURLを functions の方に向けておきます。
AD B2C の画面に戻り、"アプリケーションの登録(プレビュー)"からサービス側のアプリを選択し、"認証"をクリックします。
ここでリダイレクト先のURLを関数アプリのURL + "/api/hello" として、"保存"します。
これをPostman 用のアプリケーションでも実施します。
9-2. Postman でのアクセス
まずは、そのまま叩いてみましょう。
401 エラーとなりました。トークンなしではアクセスできませんね。
続いて新規のトークンを取得してみましょう。先ほどと同様ですが、リダイレクト先を関数アプリのURL +"/api/hello" に変更し、トークンを取得してください。
トークンの取得後、以下のAdd auth data to
でRequest Headers
が選択されていることを確認してください。Request URL
ではログインできませんでした。。
それでは、トークンを使用してGETしてみます!
ステータスが200
でhello jaxrs
のレスポンスが帰ってきました! @RolesAllowed("File.Read")
のアノテーション、ちゃんと通過できました!!
お疲れ様でした!
まとめ
振り返ってみると、一応はトークンでのアクセス出来ましたし、手順もそんなに多くはないです。
AD B2C の設定手順も Keycloak に比べてステップがすごい多いということはありません。
(Keycloakも慣れなければなかなか難解です。)
が、情報の少なさと不正確さと、UIの分かりにくさが半端ないので非常に手こずりました。。。
この手順も何ヶ月持つのかと思うと切なさと愛しさと心強さと。。。もう、愛のままにわがままに僕は君だけを傷つけない気持ちでこのまま君だけを奪い去りたいです。。。
脳みそが蕩けてますが・・・続いて Postmanではなく、Angularのクライアントからのアクセスに挑戦してみたいと思います。
今回作成したコードは以下のリポジトリにあげました。Quarkusの方はほとんどコーディングしてないけど・・・ご参考にどうぞ。
いや〜大変だった!