第1章 OpenID Connect (OIDC) ベアラートークン認証
Quarkus OpenID Connect (OIDC) エクステンションを使用して、ベアラートークン認証により、アプリケーション内の Jakarta REST (旧称 JAX-RS) エンドポイントへの HTTP アクセスを保護します。
1.1. Quarkus のベアラートークン認証メカニズムの概要
Quarkus は、Quarkus OpenID Connect (OIDC) エクステンションを通じて、ベアラートークン認証メカニズムをサポートします。
ベアラートークンは、OIDC と、Keycloak などの OAuth 2.0 準拠の認可サーバーによって発行されます。
ベアラートークン認証は、ベアラートークンの存在と有効性に基づいて HTTP リクエストを承認するプロセスです。ベアラートークンは、呼び出しのサブジェクトに関する情報を提供します。これは、HTTP リソースにアクセスできるかどうかを判断するために使用されます。
次の図は、Quarkus のベアラートークン認証メカニズムの概要を示しています。
図1.1 シングルページアプリケーションを使用した Quarkus のベアラートークン認証メカニズム
- Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
- Quarkus ユーザーは、シングルページアプリケーション (SPA) にアクセスします。
- 単一ページアプリケーションは、Authorization Code Flow を使用してユーザーを認証し、OIDC プロバイダーからトークンを取得します。
- シングルページアプリケーションは、アクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
- Quarkus サービスは、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のリクエストをチェックします。トークンが有効な場合はリクエストを続行できるようにし、シングルページアプリケーションにサービス応答を返します。
- シングルページアプリケーションは、Quarkus ユーザーに同じデータを返します。
図1.2 Java またはコマンドラインクライアントを使用した Quarkus のベアラートークン認証メカニズム
- Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
-
クライアントが、
client_credentials
かパスワードグラントを使用して、OIDC プロバイダーからアクセストークンを取得します。client_credentials には、クライアント ID とシークレットが必要です。パスワードグラントには、クライアント ID、シークレット、ユーザー名、およびパスワードが必要です。 - クライアントはアクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
- Quarkus サービスは、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のクレームをチェックして、トークンが有効な場合はリクエストの続行を許可し、サービスレスポンスをクライアントに返します。
OIDC 認可コードフローを使用してユーザーを認証および認可する必要がある場合は、Quarkus ガイド Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム を参照してください。また、Keycloak およびベアラートークンを使用する場合は、Quarkus ガイド Using Keycloak to centralize authorization を参照してください。
OIDC ベアラートークン認証を使用してサービスアプリケーションを保護する方法は、次のチュートリアルを参照してください: * OpenID Connect (OIDC) 認可コードフローを使用した Web アプリケーションの保護
複数のテナントをサポートする方法の詳細は、Quarkus ガイドの OpenID Connect マルチテナンシーの使用 を参照してください。
1.1.1. JWT クレームへのアクセス
JWT トークンクレームにアクセスする必要がある場合は、JsonWebToken
を注入します。
package org.acme.security.openid.connect; import org.eclipse.microprofile.jwt.JsonWebToken; import jakarta.inject.Inject; import jakarta.annotation.security.RolesAllowed; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/api/admin") public class AdminResource { @Inject JsonWebToken jwt; @GET @RolesAllowed("admin") @Produces(MediaType.TEXT_PLAIN) public String admin() { return "Access for subject " + jwt.getSubject() + " is granted"; } }
JsonWebToken
の注入は、@ApplicationScoped
、@Singleton
、および @RequestScoped
スコープでサポートされています。ただし、個々のクレームが単純な型として注入される場合は、@RequestScoped
を使用する必要があります。詳細は、Quarkus ガイドの "Using JWT RBAC" の Supported injection scopes セクションを参照してください。
1.1.2. UserInfo
OIDC UserInfo
エンドポイントから UserInfo JSON オブジェクトをリクエストする必要がある場合は、quarkus.oidc.authentication.user-info-required=true
を設定します。OIDC プロバイダーの UserInfo
エンドポイントにリクエストが送信され、io.quarkus.oidc.UserInfo
(単純な javax.json.JsonObject
ラッパー) オブジェクトが作成されます。io.quarkus.oidc.UserInfo
は、SecurityIdentity
userinfo
属性として注入またはアクセスできます。
1.1.3. 設定メタデータ
現在のテナントの検出された OpenID Connect 設定メタデータ は、io.quarkus.oidc.OidcConfigurationMetadata
で表され、SecurityIdentity
configuration-metadata
属性として注入またはアクセスできます。
エンドポイントがパブリックの場合、デフォルトのテナントの OidcConfigurationMetadata
が注入されます。
1.1.4. トークンクレームと SecurityIdentity ロール
検証済みの JWT アクセストークンから SecurityIdentity
ロールを次のようにマップできます。
-
quarkus.oidc.roles.role-claim-path
プロパティーが設定されており、一致する配列または文字列のクレームが見つかった場合、これらのクレームからロールが展開されます。たとえば、customroles
、customroles/array
、scope
、"http://namespace-qualified-custom-claim"/roles
、"http://namespace-qualified-roles"
などです。 -
groups
クレームが利用可能な場合は、その値が使用されます。 -
realm_access/roles
またはresource_access/client_id/roles
(client_id
はquarkus.oidc.client-id
プロパティーの値) クレームが利用可能な場合は、その値が使用されます。このチェックは、Keycloak によって発行されたトークンをサポートします。
たとえば、次の JWT トークンには、ロールを含む roles
配列のある複雑な groups
クレームがあります。
{ "iss": "https://server.example.com", "sub": "24400320", "upn": "jdoe@example.com", "preferred_username": "jdoe", "exp": 1311281970, "iat": 1311280970, "groups": { "roles": [ "microprofile_jwt_user" ], } }
microprofile_jwt_user
ロールを SecurityIdentity
ロールにマップする必要があります。これは、quarkus.oidc.roles.role-claim-path=groups/roles
という設定で実行できます。
トークンが不透明 (バイナリー) の場合、リモートトークンイントロスペクション応答の scope
プロパティーが使用されます。
UserInfo
がロールのソースである場合は、quarkus.oidc.authentication.user-info-required=true
および quarkus.oidc.roles.source=userinfo
を設定し、必要に応じて quarkus.oidc.roles.role-claim-path
を設定します。
さらに、カスタム SecurityIdentityAugmentor
を使用して、ロールを追加することもできます。詳細は、Quarkus ガイド "Security Tips and Tricks" の Security identity customization セクションを参照してください。
HTTP セキュリティーポリシー を使用して、トークン要求から作成された SecurityIdentity
ロールをデプロイメント固有のロールにマップすることもできます。
1.1.5. トークンスコープと SecurityIdentity 権限
SecurityIdentity
権限は、ロールのソース のスコープパラメーターから io.quarkus.security.StringPermission
の形式でマッピングされ、同じクレームセパレーターが使用されます。
import java.util.List; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.security.PermissionsAllowed; @Path("/service") public class ProtectedResource { @Inject JsonWebToken accessToken; @PermissionsAllowed("email") 1 @GET @Path("/email") public Boolean isUserEmailAddressVerifiedByUser() { return accessToken.getClaim(Claims.email_verified.name()); } @PermissionsAllowed("orders_read") 2 @GET @Path("/order") public List<Order> listOrders() { return List.of(new Order(1)); } public static class Order { String id; public Order() { } public Order(String id) { this.id = id; } public String getId() { return id; } public void setId() { this.id = id; } } }
io.quarkus.security.PermissionsAllowed
アノテーションの詳細は、"Authorization of web endpoints" の 権限のアノテーション セクションを参照してください。
1.1.6. トークンの検証とイントロスペクション
トークンが JWT トークンの場合、デフォルトでは、OIDC プロバイダーの JWK エンドポイントから取得されたローカル JsonWebKeySet
の JsonWebKey
(JWK) キーを使用して検証されます。トークンのキー識別子 (kid
) ヘッダー値は、一致する JWK キーを見つけるために使用されます。一致する JWK
がローカルで利用できない場合は、JWK エンドポイントから現在のキーセットを取得して JsonWebKeySet
が更新されます。JsonWebKeySet
の更新は、quarkus.oidc.token.forced-jwk-refresh-interval
の有効期限が切れた後にのみ、繰り返すことができます。デフォルトの有効期限は 10 分です。更新後に一致する JWK
が利用できない場合は、JWT トークンが OIDC プロバイダーのトークンイントロスペクションエンドポイントに送信されます。
トークンが不透明である場合は、それがバイナリートークンまたは暗号化された JWT トークンである可能性があり、そのトークンは常に OIDC プロバイダーのトークンイントロスペクションエンドポイントに送信されます。
JWT トークンのみを使用しており、一致する JsonWebKey
が常に使用可能であることが予想される場合 (たとえば、キーセットを更新した後など)、次の例に示すように、トークンイントロスペクションを無効にする必要があります。
quarkus.oidc.token.allow-jwt-introspection=false quarkus.oidc.token.allow-opaque-token-introspection=false
JWT トークンをイントロスペクションのみで検証する必要がある場合があります。これは、イントロスペクションエンドポイントアドレスのみを設定することで強制できます。次のプロパティー設定は、Keycloak を使用してこれを実現する方法の例を示しています。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.discovery-enabled=false # Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
JWT トークンのイントロスペクションをリモートで間接的に実施することには、利点と欠点があります。利点は、2 つのリモート呼び出し (リモート OIDC メタデータ検出呼び出しと、それに続く使用されない検証キーを取得するための別のリモート呼び出し) が不要になることです。欠点は、イントロスペクションエンドポイントアドレスを知って、手動で設定する必要があることです。
この他にも、OIDC メタデータ検出のデフォルトオプションを許可しながら、リモート JWT イントロスペクションのみの実行を要求する方法があります (次の例を参照)。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.token.require-jwt-introspection-only=true
このアプローチの利点は、設定がよりシンプルで理解しやすいことです。欠点は、検証キーが取得されない場合でも、イントロスペクションエンドポイントアドレスを検出するためにリモート OIDC メタデータ検出呼び出しが必要になることです。
シンプルな jakarta.json.JsonObject
ラッパーオブジェクトである io.quarkus.oidc.TokenIntrospection
が作成されます。JWT または不透明トークンのいずれかが正常にイントロスペクトされている場合、SecurityIdentity
introspection
属性として注入またはアクセスできます。
1.1.7. トークンイントロスペクションと UserInfo
キャッシュ
すべての不透明アクセストークンは、リモートでイントロスペクトする必要があります。場合によっては、JWT アクセストークンをイントロスペクトする必要もあります。UserInfo
も必要な場合は、OIDC プロバイダーへの後続のリモート呼び出しで同じアクセストークンが使用されます。したがって、UserInfo
が必要で、現在のアクセストークンが不透明である場合、そのようなトークンごとに 2 つのリモート呼び出しが行われます。1 つのリモート呼び出しはトークンをイントロスペクトするためのもので、もう 1 つは UserInfo
を取得するためのものです。トークンが JWT の場合、イントロスペクトもされている必要がない限り、UserInfo
を取得するためのリモート呼び出しは 1 回だけ必要です。
着信ベアラーまたはコードフローアクセストークンごとに最大 2 回のリモート呼び出しを実行するコストが問題になる場合があります。
実稼働環境でこれが当てはまる場合は、トークンイントロスペクションと UserInfo
データを 3 分または 5 分などの短期間キャッシュすることを検討してください。
quarkus-oidc
は、@ApplicationScoped
キャッシュ実装に使用できる quarkus.oidc.TokenIntrospectionCache
および quarkus.oidc.UserInfoCache
インターフェイスを提供します。次の例に示すように、@ApplicationScoped
キャッシュ実装を使用して、quarkus.oidc.TokenIntrospection
オブジェクトや quarkus.oidc.UserInfo
オブジェクトを保存および取得します。
@ApplicationScoped @Alternative @Priority(1) public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache { ... }
各 OIDC テナントは、quarkus.oidc.TokenIntrospection
データ、quarkus.oidc.UserInfo
データ、またはその両方の保存を許可または拒否できます。これには、ブール値 quarkus.oidc."tenant".allow-token-introspection-cache
および quarkus.oidc."tenant".allow-user-info-cache
プロパティーを使用します。
さらに、quarkus-oidc
は quarkus.oidc.TokenIntrospectionCache
と quarkus.oidc.UserInfoCache
の両方のインターフェイスを実装する、シンプルなデフォルトのメモリーベースのトークンキャッシュを提供します。
デフォルトの OIDC トークンキャッシュは、次のように設定してアクティブ化できます。
# 'max-size' is 0 by default, so the cache can be activated by setting 'max-size' to a positive value: quarkus.oidc.token-cache.max-size=1000 # 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer: quarkus.oidc.token-cache.time-to-live=3M # 'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval': quarkus.oidc.token-cache.clean-up-timer-interval=1M
デフォルトのキャッシュはトークンをキーとして使用し、各エントリーには TokenIntrospection
、UserInfo
、またはその両方を含めることができます。max-size
のエントリー数までのみが保持されます。新しいエントリーを追加するときにキャッシュがすでにいっぱいになっている場合は、期限切れのエントリーを 1 つ削除してスペースを見つけようとします。さらに、クリーンアップタイマーをアクティベートにすると、期限切れのエントリーが定期的にチェックされ、削除されます。
デフォルトのキャッシュ実装を試したり、カスタムのキャッシュ実装を登録したりできます。
1.1.8. JSON Web Token のクレームの検証
ベアラー JWT トークンの署名が検証され、その expires at
(exp
) クレームがチェックされた後、次に iss
(issuer
) クレーム値が検証されます。
デフォルトでは、iss
クレーム値は、周知のプロバイダー設定で検出された可能性のある issuer
プロパティーと比較されます。ただし、quarkus.oidc.token.issuer
プロパティーが設定されている場合は、代わりに iss
クレーム値がそれと比較されます。
場合によっては、この ISS
クレーム検証が機能しないことがあります。たとえば、検出された issuer
プロパティーに内部 HTTP/IP アドレスが含まれているのに、トークンの iss
クレーム値に外部 HTTP/IP アドレスが含まれている場合などです。または、検出された issuer
プロパティーにテンプレートテナント変数が含まれているが、トークンの iss
クレーム値には完全なテナント固有の発行者値が含まれている場合などです。
このような場合は、quarkus.oidc.token.issuer=any
を設定して発行者の検証をスキップすることを検討してください。他のオプションが利用できない場合にのみ、発行者の検証をスキップします。
-
Keycloak を使用しており、異なるホストアドレスによって発行者検証エラーが発生する場合は、
KEYCLOAK_FRONTEND_URL
プロパティーを使用して Keycloak を設定し、同じホストアドレスが使用されるようにします。 -
マルチテナントデプロイメントで
iss
プロパティーがテナント固有である場合は、SecurityIdentity
tenant-id
属性を使用して、エンドポイントまたはカスタム Jakarta フィルターで発行者が正しいことを確認します。以下に例を示します。
import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.Provider; import org.eclipse.microprofile.jwt.JsonWebToken; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.security.identity.SecurityIdentity; @Provider public class IssuerValidator implements ContainerRequestFilter { @Inject OidcConfigurationMetadata configMetadata; @Inject JsonWebToken jwt; @Inject SecurityIdentity identity; public void filter(ContainerRequestContext requestContext) { String issuer = configMetadata.getIssuer().replace("{tenant-id}", identity.getAttribute("tenant-id")); if (!issuer.equals(jwt.getIssuer())) { requestContext.abortWith(Response.status(401).build()); } } }
quarkus.oidc.token.audience
プロパティーを使用して、トークン aud
(audience
) クレーム値を検証することを検討してください。
1.1.9. シングルページアプリケーション
シングルページアプリケーション (SPA) は通常、XMLHttpRequest
(XHR) と OIDC プロバイダーが提供する JavaScript ユーティリティーコードを使用して、Quarkus service
アプリケーションにアクセスするためのベアラートークンを取得します。
たとえば、Keycloak を使用する場合は、keycloak.js
を使用してユーザーを認証し、SPA から期限切れのトークンを更新できます。
<html> <head> <title>keycloak-spa</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script src="http://localhost:8180/js/keycloak.js"></script> <script> var keycloak = new Keycloak(); keycloak.init({onLoad: 'login-required'}).success(function () { console.log('User is now authenticated.'); }).error(function () { window.location.reload(); }); function makeAjaxRequest() { axios.get("/api/hello", { headers: { 'Authorization': 'Bearer ' + keycloak.token } }) .then( function (response) { console.log("Response: ", response.status); }).catch(function (error) { console.log('refreshing'); keycloak.updateToken(5).then(function () { console.log('Token refreshed'); }).catch(function () { console.log('Failed to refresh token'); window.location.reload(); }); }); } </script> </head> <body> <button onclick="makeAjaxRequest()">Request</button> </body> </html>
1.1.10. Cross-Origin Resource Sharing
別のドメインで実行されているシングルページアプリケーションから OIDC サービス
アプリケーションを使用する予定の場合は、Cross-Origin Resource Sharing (CORS) を設定する必要があります。詳細は、"Cross-origin resource sharing" の CORS filter セクションを参照してください。
1.1.11. プロバイダーエンドポイントの設定
OIDC service
アプリケーションは、OIDC プロバイダーのトークン、JsonWebKey
(JWK) セット、そして場合によっては UserInfo
およびイントロスペクションエンドポイントアドレスを認識している必要があります。
デフォルトでは、設定された quarkus.oidc.auth-server-url
に /.well-known/openid-configuration
パスを追加することによって検出されます。
あるいは、検出エンドポイントが利用できない場合、または検出エンドポイントのラウンドトリップを節約したい場合は、検出を無効にして、相対パス値で設定することができます。以下に例を示します。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.discovery-enabled=false # Token endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/token quarkus.oidc.token-path=/protocol/openid-connect/token # JWK set endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/certs quarkus.oidc.jwks-path=/protocol/openid-connect/certs # UserInfo endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/userinfo quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo # Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
1.1.12. トークンの伝播
ダウンストリームサービスへのベアラーアクセストークンの伝播に関する詳細は、Quarkus ガイド 「OpenID Connect (OIDC) と OAuth2 クライアントおよびフィルターのリファレンス」の トークンの伝播 セクションを参照してください。
1.1.13. OIDC プロバイダークライアント認証
quarkus.oidc.runtime.OidcProviderClient
は、OIDC プロバイダーへのリモートリクエストが必要な場合に使用されます。ベアラートークンのイントロスペクションが必要な場合は、OidcProviderClient
が OIDC プロバイダーに対して認証する必要があります。サポート対象認証オプションの詳細は、Quarkus ガイド「Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム」の OIDC プロバイダークライアント認証 セクションを参照してください。
1.1.14. テスト
Keycloak 認証 を必要とする Quarkus OIDC サービスエンドポイントをテストする必要がある場合は、Keycloak 認可のテスト セクションに従ってください。
次の依存関係をテストプロジェクトに追加することで、テストを開始できます。
Maven を使用:
<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency>
Gradle を使用する場合:
testImplementation("io.rest-assured:rest-assured") testImplementation("io.quarkus:quarkus-junit5")
1.1.14.1. WireMock
テストプロジェクトに次の依存関係を追加します。
Maven を使用:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-oidc-server</artifactId> <scope>test</scope> </dependency>
Gradle を使用する場合:
testImplementation("io.quarkus:quarkus-test-oidc-server")
REST テストエンドポイントを準備し、application.properties
を設定します。以下に例を示します。
# keycloak.url is set by OidcWiremockTestResource quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/ quarkus.oidc.client-id=quarkus-service-app quarkus.oidc.application-type=service
最後にテストコードを記述します。以下に例を示します。
import static org.hamcrest.Matchers.equalTo; import java.util.Set; import org.junit.jupiter.api.Test; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.oidc.server.OidcWiremockTestResource; import io.restassured.RestAssured; import io.smallrye.jwt.build.Jwt; @QuarkusTest @QuarkusTestResource(OidcWiremockTestResource.class) public class BearerTokenAuthorizationTest { @Test public void testBearerToken() { RestAssured.given().auth().oauth2(getAccessToken("alice", Set.of("user"))) .when().get("/api/users/me") .then() .statusCode(200) // The test endpoint returns the name extracted from the injected `SecurityIdentity` principal. .body("userName", equalTo("alice")); } private String getAccessToken(String userName, Set<String> groups) { return Jwt.preferredUserName(userName) .groups(groups) .issuer("https://server.example.com") .audience("https://service.example.com") .sign(); } }
quarkus-test-oidc-server
エクステンションには、JSON Web Key
(JWK
) 形式の署名 RSA 秘密鍵ファイルが含まれており、smallrye.jwt.sign.key.location
設定プロパティーでそのファイルを指します。引数なしの sign()
操作を使用してトークンに署名できます。
OidcWiremockTestResource
を使用して quarkus-oidc
service
アプリケーションをテストすると、通信チャネルも WireMock HTTP スタブに対してテストされるため、最高のカバレッジが提供されます。OidcWiremockTestResource
でまだサポートされていない WireMock スタブを使用してテストを実行する必要がある場合は、次の例に示すように、テストクラスに WireMockServer
インスタンスを注入できます。
OidcWiremockTestResource
は、Docker コンテナーに対して @QuarkusIntegrationTest
では機能しません。これは、WireMock サーバーがテストを実行する JVM で実行され、Quarkus アプリケーションを実行する Docker コンテナーからはアクセスできないためです。
package io.quarkus.it.keycloak; import static com.github.tomakehurst.wiremock.client.WireMock.matching; import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Test; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.oidc.server.OidcWireMock; import io.restassured.RestAssured; @QuarkusTest public class CustomOidcWireMockStubTest { @OidcWireMock WireMockServer wireMockServer; @Test public void testInvalidBearerToken() { wireMockServer.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect") .withRequestBody(matching(".*token=invalid_token.*")) .willReturn(WireMock.aResponse().withStatus(400))); RestAssured.given().auth().oauth2("invalid_token").when() .get("/api/users/me/bearer") .then() .statusCode(401) .header("WWW-Authenticate", equalTo("Bearer")); } }
1.1.15. OidcTestClient
Auth0
などの SaaS OIDC プロバイダーを使用していて、テスト (開発) ドメインに対してテストを実行したり、リモート Keycloak テストレルムに対してテストを実行したりする場合、quarkus.oidc.auth-server-url
がすでに設定されている場合は、OidcTestClient
を使用できます。
たとえば、次のような設定があるとします。
%test.quarkus.oidc.auth-server-url=https://dev-123456.eu.auth0.com/ %test.quarkus.oidc.client-id=test-auth0-client %test.quarkus.oidc.credentials.secret=secret
まず、WireMock セクションで説明されているものと同じ依存関係 quarkus-test-oidc-server
を追加します。
次に、次のようにテストコードを記述します。
package org.acme; import org.junit.jupiter.api.AfterAll; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; import java.util.Map; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.oidc.client.OidcTestClient; @QuarkusTest public class GreetingResourceTest { static OidcTestClient oidcTestClient = new OidcTestClient(); @AfterAll public static void close() { oidcTestClient.close(); } @Test public void testHelloEndpoint() { given() .auth().oauth2(getAccessToken("alice", "alice")) .when().get("/hello") .then() .statusCode(200) .body(is("Hello, Alice")); } private String getAccessToken(String name, String secret) { return oidcTestClient.getAccessToken(name, secret, Map.of("audience", "https://dev-123456.eu.auth0.com/api/v2/", "scope", "profile")); } }
このテストコードは、クライアント ID test-auth0-client
でアプリケーションを登録し、パスワード alice
でユーザー alice
を作成したテスト Auth0
ドメインから password
付与を使用してトークンを取得します。このようなテストが機能するには、テスト Auth0
アプリケーションで password
付与が有効になっている必要があります。このサンプルコードでは、追加のパラメーターを渡す方法も示しています。Auth0
の場合、これらは audience
と scope
パラメーターです。
1.1.15.1. Dev Services for Keycloak
Keycloak に対する結合テストを行う際に推奨される手法は、Dev Services for Keycloak です。Dev Services for Keycloak
が起動し、テストコンテナーを初期化します。次に、quarkus
レルムと quarkus-app
クライアント (secret
secret) を作成し、alice
(admin
および user
ロール) および bob
(user
ロール) ユーザーを追加します。これらのプロパティーはすべてカスタマイズできます。
まず、アクセストークンを取得するためのテストで使用できるユーティリティークラス io.quarkus.test.keycloak.client.KeycloakTestClient
を提供する次の依存関係を追加します。
Maven を使用:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-keycloak-server</artifactId> <scope>test</scope> </dependency>
Gradle を使用する場合:
testImplementation("io.quarkus:quarkus-test-keycloak-server")
次に、application.properties
設定ファイルを準備します。Dev Services for Keycloak
は quarkus.oidc.auth-server-url
を登録し、それを実行中のテストコンテナー quarkus.oidc.client-id=quarkus-app
および quarkus.oidc.credentials.secret=secret
を指すため、空の application.properties
ファイルから開始できます。
ただし、必要な quarkus-oidc
プロパティーをすでに設定している場合は、次の例に示すように、quarkus.oidc.auth-server-url
を `Dev Services for Keycloak` の prod
プロファイルに関連付けるだけでコンテナーを起動できます。
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
テストを実行する前にカスタムレルムファイルを Keycloak にインポートする必要がある場合は、次のように Dev Services for Keycloak
を設定します。
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.keycloak.devservices.realm-path=quarkus-realm.json
最後に、次の例に示すように、JVM モードで実行されるテストを記述します。
JVM モードで実行されたテストの例:
package org.acme.security.openid.connect; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.keycloak.client.KeycloakTestClient; import io.restassured.RestAssured; import org.junit.jupiter.api.Test; @QuarkusTest public class BearerTokenAuthenticationTest { KeycloakTestClient keycloakClient = new KeycloakTestClient(); @Test public void testAdminAccess() { RestAssured.given().auth().oauth2(getAccessToken("alice")) .when().get("/api/admin") .then() .statusCode(200); RestAssured.given().auth().oauth2(getAccessToken("bob")) .when().get("/api/admin") .then() .statusCode(403); } protected String getAccessToken(String userName) { return keycloakClient.getAccessToken(userName); } }
ネイティブモードで実行されたテストの例:
package org.acme.security.openid.connect; import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthenticationTest { }
Dev Services for Keycloak の初期化と設定の詳細は、Dev Services for Keycloak ガイドを参照してください。
1.1.15.2. ローカル公開鍵
次の例に示すように、ローカルのインライン公開鍵を使用して quarkus-oidc
service
アプリケーションをテストできます。
quarkus.oidc.client-id=test quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB smallrye.jwt.sign.key.location=/privateKey.pem
JWT トークンを生成するには、main
Quarkus リポジトリーの integration-tests/oidc-tenancy
から privateKey.pem
をコピーし、前の WireMock セクションのものと同様のテストコードを使用します。必要に応じて、独自のテストキーを使用することもできます。
このアプローチでは、WireMock アプローチと比較してカバレッジが制限されます。たとえば、リモート通信コードはカバーされません。
1.1.15.3. TestSecurity アノテーション
@TestSecurity
および @OidcSecurity
アノテーションを使用して、次のインジェクションのいずれか 1 つまたは 3 つすべてに依存する service
アプリケーションエンドポイントコードをテストできます。
-
JsonWebToken
-
UserInfo
-
OidcConfigurationMetadata
まず、次の依存関係を追加します。
Maven を使用:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-oidc</artifactId> <scope>test</scope> </dependency>
Gradle を使用する場合:
testImplementation("io.quarkus:quarkus-test-security-oidc")
次の例に示すようにテストコードを記述します。
import static org.hamcrest.Matchers.is; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.quarkus.test.security.oidc.Claim; import io.quarkus.test.security.oidc.ConfigMetadata; import io.quarkus.test.security.oidc.OidcSecurity; import io.quarkus.test.security.oidc.OidcConfigurationMetadata; import io.quarkus.test.security.oidc.UserInfo; import io.restassured.RestAssured; @QuarkusTest @TestHTTPEndpoint(ProtectedResource.class) public class TestSecurityAuthTest { @Test @TestSecurity(user = "userOidc", roles = "viewer") public void testOidc() { RestAssured.when().get("test-security-oidc").then() .body(is("userOidc:viewer")); } @Test @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(claims = { @Claim(key = "email", value = "user@gmail.com") }, userinfo = { @UserInfo(key = "sub", value = "subject") }, config = { @ConfigMetadata(key = "issuer", value = "issuer") }) public void testOidcWithClaimsUserInfoAndMetadata() { RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then() .body(is("userOidc:viewer:user@gmail.com:subject:issuer")); } }
このコード例で使用されている ProtectedResource
クラスは、次のようになります。
import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.OidcConfigurationMetadata; import io.quarkus.oidc.UserInfo; import io.quarkus.security.Authenticated; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/service") @Authenticated public class ProtectedResource { @Inject JsonWebToken accessToken; @Inject UserInfo userInfo; @Inject OidcConfigurationMetadata configMetadata; @GET @Path("test-security-oidc") public String testSecurityOidc() { return accessToken.getName() + ":" + accessToken.getGroups().iterator().next(); } @GET @Path("test-security-oidc-claims-userinfo-metadata") public String testSecurityOidcWithClaimsUserInfoMetadata() { return accessToken.getName() + ":" + accessToken.getGroups().iterator().next() + ":" + accessToken.getClaim("email") + ":" + userInfo.getString("sub") + ":" + configMetadata.get("issuer"); } }
常に @TestSecurity
アノテーションを使用する必要があります。その user
プロパティーは JsonWebToken.getName()
として返され、その roles
プロパティーは JsonWebToken.getGroups()
として返されます。@OidcSecurity
アノテーションはオプションであり、これを使用して追加のトークン要求と UserInfo
および OidcConfigurationMetadata
プロパティーを設定できます。さらに、quarkus.oidc.token.issuer
プロパティーが設定されている場合は、OidcConfigurationMetadata
issuer
プロパティー値として使用されます。
不透明なトークンを使用する場合は、次のコード例に示すようにテストできます。
import static org.hamcrest.Matchers.is; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.quarkus.test.security.oidc.OidcSecurity; import io.quarkus.test.security.oidc.TokenIntrospection; import io.restassured.RestAssured; @QuarkusTest @TestHTTPEndpoint(ProtectedResource.class) public class TestSecurityAuthTest { @Test @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(introspectionRequired = true, introspection = { @TokenIntrospection(key = "email", value = "user@gmail.com") } ) public void testOidcWithClaimsUserInfoAndMetadata() { RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then() .body(is("userOidc:viewer:userOidc:viewer")); } }
このコード例で使用されている ProtectedResource
クラスは、次のようになります。
import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.TokenIntrospection; import io.quarkus.security.Authenticated; import io.quarkus.security.identity.SecurityIdentity; @Path("/service") @Authenticated public class ProtectedResource { @Inject SecurityIdentity securityIdentity; @Inject TokenIntrospection introspection; @GET @Path("test-security-oidc-opaque-token") public String testSecurityOidcOpaqueToken() { return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next() + ":" + introspection.getString("username") + ":" + introspection.getString("scope") + ":" + introspection.getString("email"); } }
@TestSecurity
、user
、および roles
属性は、TokenIntrospection
、username
、および scope
プロパティーとして使用できます。io.quarkus.test.security.oidc.TokenIntrospection
を使用して、email
などの追加のイントロスペクション応答プロパティーを追加します。
@TestSecurity
と @OidcSecurity
は、次の例に示すように、メタアノテーションで組み合わせることができます。
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(introspectionRequired = true, introspection = { @TokenIntrospection(key = "email", value = "user@gmail.com") } ) public @interface TestSecurityMetaAnnotation { }
これは、複数のテスト方法で同じセキュリティー設定セットを使用する必要がある場合に特に便利です。
1.1.16. ログエラーの確認
トークン検証エラーの詳細を確認するには、io.quarkus.oidc.runtime.OidcProvider
と TRACE
レベルのロギングを有効にします。
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
OidcProvider
クライアント初期化エラーの詳細を確認するには、次のように io.quarkus.oidc.runtime.OidcRecorder
と TRACE
レベルのロギングを有効にします。
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
1.1.17. OIDC プロバイダーへの外部および内部アクセス
OIDC プロバイダーおよびその他のエンドポイントの外部アクセス可能なトークンは、quarkus.oidc.auth-server-url
内部 URL を基準として自動検出された URL または設定された URL とは異なる HTTP(S) URL を持つ場合があります。たとえば、SPA が外部トークンエンドポイントアドレスからトークンを取得し、それをベアラートークンとして Quarkus に送信するとします。その場合、エンドポイントは発行者の検証の失敗を報告する可能性があります。
このような場合、Keycloak を使用する場合は、KEYCLOAK_FRONTEND_URL
システムプロパティーを外部からアクセス可能なベース URL に設定して起動します。他の OIDC プロバイダーを使用する場合は、プロバイダーのドキュメントを参照してください。
1.1.18. client-id
プロパティーの使用
quarkus.oidc.client-id
プロパティーは、現在のベアラートークンをリクエストした OIDC クライアントを識別します。OIDC クライアントは、ブラウザーで実行される SPA アプリケーション、または Quarkus service
アプリケーションにアクセストークンを伝播する Quarkus web-app
の機密クライアントアプリケーションです。
service
アプリケーションがトークンをリモートでイントロスペクトすることが予想される場合、このプロパティーは必須です。これは、不透明トークンの場合は常に該当します。このプロパティーは、ローカル JSON Web Token (JWT) 検証の場合のみのオプションです。
エンドポイントがリモートイントロスペクションエンドポイントへのアクセスを必要としない場合でも、quarkus.oidc.client-id
プロパティーを設定することを推奨します。これは、client-id
が設定されている場合、それを使用してトークン audience を検証できるためです。また、トークンの検証が失敗した場合にもログに含まれるため、特定のクライアントに発行されたトークンの追跡可能性が向上し、より長い期間にわたる分析が可能になります。
たとえば、OIDC プロバイダーがトークン audience を設定する場合は、次の設定パターンを検討してください。
# Set client-id quarkus.oidc.client-id=quarkus-app # Token audience claim must contain 'quarkus-app' quarkus.oidc.token.audience=${quarkus.oidc.client-id}
quarkus.oidc.client-id
を設定したが、エンドポイントが OIDC プロバイダーエンドポイントの 1 つへのリモートアクセスを必要としない場合 (イントロスペクション、トークンの取得など) は、quarkus.oidc.credentials
または同様のプロパティーを使用してクライアントシークレットを設定しないでください (このプロパティーは使用されないため)。
Quarkus web-app
アプリケーションには常に quarkus.oidc.client-id
プロパティーが必要です。