Quarkus Sample – OpenID Connect でセキュアなエンドポイントを構築する
はじめに
個人的に CloudNative なアプリケーションフレームワーク Quarkus のいろいろなモジュールを試しています。その中から今回は OpenID Connect クライアントモジュールを利用してみます。
QUARKUS - USING OPENID CONNECT TO PROTECT WEB APPLICATIONS USING AUTHORIZATION CODE FLOW
例では KeyCloak を利用していましたが、今回は Auth0 を利用しました。
事前準備
Quarkus ブランクプロジェクトの作成
Quarkus の最低限のブランクプロジェクトを作成します。追加の依存は何も指定しません。
pom.xml の中は以下の通り。
<dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> </dependencies>
デフォルトで /hello-resteasy
エンドポイントや index.html
が配置されており、テストコードもあります。このままでも普通に動作します。
$ ./mvnw compile quarkus:dev --2021-01-28 00:51:13-- https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar Resolving repo.maven.apache.org (repo.maven.apache.org)... 151.101.40.215 Connecting to repo.maven.apache.org (repo.maven.apache.org)|151.101.40.215|:443... connected. HTTP request sent, awaiting response... 200 OK ... Listening for transport dt_socket at address: 5005 __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2021-01-28 00:56:08,979 INFO [io.quarkus] (Quarkus Main Thread) quarkus-oidc-sample 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.11.0.Final) started in 1.242s. Listening on: http://localhost:8080 2021-01-28 00:56:08,982 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2021-01-28 00:56:08,982 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy]
/hello-resteasy
エンドポイントを確認。
$ curl http://localhost:8080/hello-resteasy Hello RESTEasy
はい。
ブラウザで index.html
を確認。
はい。
Auth0 Web Application 作成
Auth0 を利用します。前提としてすでにアカウントサインアップは完了している状態から開始します。
通常の Web Application を作成します。
Settings
以下の Domain, ClientId, Secret を利用します。
Social Login 設定
Social Login でログインしたい場合、 Social Connection の設定が必要です。今回は Google の Social Login を利用して Google の Profile 情報を読みたいのでこちらの設定を行います。
Connections > Social
を選択し、CREATE SOCIAL CONNECTION
をクリック。- Google を選択。
- Settings はひとまずデフォルト状態で Connection を作成。
- 対象の Application に対して有効に
これで Auth0 のログインフォームで Google の Social Login が利用できるように。
Redirect URL の設定
Auth0 の認証処理を経てアプリケーションへ戻る際に、Callback URL を指定します。これはホワイトリストで Redirect される可能性のある URL をすべて列挙する必要があります。今回は、 http://localhost:8080/tokens
が Callback 先なのでこちらを登録します。
OIDC モジュールの追加
pom.xml に OIDC モジュールを追加します。
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc</artifactId> </dependency>
ちなみにモジュールを入れた直後に何もせずに起動すると失敗します。
Listening for transport dt_socket at address: 5005 Both 'auth-server-url' and 'client-id' properties must be configured Quarkus application exited with code 1 Press Enter to restart or Ctrl + C to quit __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2021-01-28 00:59:12,430 INFO [io.qua.dep.dev.IsolatedDevModeMain] (main) Attempting to start hot replacement endpoint to recover from previous Quarkus startup failure
Both 'auth-server-url' and 'client-id' properties must be configured
最低限この2つの設定は必須だからな!と怒られました。すみません。
application.properties
OIDC モジュールで必要な設定値を埋めていきます。先程のログからわかるように最低限の設定は auth-server-url
と client-id
の模様。他は Optional な値であったり、 Default 値が適用される設定値のようです。
Tutorial の方ではベタ書きしているのですが、Secret 情報をベタ書きしてしまうと、意図せず Github へアップロードしたりする可能性があるため環境変数から取得するようにします。
quarkus.oidc.auth-server-url=${AUTH_SERVER_URL} quarkus.oidc.client-id=${CLIENT_ID} quarkus.oidc.credentials.secret=${CLIENT_SECRET} quarkus.oidc.application-type=web-app quarkus.oidc.authentication.scopes=openid,profile
プロパティの詳細な説明はこちらです。それぞれの項目については以下の通り。
auth-server-url
今回は Auth0 のドメインを指定します。 https://<YOUR_DOMAIN>.auth0.com
といった値です。 Auth0 のアプリケーション設定値では <YOUR_DOMAIN>.auth0.com
が表示されているのでスキーマを追加して設定します。
client-id
ClientId を指定します。この値は Application ごとに異なります。
Credentials.secret
Client Secret の値を指定します。Secret の値は重要なため Auth0 の設定画面でもマスクされた状態です。
application-type
Quarkus で構成するアプリケーションがどのようなタイプなのかを指定します。
API サーバーとして構成したい場合は、未認証の場合のログインフォームへのリダイレクトは不要なので service
を指定します。これを指定すると Authorization ヘッダに有効な Bearer Token が付与されていない場合は、 401 Unauthorize
エラーが返却されます。利用者は自身で有効な Bearer Token を取得し、Authorization ヘッダに自身で付与する必要があります。
今回はユーザーへ画面を提供する Web アプリケーションとして構成するため web-app
を指定します。これにより認証が済んでいないユーザーは Auth0 が提供するログインフォームへリダイレクトされます。
Default は service
が設定されるので明示的に指定する必要があります。
authentication.scopes
OpenID Connect を指定するため openid
の指定は必須。名前等のプロフィール情報もほしいので profile
を指定します。
tokens エンドポイント
保護されるエンドポイントを作成します。このエンドポイントにアクセスする場合は、認可されていないとアクセスできません。 Social Login で取得できた IdToken
, AccessToken
, RefreshToken
をダンプする HTML を作成します。
@Path("/tokens") public class TokenResource { @Inject @IdToken JsonWebToken idToken; @Inject AccessTokenCredential accessToken; // ※1 @Inject RefreshToken refreshToken; @GET public String getTokens() { var response = new StringBuilder().append("<html>") .append("<body>") .append("<ul>"); if (idToken != null) { response.append("<li>IdToken:").append(idToken.toString()).append("</li>"); } if (accessToken != null) { // ※2 response.append("<li>AccessToken:").append(accessToken.getToken()).append("</li>"); } var userName = this.idToken.getClaim("preferred_username"); if (userName != null) { response.append("<li>username:").append(userName.toString()).append("</li>"); } // var scopes = this.accessToken.getClaim("scope"); ※3 response.append("<li>refresh_token:").append(refreshToken.getToken() != null).append("</li>"); return response.append("</ul>").append("</body>").append("</html>").toString(); } }
ガイドに記載されているコードから一部修正を行っています。理由は後述します。
アプリケーションを確認
事前準備
application.properties
で環境変数から値を取得するようにしているため、環境変数を設定します。
export AUTH_SERVER_URL=https://<YOUR_DOMAIN>.auth0.com export CLIENT_ID=<AUTH0_APPLICATION_CLIENT_ID> export CLIENT_SECRET=<AUTH0_APPLICATION_CLIENT_SECRET>
それぞれ Auth0 のアプリケーション設定からコピペして設定してください。
実行する
$ ./mvnw clean compile quarkus:dev ... [INFO] --- quarkus-maven-plugin:1.11.0.Final:dev (default-cli) @ quarkus-oidc-sample --- Listening for transport dt_socket at address: 5005 __ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2021-02-04 16:38:46,578 INFO [io.quarkus] (Quarkus Main Thread) quarkus-oidc-sample 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.11.0.Final) started in 3.019s. Listening on: http://localhost:8080 2021-02-04 16:38:46,581 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2021-02-04 16:38:46,581 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, oidc, resteasy, security]
動作を確認
http://localhost:8080/tokens
へアクセス- Auth0 のログインフォームへ Redirect
- Google アカウントで Social Login
- Auth0 へ対象データの読み取りを許可。
http://localhost:8080/tokens
へRedirect され、結果を表示。
<html> <body> <ul> <li>IdToken:DefaultJWTCallerPrincipal{id='null', name='google-oauth2|XXXXXXXXXXXX', expiration=1612460836, notBefore=0, issuedAt=1612424836, issuer='https://xxxxxx.auth0.com/', audience=[39YRjuE4U3bCJm85emPkTbwhJTVvGLZF], subject='google-oauth2|XXXXXXXXXXXX', type='JWT', issuedFor='null', authTime=0, givenName='HIRAKU', familyName='KOMURO', middleName='null', nickName='hogehoge', preferredUsername='null', email='null', emailVerified=null, allowedOrigins=null, updatedAt=null, acr='null', groups=]}</li> <li>AccessToken:izvFojU3g_aIAirlkxxxxxxxxXXXXXX</li> <li>refresh_token:true</li> </ul> </body> </html>
IdToken
の全体情報、AccessToken
情報、RefreshToken
の有無を表示しています。Google の Profile 情報から取得しているのでわたしの本名やニックネームといった情報が含まれているのが確認できます。 email
は scope で指定していないので取れていません。想定通り。
TokenResource
のコード上では Profile の中の prefered_username
を探しているようですが、今回は null
なので値なし。
サンプルコードの修正箇所について
何箇所かそのままでは利用できなかったので、一部修正しています。元々 KeyCloak を対象にしたサンプルコードであるため、違いが出ているようです。
標準があるとはいえ、若干の違いが出る関係上、少々調整が必要なようです。
※1
ガイドでは以下でコードが記載されています。
@Inject JsonWebToken accessToken;
しかしサーバーを起動後、ログインフォームにて Google の Social Login を実行すると以下のエラーが発生し Failed します。
2021-02-03 22:15:49,664 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /tokens failed, error id: 69786bf6-a838-42c0-95ac-f411940ef88f-1: org.jboss.resteasy.spi.UnhandledException: io.quarkus.oidc.OIDCException: Opaque access token can not be converted to JsonWebToken at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:106) ...
OpaqueToken
を JsonWebToken
にしようとして失敗してます。どうやら正常にパースできてなさそう。
そこで、AccessTokenCredential
に変更して対応しています。
これが正しい対応かは不明です。同じパッケージ内にあった最も Opaque Token をハンドリングできそうなクラスを適用しました。
Auth0 に AccessToken に関するセクションがありました。こちらを確認すると Opaque AccessToken
と JWT AccessToken
があるのが確認できます。今回は OpaqueToken であるため、構造化されたデータではなく単なる文字列が返ってきています。
※2
前段で accessToken
を AccessTokenCredential
に変更しました。ここでは Token の中身を出力するようにダンプしました。 AccessTokenCredential
には getToken()
メソッドが存在し、トークンの中身をStringで取り出せるようです。
※3
元のコードでは JsonWebToken
で構造化された Token を期待していたため、中から scope
にあたる Claim を取得しようとしています。しかし今回 accessToken
は JWT ではないため、この scope
は取得できません。従ってコードを削除しています。
Scopes
今回は openid
と profile
を scope に設定しました。しかしこの設定値は Required になっていません。そのため、明示的に設定しなくても動くはずです。
何も指定しない場合のデフォルト値
コードを確認してみると、scopes
のデフォルト値は空のようでした。
extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java
/** * List of scopes */ @ConfigItem public Optional<List<String>> scopes = Optional.empty();
OpenID Connect を利用する場合 openid
の指定が必須のはず。
Quarkus の設定では Optional になっており、 Default 値は明記されていませんでした。Auth0 のドキュメントでは required
とされているので、おそらく指定はされている気がするのですが。
今度深追いしてみたい。どこかで設定されてそう。
おまけ
Auth0 の IdToken に含まれる updated_at
のパースに失敗しており、 Quarkus の中で参照する IdToken の Claim に情報を適用できていません。従って、 updated_at
は常に null
です。
以下 StackTrace です。
2021-01-19 23:40:55,174 WARN [io.sma.jwt.aut.principal] (executor-thread-1) SRJWT08002: getClaimValue failure for: updated_at: org.jose4j.jwt.MalformedClaimException: The value of the 'updated_at' claim is not the expected type (2021-01-17T07:27:40.906Z - Cannot cast java.lang.String to java.lang.Long) at org.jose4j.jwt.JwtClaims.getClaimValue(JwtClaims.java:256) ...
どうやら String から Long への Cast に失敗しているようです。updated_at
の値は Auth0 から ISO 8601 形式の日時が送られてきてる模様。"1615993934"
といった文字列を期待してパースを行っている模様。
OpenID Connect Core 1.0 incorporating errata set 1 - 5.1. Standard Claims では updated_at
は Epoch Sec を number で返すよう定義してあるため、おそらくQuarkus 側のパーサーはこれを期待していそうです。
Auth0 の Community に随分昔から該当の Issue はあるようです。特に修正予定はなさそう。
今後の宿題
- HTML テンプレートを使いたい
- Json でログを出力したい
- Docker イメージをビルドしてコンテナで実行したい
- Id Token のカスタムパーサーが書けるかどうか試したい
- Quarkus の OIDC モジュール使って何も指定しない際の scope 指定はどうなってるか