The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

OpenID Connect (OIDC) ベアラートークン認証

Quarkus OpenID Connect(OIDC)エクステンションを使用することで、ベアラートークン認証を使って、アプリケーション内のJakarta REST(旧称JAX-RS)エンドポイントへのHTTPアクセスを保護できます。

Quarkusのベアラートークン認証メカニズムの概要

Quarkusは、Quarkus OpenID Connect(OIDC)エクステンションを通じて、ベアラートークン認証メカニズムをサポートしています。

ベアラートークンは、 Keycloak などのOIDCやOAuth2.0に準拠した認可サーバーで発行されます。

ベアラートークン認証は、ベアラートークンの存在と有効性に基づいて HTTP リクエストを認可するプロセスです。 ベアラートークンは、呼び出しの主体に関する情報を提供します。この情報は、HTTP リソースにアクセスできるかどうかを判断するために使用されます。

以下の図に、Quarkusのベアラートークン認証メカニズムの概要を示します:

Bearer token authentication
Figure 1. Quarkus のベアラー認証メカニズムとシングルページアプリケーション
  1. Quarkus サービスが、OIDC プロバイダーから検証キーを取得します。 検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。

  2. Quarkus ユーザーが、シングルページアプリケーション (SPA) にアクセスします。

  3. シングルページアプリケーションが、認可コードフローを使用してユーザーを認証し、OIDC プロバイダーからトークンを取得します。

  4. シングルページアプリケーションが、アクセストークンを使用して、Quarkus サービスからサービスデータを取得します。

  5. Quarkus サービスが、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のクレームをチェックします。トークンが有効であればリクエストの続行を許可し、シングルページアプリケーションにサービスレスポンスを返します。

  6. シングルページアプリケーションが、Quarkus ユーザーに同じデータを返します。

Bearer token authentication
Figure 2. Quarkus のベアラートークン認証メカニズムとJava またはコマンドラインクライアント
  1. Quarkus サービスが、OIDC プロバイダーから検証キーを取得します。 検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。

  2. クライアントが、 client_credentials かパスワードグラントを使用して、OIDC プロバイダーからアクセストークンを取得します。client_credentials には、クライアント ID とシークレットが必要です。パスワードグラントには、クライアント ID、シークレット、ユーザー名、およびパスワードが必要です。

  3. クライアントがアクセストークンを使用して、Quarkus サービスからサービスデータを取得します。

  4. Quarkus サービスが、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のクレームをチェックします。トークンが有効であればリクエストの続行を許可し、サービスレスポンスをクライアントに返します。

OIDC 認可コードフローを使用してユーザーを認証および認可する必要がある場合は、Quarkus の Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム ガイドを参照してください。 また、Keycloak とベアラートークンを使用する場合は、Quarkus Keycloak を使用した認可の一元化 ガイドを参照してください。

OIDC ベアラートークン認証を使用してサービスアプリケーションを保護する方法については、次のチュートリアルを参照してください。

マルチテナントをサポートする方法については、Quarkus OpenID Connect マルチテナンシーの使用 ガイドを参照してください。

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 の「JWT RBAC の使用」ガイドの サポートされている注入スコープ セクションを参照してください。

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 属性として注入またはアクセスできます。

quarkus.oidc.authentication.user-info-required は、次のいずれかの条件が満たされた場合に自動的に有効になります。

  • quarkus.oidc.roles.sourceuserinfo に設定されている場合、または quarkus.oidc.token.verify-access-token-with-user-infotrue に設定されている場合、または quarkus.oidc.authentication.id-token-requiredfalse に設定されている場合。このような場合、現在の OIDC テナントが UserInfo エンドポイントをサポートしている必要があります。

  • io.quarkus.oidc.UserInfo インジェクションポイントが検出された場合。ただし、有効になるのは、現在の OIDC テナントが UserInfo エンドポイントをサポートしている場合だけです。

設定メタデータ

現在のテナントの検出された OpenID Connect 設定メタデータ は、 io.quarkus.oidc.OidcConfigurationMetadata で表され、 SecurityIdentity configuration-metadata 属性として注入またはアクセスできます。

エンドポイントがパブリックの場合、デフォルトのテナントの OidcConfigurationMetadata が注入されます。

トークンクレームと SecurityIdentity ロール

SecurityIdentity ロールは、次のように、検証済みの JWT アクセストークンからマッピングすることができます。

  • quarkus.oidc.roles.role-claim-path プロパティーが設定されており、一致する配列または文字列のクレームが見つかった場合、そのクレームからロールが抽出されます。 たとえば、 customrolescustomroles/arrayscope"http://namespace-qualified-custom-claim"/roles"http://namespace-qualified-roles" などです。

  • groups クレームが利用可能な場合は、その値が使用されます。

  • realm_access/roles または resource_access/client_id/roles (client_idquarkus.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 の「セキュリティーに関するヒントとコツ」ガイドの セキュリティーアイデンティティーのカスタマイズ セクションを参照してください。

また、HTTP セキュリティーポリシー を使用して、トークンクレームから作成された SecurityIdentity ロールをデプロイメント固有のロールにマップすることもできます。

トークンのスコープと SecurityIdentity の権限

SecurityIdentity 権限は、 io.quarkus.security.StringPermission の形式で、source of the roles のスコープパラメーターから、同じクレームセパレーターを使用してマッピングされます。

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;
        }
    }
}
1 OpenID Connect スコープ email を持つリクエストのみにアクセスが許可されます。
2 読み取りアクセスは、 orders_read スコープを持つクライアントリクエストに制限されます。

io.quarkus.security.PermissionsAllowed アノテーションの詳細は、「Web エンドポイントの認可」ガイドの 権限アノテーション セクションを参照してください。

トークン検証およびイントロスペクション

トークンが JWT トークンの場合、デフォルトでは、OIDC プロバイダーの JWK エンドポイントから取得されたローカルの JsonWebKeySetJsonWebKey (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 メタデータ検出呼び出しが必要になることです。

io.quarkus.oidc.TokenIntrospection (シンプルな jakarta.json.JsonObject ラッパーオブジェクト) が作成されます。 このオブジェクトは、JWT または不透明トークンどちらかのイントロスペクションが成功した場合、 SecurityIdentity introspection 属性として注入またはアクセスできます。

トークンのイントロスペクションと UserInfo キャッシュ

不透明アクセストークンは、すべてリモートでイントロスペクトする必要があります。 場合によっては、JWT アクセストークンをイントロスペクトする必要もあります。 UserInfo も必要な場合は、OIDC プロバイダーへの後続のリモート呼び出しで同じアクセストークンが使用されます。 したがって、 UserInfo が必要で、現在のアクセストークンが不透明トークンである場合、そのトークンごとに 2 つのリモート呼び出しが行われます。1 つのリモート呼び出しはトークンをイントロスペクトするためのもので、もう 1 つは UserInfo を取得するためのものです。 トークンが JWT の場合は、トークンをイントロスペクトする必要がなければ、 UserInfo を取得するための 1 つのリモート呼び出しだけで済みます。

場合によっては、着信ベアラーまたはコードフローアクセストークンごとに最大 2 つのリモート呼び出しを行うコストが問題になります。

これが本番環境に当てはまる場合は、トークンイントロスペクションと UserInfo データを短期間 (たとえば 3 分から 5 分) キャッシュすることを検討してください。

quarkus-oidc は、 quarkus.oidc.TokenIntrospectionCache および quarkus.oidc.UserInfoCache インターフェイスを提供します。 これらは @ApplicationScoped キャッシュ実装に使用できます。次の例に示すように、 @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.TokenIntrospectionCachequarkus.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

デフォルトのキャッシュは、トークンをキーとして使用します。各エントリーには、 TokenIntrospectionUserInfo、またはその両方が含まれます。 最大で max-size 個のエントリーのみが保持されます。 新しいエントリーを追加するときにキャッシュがすでにいっぱいになっている場合は、期限切れのエントリーを 1 つ削除して領域を確保する試みが行われます。 さらに、クリーンアップタイマーを有効にすると、期限切れのエントリーが定期的にチェックされ、削除されます。

デフォルトのキャッシュ実装を試すことも、カスタムのキャッシュ実装を登録することもできます。

JSON Web トークンのクレーム検証

ベアラーの 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 に KEYCLOAK_FRONTEND_URL プロパティーを設定して、同じホストアドレスを使用してください。

  • マルチテナントデプロイメントで 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());
        }
    }
}

トークンの aud (audience) クレーム値を検証するために、 quarkus.oidc.token.audience プロパティーを使用することを検討してください。

Jose4j Validator

org.eclipse.microprofile.jwt.JsonWebToken が初期化される前に、カスタム Jose4j Validator を登録して、JWT クレーム検証プロセスをカスタマイズできます。 以下に例を示します。

package org.acme.security.openid.connect;

import static org.eclipse.microprofile.jwt.Claims.iss;

import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;

import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;

@Unremovable
@ApplicationScoped
public class IssuerValidator implements Validator { (1)

    @Override
    public String validate(JwtContext jwtContext) throws MalformedClaimException {
        if (jwtContext.getJwtClaims().hasClaim(iss.name())
                && "my-issuer".equals(jwtContext.getJwtClaims().getClaimValueAsString(iss.name()))) {
            return "wrong issuer"; (2)
        }
        return null; (3)
    }
}
1 すべての OIDC テナントの JWT トークンを検証するために、Jose4j Validator を登録します。
2 クレーム検証エラーの説明を返します。
3 この Validator がトークンを正常に検証したことを確認するために、 null を返します。
カスタム Validator を特定の OIDC テナントにのみバインドするには、 @quarkus.oidc.TenantFeature アノテーションを使用します。

シングルページアプリケーション

シングルページアプリケーション (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 type="importmap">
        {
            "imports": {
                "keycloak-js": "https://cdn.jsdelivr.net/npm/keycloak-js@26.0.7/lib/keycloak.js"
            }
        }
    </script>
    <script type="module">
        import Keycloak from "keycloak-js";

        const keycloak = new Keycloak({
            url: 'http://localhost:8180',
            realm: 'quarkus',
            clientId: 'quarkus-app'
        });

        await keycloak.init({onLoad: 'login-required'}).then(function () {
            console.log('User is now authenticated.');
        }).catch(function () {
            console.log('User is NOT authenticated.');
        });

        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();
                });
            });
        }

        let button = document.getElementById('ajax-request');
        button.addEventListener('click', makeAjaxRequest);
    </script>
</head>
<body>
    <button id="ajax-request">Request</button>
</body>
</html>

この SPA Keycloak の例で認証を有効にするには、 クライアント認証 を無効にし、 Web origins を次のように設定します。 http://localhost:8080 .これらの設定により、KeycloakのCORSポリシーがQuarkusアプリケーションと通信できるようになります。このコードでは、Keycloakと統合されたQuarkusのシングルページアプリケーションを構築する例を示します。Keycloakを統合したシングルページアプリケーションの作成の詳細については、 Keycloak JavaScriptアダプターの 公式 ドキュメントを 参照してください。

クロスオリジンリソース共有

別のドメインで実行されているシングルページアプリケーションから OIDC service アプリケーションを使用する予定の場合は、クロスオリジンリソース共有 (CORS) を設定する必要があります。 詳細は、「クロスオリジンリソース共有」ガイドの CORS フィルター セクションを参照してください。

プロバイダーエンドポイント設定

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

トークンの伝播

ダウンストリームサービスへのベアラーアクセストークンの伝播については、Quarkus の「OpenID Connect (OIDC) および OAuth2 クライアントとフィルターのリファレンス」ガイドの トークンの伝播 セクションを参照してください。

JWT トークン証明書チェーン

場合によっては、JWT ベアラートークンに x5c ヘッダーがあり、このヘッダーは X509 証明書チェーンを表します。この証明書チェーンのリーフ証明書には、トークンの署名の検証に使用する必要がある公開鍵が含まれています。 まず証明書チェーンを検証してから、この公開鍵を受け入れて署名を検証する必要があります。 証明書チェーンの検証にはいくつかの手順が含まれます。

  1. ルート証明書以外のすべての証明書が親証明書によって署名されていることを確認します。

  2. チェーンのルート証明書もトラストストアにインポートされていることを確認します。

  3. チェーンのリーフ証明書を検証します。リーフ証明書の共通名が設定されている場合は、チェーンのリーフ証明書の共通名がその共通名に一致する必要があります。それ以外の場合は、1 つ以上のカスタム TokenCertificateValidator 実装が登録されていない限り、チェーンのリーフ証明書もトラストストアで使用可能である必要があります。

  4. quarkus.oidc.TokenCertificateValidator を使用すると、カスタム証明書チェーン検証手順を追加できます。これは、証明書チェーンを持つトークンを要求するすべてのテナント、または @quarkus.oidc.TenantFeature アノテーションを使用して特定の OIDC テナントにバインドされたすべてのテナントで使用できます。

たとえば、 quarkus.oidc.TokenCertificateValidator を使用せずにトークンの証明書チェーンを検証するように Quarkus OIDC を設定する方法は次のとおりです。

quarkus.oidc.certificate-chain.trust-store-file=truststore-rootcert.p12 (1)
quarkus.oidc.certificate-chain.trust-store-password=storepassword
quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com (2)
1 トラストストアに証明書チェーンのルート証明書が含まれている必要があります。
2 証明書チェーンのリーフ証明書の共通名は www.quarkusio.com にする必要があります。このプロパティーが設定されていない場合は、1 つ以上のカスタム TokenCertificateValidator 実装が登録されていない限り、トラストストアに証明書チェーンのリーフ証明書が含まれている必要があります。

カスタムの quarkus.oidc.TokenCertificateValidator を登録することで、カスタムの証明書チェーン検証ステップを追加できます。次に例を示します。

package io.quarkus.it.keycloak;

import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenCertificateValidator;
import io.quarkus.oidc.runtime.TrustStoreUtils;
import io.vertx.core.json.JsonObject;

@ApplicationScoped
@Unremovable
public class BearerGlobalTokenChainValidator implements TokenCertificateValidator {

    @Override
    public void validate(OidcTenantConfig oidcConfig, List<X509Certificate> chain, String tokenClaims) throws CertificateException {
        String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1));
        JsonObject claims = new JsonObject(tokenClaims);
        if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { (1)
            throw new CertificateException("Invalid root certificate");
        }
    }
}
1 証明書チェーンのルート証明書がカスタム JWT トークンのクレームにバインドされていることを確認します。

OIDC プロバイダーのクライアント認証

quarkus.oidc.runtime.OidcProviderClient は、OIDC プロバイダーへのリモートリクエストが必要な場合に使用されます。 ベアラートークンのイントロスペクションが必要な場合は、 OidcProviderClient が OIDC プロバイダーに対して認証する必要があります。 サポートされている認証オプションの詳細は、Quarkus の「Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム」ガイドの OIDC プロバイダーのクライアント認証 セクションを参照してください。

テスト

Keycloak 認証 を必要とする Quarkus OIDC サービスエンドポイントをテストする必要がある場合は、Keycloak 認証のテスト セクションに従ってください。

次の依存関係をテストプロジェクトに追加することで、テストを開始できます。

pom.xml
<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>
build.gradle
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")

Dev Services for Keycloak

Keycloak に対する結合テストには、Dev Services for Keycloak を使用することを推奨します。 Dev Services for Keycloak は、テストコンテナーを起動して初期化します。次に、 quarkus レルムと quarkus-app クライアント (secret シークレット) を作成し、 alice (admin および user ロール) および bob (user ロール) ユーザーを追加します。これらのプロパティーはすべてカスタマイズできます。

まず、次の依存関係を追加します。この依存関係は、アクセストークンを取得するテストで使用できるユーティリティークラス io.quarkus.test.keycloak.client.KeycloakTestClient を提供します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-keycloak-server</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-keycloak-server")

次に、 application.properties 設定ファイルを準備します。 空の application.properties ファイルから開始できます。 Dev Services for Keycloakquarkus.oidc.auth-server-url を登録し、その参照先として実行中のテストコンテナー、 quarkus.oidc.client-id=quarkus-app、および quarkus.oidc.credentials.secret=secret を設定するためです。

ただし、必要な quarkus-oidc プロパティーをすでに設定している場合は、次の例に示すように、 quarkus.oidc.auth-server-urlDev Services for Keycloakprod プロファイルに関連付けるだけで、コンテナーを起動できます。

%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 ガイドを参照してください。

WireMock

テストプロジェクトに以下の依存関係を追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-oidc-server</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-oidc-server")

REST テストエンドポイントを準備し、 application.properties を設定します。 以下に例を示します。

# keycloak.url is set by OidcWiremockTestResource
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/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 で実行されるためです。その 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"));
    }
}

OidcTestClient

SaaS OIDC プロバイダー (Auth0 など) を使用していて、テスト (開発) ドメインに対してテストを実行したり、リモート 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 パラメーターです。

OIDC DevService のテスト

また、 OidcTestClient を使用して、OIDC 用 Dev Services でサポートされている Quarkus エンドポイントをテストすることもできます。 application.properties ファイルでの設定は必要ありません。Quarkus が OidcTestClient を設定します。

package org.acme;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;

@QuarkusTest
public class GreetingResourceTest {

    static final OidcTestClient oidcTestClient = new OidcTestClient();

    @AfterAll
    public static void close() {
        oidcTestClient.close();
    }

    @Test
    public void testHelloEndpoint() {
        String accessToken = oidcTestClient.getAccessToken("alice", "alice");
        given()
          .auth().oauth2(accessToken)
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("Hello, Alice"));
    }

}

KeycloakTestResourceLifecycleManager

Keycloak との結合テストには KeycloakTestResourceLifecycleManager を使用することもできます。

KeycloakTestResourceLifecycleManager を使用する特別な要件がない限り、Keycloak との結合テストには、 KeycloakTestResourceLifecycleManager ではなく Dev Services for Keycloak を使用してください。

まず、以下の依存関係を追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-keycloak-server</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-keycloak-server")

これは、Keycloak コンテナーを起動する io.quarkus.test.common.QuarkusTestResourceLifecycleManager の実装である`io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager` を提供します。

Maven Surefire プラグインを次のように設定します。または、ネイティブイメージテストの maven.failsafe.plugin と同様に設定します。

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <!-- Or, alternatively, configure 'keycloak.version' -->
            <keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
            <!--
              Disable HTTPS if required:
              <keycloak.use.https>false</keycloak.use.https>
            -->
        </systemPropertyVariables>
    </configuration>
</plugin>

REST テストエンドポイントを準備し、次の例に示すように application.properties を設定します。

# keycloak.url is set by KeycloakTestResourceLifecycleManager
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials=secret
quarkus.oidc.application-type=service

最後にテストコードを記述します。 以下に例を示します。

import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken;
import static org.hamcrest.Matchers.equalTo;

import org.junit.jupiter.api.Test;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;

@QuarkusTest
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class BearerTokenAuthorizationTest {

    @Test
    public void testBearerToken() {
        RestAssured.given().auth().oauth2(getAccessToken("alice"))
            .when().get("/api/users/preferredUserName")
            .then()
            .statusCode(200)
            // The test endpoint returns the name extracted from the injected SecurityIdentity Principal
            .body("userName", equalTo("alice"));
    }

}
概要

上記の例では、 KeycloakTestResourceLifecycleManageraliceadmin の 2 人のユーザーを登録します。 デフォルトでは、* ユーザー alice には user ロールがあります。このロールは、 keycloak.token.user-roles システムプロパティーを使用してカスタマイズできます。 * ユーザー admin には useradmin の両方のロールがあります。これらのロールは、 keycloak.token.admin-roles システムプロパティーを使用してカスタマイズできます。

デフォルトでは、 KeycloakTestResourceLifecycleManager は HTTPS を使用して Keycloak インスタンスを初期化します。これは keycloak.use.https=false を使用して無効にできます。 デフォルトのレルム名は quarkus で、クライアント ID は quarkus-service-app です。 これらの値をカスタマイズする場合は、 keycloak.realm および keycloak.service.client システムプロパティーを設定します。

ローカル公開鍵

次の例に示すように、 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 方法と比較してカバレッジが制限されます。 たとえば、リモート通信コードはカバーされません。

TestSecurity アノテーション

@TestSecurity および @OidcSecurity アノテーションを使用して、 service アプリケーションエンドポイントのコードをテストできます。このコードは、次の注入のいずれか 1 つまたは 3 つすべてに依存します。

  • JsonWebToken

  • UserInfo

  • OidcConfigurationMetadata

まず、以下の依存関係を追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-security-oidc</artifactId>
    <scope>test</scope>
</dependency>
build.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.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-opaque-token").then()
                .body(is("userOidc:viewer:userOidc:viewer:user@gmail.com"));
    }

}

このコード例で使用されている 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");
    }
}

@TestSecurityuser、および roles 属性は、 TokenIntrospectionusername、および 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 {

    }

これは、複数のテスト方法で同じセキュリティー設定セットを使用する必要がある場合に特に便利です。

ログでのエラーの確認

トークン検証エラーの詳細を確認するには、 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

OIDC プロバイダーへの外部および内部アクセス

OIDC プロバイダーおよびその他のエンドポイントの外部からアクセス可能なトークンは、自動検出された URL や quarkus.oidc.auth-server-url 内部 URL を基準に設定された URL とは異なる HTTP(S) URL を持っている可能性があります。 たとえば、SPA が外部トークンエンドポイントアドレスからトークンを取得し、それをベアラートークンとして Quarkus に送信するとします。 その場合、エンドポイントから発行者の検証の失敗が報告される可能性があります。

このような場合に Keycloak を使用する場合は、 KEYCLOAK_FRONTEND_URL システムプロパティーを外部からアクセス可能なベース URL に設定して Keycloak を起動してください。 他の OIDC プロバイダーを使用する場合は、プロバイダーのドキュメントを参照してください。

`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 が設定されている場合、それを使用してトークンオーディエンスを検証できるためです。 また、トークンの検証が失敗した場合にも client-id がログに含まれるため、特定のクライアントに発行されたトークンのトレーサビリティーが向上し、より長い期間にわたる分析が可能になります。

たとえば、OIDC プロバイダーでトークンオーディエンスを設定する場合は、次の設定パターンを検討してください。

# 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 プロバイダーエンドポイントへのリモートアクセスがエンドポイントに必要ない場合 (イントロスペクション、トークンの取得など)、 quarkus.oidc.credentials または同様のプロパティーを使用してクライアントシークレットを設定しないでください。その場合、クライアントシークレットは使用されないためです。

Quarkus web-app アプリケーションには、常に quarkus.oidc.client-id プロパティーが必要です。

送信者制限アクセストークン

Demonstrating Proof of Possession (DPoP)

RFC9449は 、アクセストークンを現在のクライアントに暗号的にバインドし、アクセストークンの紛失やリプレイを防ぐためのDemonstrating Proof of Possession(DPoP)メカニズムについて記述しています。

シングル・ページ・アプリケーション(SPA)のパブリック・クライアントはDPoPプルーフ・トークンを生成し、DPoPプルーフに暗号学的にバインドされたアクセストークンの取得と提出に使用します。

QuarkusでDPoPサポートを有効にするには、1つのプロパティが必要です。

例:

quarkus.oidc.auth-server-url=${your_oidc_provider_url}
quarkus.oidc.token.authorization-scheme=dpop (1)
1 HTTP Authorization DPoP スキーム値を使用してアクセストークンが提供されることを要求します。

このようなトークンを受け入れた後、Quarkusは DPoPトークンの 完全な 検証プロセスを 行います。

カスタムDPoP nonceプロバイダのサポートは将来提供されるかもしれません。

相互 TLS トークンバインディング

RFC8705 では、アクセストークンを Mutual TLS (mTLS) クライアント認証証明書にバインドするメカニズムについて説明しています。 クライアント証明書の SHA256 サムプリントが JWT トークンまたはトークンイントロスペクション確認 x5t#S256 証明書サムプリントと一致する必要があります。

たとえば、 RFC8705JWT 証明書のサムプリントの確認方法 および トークンイントロスペクションの確認方法 セクションを参照してください。

MTLS トークンバインディングは キーの所有者 の概念をサポートしており、現在のアクセストークンがこのトークンを提示する現在の認証済みクライアントに発行されたことを確認するために使用できます。

mTLS と OIDC ベアラー認証メカニズムの両方を使用する場合、Quarkus エンドポイントと Quarkus OIDC で mTLS の使用を要求するように設定した後、強制的にアクセストークンが単一のプロパティーで証明書にバインドされるように指定できます。

例:

quarkus.oidc.auth-server-url=${your_oidc_provider_url}
quarkus.oidc.token.binding.certificate=true (1)
quarkus.oidc.tls.tls-configuration-name=oidc-client-tls (2)

quarkus.tls.oidc-client-tls.key-store.p12.path=target/certificates/oidc-client-keystore.p12 (2)
quarkus.tls.oidc-client-tls.key-store.p12.password=password
quarkus.tls.oidc-client-tls.trust-store.p12.path=target/certificates/oidc-client-truststore.p12
quarkus.tls.oidc-client-tls.trust-store.p12.password=password

quarkus.http.tls-configuration-name=oidc-server-mtls (3)
quarkus.tls.oidc-server-mtls.key-store.p12.path=target/certificates/oidc-keystore.p12
quarkus.tls.oidc-server-mtls.key-store.p12.password=password
quarkus.tls.oidc-server-mtls.trust-store.p12.path=target/certificates/oidc-server-truststore.p12
quarkus.tls.oidc-server-mtls.trust-store.p12.password=password
1 ベアラーアクセストークンをクライアント証明書にバインドする必要があることを必須にします。
2 Quarkus OIDC の TLS レジストリー設定により、MTLS 経由で OIDC プロバイダーと通信できるようになります。
3 外部クライアントが MTLS 経由での Quarkus エンドポイントの認証を必須にする TLS レジストリー設定

上記の設定を使用すると、OIDC ベアラートークンをクライアント証明書にバインドすることが必須にできます。

次に、mTLS と OIDC ベアラーセキュリティーアイデンティティーの両方にアクセスする必要がある場合は、 quarkus.http.auth.inclusive=true を使用して 統合認証 を有効にすることを検討してください。

次のようにして、MTLS と OIDC の両方のセキュリティーアイデンティティーにアクセスできるようになりました。

package io.quarkus.it.oidc;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.security.Authenticated;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.SecurityIdentity;

@Path("/service")
@Authenticated
public class OidcMtlsEndpoint {

    @Inject
    SecurityIdentity mtlsIdentity; (1)

    @Inject
    JsonWebToken oidcAccessToken; (2)

    @GET
    public String getIdentities() {
        var cred = identity.getCredential(CertificateCredential.class).getCertificate();
        return "Identities: " + cred.getSubjectX500Principal().getName().split(",")[0]
                + ", " + accessToken.getName();
    }
}
1 mTLS が使用され、統合認証が有効になっている場合、 SecurityIdentity は常にプライマリー mTLS 認証を表します。
2 統合認証を有効にするには、登録されているすべてのメカニズムでセキュリティーアイデンティティーを生成する必要があるため、OIDC セキュリティーアイデンティティーも利用できます。

HTTP リクエストが完了した後の認証

場合によっては、アクティブな HTTP リクエストコンテキストが存在しない場合に、特定のトークンの SecurityIdentity を作成する必要があります。 quarkus-oidc エクステンションは、トークンを SecurityIdentity インスタンスに変換するための io.quarkus.oidc.TenantIdentityProvider を提供します。 たとえば、HTTP リクエストが完了した後にトークンを検証する必要がある状況の 1 つとして、Vert.x イベントバス を使用してメッセージを処理する場合があります。 以下の例では、さまざまな CDI リクエストコンテキスト内で 'product-order' メッセージを使用しています。 したがって、注入された SecurityIdentity は、検証されたアイデンティティーを正しく表さず、匿名になります。

package org.acme.quickstart.oidc;

import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;

import jakarta.inject.Inject;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import io.vertx.core.eventbus.EventBus;

@Path("order")
public class OrderResource {

    @Inject
    EventBus eventBus;

    @POST
    public void order(String product, @HeaderParam(AUTHORIZATION) String bearer) {
        String rawToken = bearer.substring("Bearer ".length()); (1)
        eventBus.publish("product-order", new Product(product, rawToken));
    }

    public static class Product {
         public String product;
         public String customerAccessToken;
         public Product() {
         }
         public Product(String product, String customerAccessToken) {
             this.product = product;
             this.customerAccessToken = customerAccessToken;
         }
    }
}
1 この時点では、プロアクティブ認証が無効な場合、トークンは検証されません。
package org.acme.quickstart.oidc;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.ConsumeEvent;
import io.smallrye.common.annotation.Blocking;

@ApplicationScoped
public class OrderService {

    @Tenant("tenantId")
    @Inject
    TenantIdentityProvider identityProvider;

    @Inject
    TenantIdentityProvider defaultIdentityProvider; (1)

    @Blocking
    @ConsumeEvent("product-order")
    void processOrder(OrderResource.Product product) {
        AccessTokenCredential tokenCredential = new AccessTokenCredential(product.customerAccessToken);
        SecurityIdentity securityIdentity = identityProvider.authenticate(tokenCredential).await().indefinitely(); (2)
        ...
    }

}
1 デフォルトのテナントの場合、 Tenant 修飾子は任意です。
2 トークンの検証を実行し、トークンを SecurityIdentity に変換します。

HTTP リクエスト中にプロバイダーが使用される場合、テナント設定を解決できます。 OpenID Connect マルチテナンシーの使用 ガイドの説明を参照してください。 ただし、アクティブな HTTP リクエストがない場合は、 io.quarkus.oidc.Tenant 修飾子を使用してテナントを明示的に選択する必要があります。

動的テナント設定解決 は現在サポートされていません。 動的テナントを必要とする認証は失敗します。

OIDC リクエストフィルター

1 つ以上の OidcRequestFilter 実装を登録すると、Quarkus から OIDC プロバイダーに対して行われた OIDC リクエストをフィルタリングできます。この実装により、新しいリクエストヘッダーを更新または追加したり、リクエストをログに記録したりできます。 詳細は、OIDC リクエストフィルター を参照してください。

OIDC レスポンスフィルター

1 つ以上の OidcResponseFilter 実装を登録することで、OIDC プロバイダーからのレスポンスをフィルタリングできます。これにより、レスポンスのステータス、ヘッダー、本文をチェックして、ログに記録したり、その他のアクションを実行したりできます。

すべての OIDC レスポンスをインターセプトする単一のフィルターを使用することも、 @OidcEndpoint アノテーションを使用してこのフィルターを特定のエンドポイントレスポンスにのみ適用することもできます。例:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.logging.Log;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcUtils;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.DISCOVERY) (1)
public class DiscoveryEndpointResponseFilter implements OidcResponseFilter {

    @Override
    public void filter(OidcResponseContext rc) {
        String contentType = rc.responseHeaders().get("Content-Type"); (2)
        if (contentType.equals("application/json") {
            String tenantId = rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE); (3)
            String metadata = rc.responseBody().toString(); (4)
            Log.debugf("Tenant %s OIDC metadata: %s", tenantId, metadata);
        }
    }
}
1 このフィルターを、OIDC 検出エンドポイントのみを対象とするリクエストに制限します。
2 レスポンスの Content-Type ヘッダーを確認します。
3 テナント ID を取得するには、 OidcRequestContextProperties リクエストプロパティーを使用します。
4 レスポンスデータを文字列として取得します。

プログラムによる OIDC のスタートアップ

OIDC テナントは、次の例のようにプログラムで作成できます。

package io.quarkus.it.oidc;

import io.quarkus.oidc.Oidc;
import jakarta.enterprise.event.Observes;

public class OidcStartup {

    void observe(@Observes Oidc oidc) {
        oidc.createServiceApp("http://localhost:8180/realms/quarkus");
    }

}

上記のコードは、 application.properties ファイル内の次の設定とプログラム的に同等です。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus

さらに多くの OIDC テナントプロパティーを設定する必要がある場合は、次の例のように OidcTenantConfig ビルダーを使用します。

package io.quarkus.it.oidc;

import io.quarkus.oidc.Oidc;
import io.quarkus.oidc.OidcTenantConfig;
import jakarta.enterprise.event.Observes;

public class OidcStartup {

    void createDefaultTenant(@Observes Oidc oidc) {
        var defaultTenant = OidcTenantConfig
                .authServerUrl("http://localhost:8180/realms/quarkus")
                .token().requireJwtIntrospectionOnly().end()
                .build();
        oidc.create(defaultTenant);
    }
}

複数のテナントが関係するより複雑な設定については、OpenID Connect マルチテナンシーガイドの マルチテナントアプリケーション用のプログラムによる OIDC の起動 セクションを参照してください。

関連コンテンツ