検索

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

download PDF

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 のベアラートークン認証メカニズム

ベアラートークン認証
  1. Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
  2. Quarkus ユーザーは、シングルページアプリケーション (SPA) にアクセスします。
  3. 単一ページアプリケーションは、Authorization Code Flow を使用してユーザーを認証し、OIDC プロバイダーからトークンを取得します。
  4. シングルページアプリケーションは、アクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
  5. Quarkus サービスは、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のリクエストをチェックします。トークンが有効な場合はリクエストを続行できるようにし、シングルページアプリケーションにサービス応答を返します。
  6. シングルページアプリケーションは、Quarkus ユーザーに同じデータを返します。

図1.2 Java またはコマンドラインクライアントを使用した Quarkus のベアラートークン認証メカニズム

ベアラートークン認証
  1. Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
  2. クライアントが、client_credentials かパスワードグラントを使用して、OIDC プロバイダーからアクセストークンを取得します。client_credentials には、クライアント ID とシークレットが必要です。パスワードグラントには、クライアント ID、シークレット、ユーザー名、およびパスワードが必要です。
  3. クライアントはアクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
  4. 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 プロパティーが設定されており、一致する配列または文字列のクレームが見つかった場合、これらのクレームからロールが展開されます。たとえば、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 ガイド "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;
        }
    }
}
1
OpenID Connect スコープの email を含むリクエストのみにアクセスが許可されます。
2
読み取りアクセスは、orders_read スコープを持つクライアントリクエストに制限されます。

io.quarkus.security.PermissionsAllowed アノテーションの詳細は、"Authorization of web endpoints" の 権限のアノテーション セクションを参照してください。

1.1.6. トークンの検証とイントロスペクション

トークンが 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 メタデータ検出呼び出しが必要になることです。

シンプルな 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-oidcquarkus.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 つ削除してスペースを見つけようとします。さらに、クリーンアップタイマーをアクティベートにすると、期限切れのエントリーが定期的にチェックされ、削除されます。

デフォルトのキャッシュ実装を試したり、カスタムのキャッシュ実装を登録したりできます。

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 の場合、これらは audiencescope パラメーターです。

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 Keycloakquarkus.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");
    }
}

@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 {

    }

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

1.1.16. ログエラーの確認

トークン検証エラーの詳細を確認するには、io.quarkus.oidc.runtime.OidcProviderTRACE レベルのロギングを有効にします。

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.OidcRecorderTRACE レベルのロギングを有効にします。

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 プロパティーが必要です。

Red Hat logoGithubRedditYoutubeTwitter

詳細情報

試用、購入および販売

コミュニティー

Red Hat ドキュメントについて

Red Hat をお使いのお客様が、信頼できるコンテンツが含まれている製品やサービスを活用することで、イノベーションを行い、目標を達成できるようにします。

多様性を受け入れるオープンソースの強化

Red Hat では、コード、ドキュメント、Web プロパティーにおける配慮に欠ける用語の置き換えに取り組んでいます。このような変更は、段階的に実施される予定です。詳細情報: Red Hat ブログ.

会社概要

Red Hat は、企業がコアとなるデータセンターからネットワークエッジに至るまで、各種プラットフォームや環境全体で作業を簡素化できるように、強化されたソリューションを提供しています。

© 2024 Red Hat, Inc.