The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

OpenID Connect (OIDC) マルチテナンシーの使用

このガイドでは、OpenID Connect (OIDC) アプリケーションがマルチテナンシーをサポートして、単一のアプリケーションから複数のテナントにサービスを提供する方法を説明します。テナントは、同じ OpenID プロバイダー内の個別のレルムまたはセキュリティードメイン、あるいは個別の OpenID プロバイダーです。

同じアプリケーション (例: SaaS) から複数の顧客にサービスを提供する場合、各顧客はテナントです。アプリケーションに対してマルチテナンシーサポートを有効にすることで、Keycloak や Google などのさまざまな OpenID プロバイダーに対する認証であっても、テナントごとに異なる認証ポリシーをサポートできます。

Please read the OIDC Bearer token authentication guide if you need to authorize a tenant using Bearer Token Authorization.

If you need to authenticate and authorize a tenant using OpenID Connect Authorization Code Flow, read the OIDC code flow mechanism for protecting web applications guide.

また、 OIDC設定プロパティ のリファレンスガイドもご参照ください。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 11+ がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.3

  • 動作するコンテナランタイム(Docker, Podman)

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

  • jq tool

アーキテクチャ

この例では、2つのリソースメソッドをサポートする非常にシンプルなアプリケーションを構築します:

  • /{tenant}

OpenID Providerが発行するIDトークンから取得した、認証されたユーザと現在のテナントに関する情報を返すリソースです。

  • /{tenant}/bearer

OpenID Providerが発行するアクセストークンから取得した、認証されたユーザと現在のテナントに関する情報を返すリソースです。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Git リポジトリーのクローンを作成: git clone https://github.com/quarkusio/quarkus-quickstarts.git、または アーカイブ をダウンロードします。

The solution is located in the security-openid-connect-multi-tenancy-quickstart directory.

Maven プロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します:

コマンドラインインタフェース
quarkus create app org.acme:security-openid-connect-multi-tenancy-quickstart \
    --extension='oidc,resteasy-reactive-jackson' \
    --no-code
cd security-openid-connect-multi-tenancy-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

For more information about how to install and use the Quarkus CLI, see the Quarkus CLI guide.

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.4.1:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-openid-connect-multi-tenancy-quickstart \
    -Dextensions='oidc,resteasy-reactive-jackson' \
    -DnoCode
cd security-openid-connect-multi-tenancy-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windowsユーザーの場合:

  • If using cmd, (don’t use backward slash \ and put everything on the same line)

  • If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=security-openid-connect-multi-tenancy-quickstart"

すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに oidc エクステンションを追加することができます:

コマンドラインインタフェース
quarkus extension add 'oidc'
Maven
./mvnw quarkus:add-extension -Dextensions='oidc'
Gradle
./gradlew addExtension --extensions='oidc'

これにより、ビルドファイルに以下が追加されます:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-oidc")

アプリケーションの記述

まず、 /{tenant} のエンドポイントを実装することから始めましょう。以下のソースコードからわかるように、これは通常の Jakarta REST リソースです:

package org.acme.quickstart.oidc;

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

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;

@Path("/{tenant}")
public class HomeResource {
    /**
     * Injection point for the ID Token issued by the OpenID Connect Provider
     */
    @Inject
    @IdToken
    JsonWebToken idToken;

    /**
     * Injection point for the Access Token issued by the OpenID Connect Provider
     */
    @Inject
    JsonWebToken accessToken;

    /**
     * Returns the ID Token info. This endpoint exists only for demonstration purposes, you should not
     * expose this token in a real application.
     *
     * @return ID Token info
     */
    @GET
    @Produces("text/html")
    public String getIdTokenInfo() {
        StringBuilder response = new StringBuilder().append("<html>")
                .append("<body>");

        response.append("<h2>Welcome, ").append(this.idToken.getClaim("email").toString()).append("</h2>\n");
        response.append("<h3>You are accessing the application within tenant <b>").append(idToken.getIssuer()).append(" boundaries</b></h3>");

        return response.append("</body>").append("</html>").toString();
    }

    /**
     * Returns the Access Token info. This endpoint exists only for demonstration purposes, you should not
     * expose this token in a real application.
     *
     * @return Access Token info
     */
    @GET
    @Produces("text/html")
    @Path("bearer")
    public String getAccessTokenInfo() {
        StringBuilder response = new StringBuilder().append("<html>")
                .append("<body>");

        response.append("<h2>Welcome, ").append(this.accessToken.getClaim("email").toString()).append("</h2>\n");
        response.append("<h3>You are accessing the application within tenant <b>").append(accessToken.getIssuer()).append(" boundaries</b></h3>");

        return response.append("</body>").append("</html>").toString();
    }
}

受信リクエストからテナントを解決し、application.propertiesで特定の quarkus-oidc テナント設定にマッピングするためには、テナント設定を動的に解決するために使用できる io.quarkus.oidc.TenantConfigResolver インターフェイスの実装を作成する必要があります:

package org.acme.quickstart.oidc;

import jakarta.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.config.ConfigProvider;

import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String path = context.request().path();

        if (path.startsWith("/tenant-a")) {
           String keycloakUrl = ConfigProvider.getConfig().getValue("keycloak.url", String.class);

            OidcTenantConfig config = new OidcTenantConfig();
            config.setTenantId("tenant-a");
            config.setAuthServerUrl(keycloakUrl + "/realms/tenant-a");
            config.setClientId("multi-tenant-client");
            config.getCredentials().setSecret("secret");
            config.setApplicationType(ApplicationType.HYBRID);
            return Uni.createFrom().item(config);
        } else {
            // resolve to default tenant config
            return Uni.createFrom().nullItem();
        }
    }
}

上記の実装から、テナントはリクエストパスから解決されるため、テナントを推測できなかった場合は、デフォルトのテナント設定を使用する必要があることを示すために null が返されます。

tenant-a アプリケーションタイプは hybrid であることに注意してください。HTTPベアラートークンが提供された場合はそれを受け入れることができますが、そうでない場合は認証が必要なときに認可コードフローが開始されます。

アプリケーションの設定

# Default Tenant Configuration
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=multi-tenant-client
quarkus.oidc.application-type=web-app

# Tenant A Configuration is created dynamically in CustomTenantConfigResolver

# HTTP Security Configuration
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated

The first configuration is the default tenant configuration that should be used when the tenant can not be inferred from the request. Note that a %prod profile prefix is used with quarkus.oidc.auth-server-url - it is done to support testing a multi-tenant application with Dev Services For Keycloak. This configuration is using a Keycloak instance to authenticate users.

2つ目の設定は TenantConfigResolver によって提供されます。これは、受信リクエストがテナント tenant-a にマッピングされるときに使用される設定です。

どちらの設定でも、異なる realms を使用しばら、同じ Keycloak サーバーインスタンスにマップされることに注意してください。

または、 application.properties で直接テナント tenant-a を設定することもできます:

# Default Tenant Configuration
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=multi-tenant-client
quarkus.oidc.application-type=web-app

# Tenant A Configuration
quarkus.oidc.tenant-a.auth-server-url=http://localhost:8180/realms/tenant-a
quarkus.oidc.tenant-a.client-id=multi-tenant-client
quarkus.oidc.tenant-a.application-type=web-app

# HTTP Security Configuration
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated

そして、カスタム TenantConfigResolver を使って解決します:

package org.acme.quickstart.oidc;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        String path = context.request().path();
        String[] parts = path.split("/");

        if (parts.length == 0) {
            // resolve to default tenant configuration
            return null;
        }

        return parts[1];
    }
}

設定ファイルで複数のテナントを定義できます。TenantResolver 実装からテナントを解決するときに適切にマップできるように、それらに一意のエイリアスがあることを確認してください。

しかし、静的なテナント解決( application.properties でテナントを設定し、 TenantResolver で解決する)を使用すると、 Dev Services for Keycloak でエンドポイントをテストすることができません。 Dev Services for Keycloak は、リクエストが個々のテナントにどのようにマッピングされるかを知らないため、テナント固有の quarkus.oidc.<tenant-id>.auth-server-url 値を動的に提供できず、したがって %prod プレフィックスを使用して application.properties のテナントに固有の URL を使用するとテストや開発モードで動作しません。

現在のテナントが OIDC の web-app アプリケーションを表す場合、カスタムテナントリゾルバーがすべてのリクエストに対して呼び出されるまでに、現在の io.vertx.ext.web.RoutingContext には tenant-id 属性が含まれ、テナント固有の状態またはセッション Cookie のいずれかがすでに存在する場合に、コード認証フローとすでに認証されたリクエストを完了します。したがって、複数の OpenID Connect プロバイダーを使用する場合、RoutingContexttenant-id 属性が設定されていない場合にのみ、パス固有のチェックを行ってテナント ID を解決する必要があります。以下はその例です。

package org.acme.quickstart.oidc;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        String tenantId = context.get("tenant-id");
        if (tenantId != null) {
            return tenantId;
        } else {
            // Initial login request
            String path = context.request().path();
            String[] parts = path.split("/");

            if (parts.length == 0) {
                // resolve to default tenant configuration
                return null;
            }
            return parts[1];
        }
    }
}

In fact, this is how Quarkus OIDC resolves static custom tenants itself if no custom TenantResolver is registered.

同様の手法は、 TenantConfigResolver でも使用できます。コンテキストで提供される tenant-id を使用して、前のリクエストで既に用意された OidcTenantConfig を返すことができます。

If you also use Hibernate ORM multitenancy or MongoDB with Panache multitenancy and both tenant IDs are the same and must be extracted from the Vert.x RoutingContext you can pass the tenant id from the OIDC Tenant Resolver to the Hibernate ORM Tenant Resolver or MongoDB with Panache Mongo Database Resolver as a RoutingContext attribute, for example:

public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        String tenantId = extractTenantId(context);
        context.put("tenantId", tenantId);
        return tenantId;
    }
}

Keycloak サーバーの起動と設定

Keycloak サーバーを起動するにはDockerを使用し、以下のコマンドを実行するだけです。

docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev

ここで、keycloak.version17.0.0 以上に設定する必要があります。

localhost:8180 で Keycloak サーバーにアクセスできるはずです。

Keycloak 管理コンソールにアクセスするには、 admin ユーザーとしてログインしてください。ユーザー名は admin 、パスワードは admin です。

さて、以下の手順に従って、2つのテナントのためのレルムをインポートします。

詳細は、 新規レルムの作成 方法に関する Keycloak ドキュメントを参照してください。

アプリケーションの実行と使用

デベロッパーモードでの実行

マイクロサービスをdevモードで実行する場合、次を実行して下さい:

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

JVM モードでの実行

「開発モード」で遊び終わったら、標準のJavaアプリケーションとして実行することができます。

まずコンパイルします。

コマンドラインインタフェース
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

次に、以下を実行してください。

java -jar target/quarkus-app/quarkus-run.jar

ネイティブモードでの実行

同じデモをネイティブコードにコンパイルすることができます。

これは、生成されたバイナリーにランタイム技術が含まれており、最小限のリソースオーバーヘッドで実行できるように最適化されているため、本番環境にJVMをインストールする必要がないことを意味します。

コンパイルには少し時間がかかるので、このステップはデフォルトで無効になっています。ネイティブビルドを有効にして再度ビルドしてみましょう。

コマンドラインインタフェース
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

コーヒーを飲み終わると、このバイナリーは以下のように直接実行出来るようになります:

./target/security-openid-connect-multi-tenancy-quickstart-runner

アプリケーションのテスト

Dev Services for Keycloakの使用

Keycloakに対する統合テストには、 Dev Services for Keycloak の使用を推奨します。 Dev Services for Keycloak はテストコンテナを起動し初期化します。設定されたレルムをインポートし、このクイックスタートで使用される CustomTenantResolver のベース Keycloak URL を設定して、レルム固有の URL を計算します。

まず、以下の依存関係を追加する必要があります:

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

quarkus-test-keycloak-server は、レルム固有のアクセストークンを取得するためのユーティリティクラス io.quarkus.test.keycloak.client.KeycloakTestClient を提供し、 RestAssured と共にベアラアクセストークンを期待する /{tenant}/bearer エンドポイントのテストに使用できます。HtmlUnit/{tenant} エンドポイントと認可コードフローのテストに使用します。

次に、必要なレルムを設定します:

# Default Tenant Configuration
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=multi-tenant-client
quarkus.oidc.application-type=web-app

# Tenant A Configuration is created dynamically in CustomTenantConfigResolver

# HTTP Security Configuration
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated

quarkus.keycloak.devservices.realm-path=default-tenant-realm.json,tenant-a-realm.json

最後に、JVM モードで実行されるテストを作成します。

package org.acme.quickstart.oidc;

import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;

import org.junit.jupiter.api.Test;

import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.restassured.RestAssured;

@QuarkusTest
public class CodeFlowTest {

    KeycloakTestClient keycloakClient = new KeycloakTestClient();

    @Test
    public void testLogInDefaultTenant() throws IOException {
        try (final WebClient webClient = createWebClient()) {
            HtmlPage page = webClient.getPage("http://localhost:8081/default");

            assertEquals("Sign in to quarkus", page.getTitleText());

            HtmlForm loginForm = page.getForms().get(0);

            loginForm.getInputByName("username").setValueAttribute("alice");
            loginForm.getInputByName("password").setValueAttribute("alice");

            page = loginForm.getInputByName("login").click();

            assertTrue(page.asText().contains("tenant"));
        }
    }

    @Test
    public void testLogInTenantAWebApp() throws IOException {
        try (final WebClient webClient = createWebClient()) {
            HtmlPage page = webClient.getPage("http://localhost:8081/tenant-a");

            assertEquals("Sign in to tenant-a", page.getTitleText());

            HtmlForm loginForm = page.getForms().get(0);

            loginForm.getInputByName("username").setValueAttribute("alice");
            loginForm.getInputByName("password").setValueAttribute("alice");

            page = loginForm.getInputByName("login").click();

            assertTrue(page.asText().contains("alice@tenant-a.org"));
        }
    }

    @Test
    public void testLogInTenantABearerToken() throws IOException {
        RestAssured.given().auth().oauth2(getAccessToken()).when()
            .get("/tenant-a/bearer").then().body(containsString("alice@tenant-a.org"));
    }

    private String getAccessToken() {
        return keycloakClient.getRealmAccessToken("tenant-a", "alice", "alice", "multi-tenant-client", "secret");
    }

    private WebClient createWebClient() {
        WebClient webClient = new WebClient();
        webClient.setCssErrorHandler(new SilentCssErrorHandler());
        return webClient;
    }
}

ネイティブモードの場合:

package org.acme.quickstart.oidc;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
public class CodeFlowIT extends CodeFlowTest {
}

初期化および設定方法の詳細については、Dev Services for Keycloak を参照してください。

ブラウザの使用

テストするには、ブラウザを開いて以下のURLにアクセスする必要があります。

すべてが期待どおりに機能している場合は、認証のために Keycloak サーバーにリダイレクトする必要があります。リクエストされたパスは、設定ファイルにマップされていない default テナントを定義していることに注意してください。この場合、デフォルトの設定が使用されます。

アプリケーションを認証するためには、Keycloakのログインページで以下の認証情報を入力する必要があります。

  • Username: alice

  • Password: alice

Login ボタンをクリックすると、アプリケーションにリダイレクトされます。

次の URL でアプリケーションにアクセスを試みます。

Keycloak のログインページに再度リダイレクトされます。ただし、ここでは別の realm を使用して認証します。

どちらの場合も、ユーザーが正常に認証されると、ランディングページにユーザーの名前と電子メールが表示されます。ユーザー alice は両方のテナントに存在しますが、アプリケーションにおいて、それらは異なるレルム/テナントに属する別個のユーザーです。

Static tenant configuration resolution

When you set multiple tenant configurations in the application.properties file, you only need to specify how the tenant identifier gets resolved. To configure the resolution of the tenant identifier, use one of the following options:

Default resolution

The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in the last segment of the request path.

The following application.properties example shows how you can configure two tenants named google and github:

# Tenant 'google' configuration
quarkus.oidc.google.provider=google
quarkus.oidc.google.client-id=${google-client-id}
quarkus.oidc.google.credentials.secret=${google-client-secret}
quarkus.oidc.google.authentication.redirect-path=/signed-in

# Tenant 'github' configuration
quarkus.oidc.github.provider=google
quarkus.oidc.github.client-id=${github-client-id}
quarkus.oidc.github.credentials.secret=${github-client-secret}
quarkus.oidc.github.authentication.redirect-path=/signed-in

In this example, both tenants configure OIDC web-app applications to use an authorization code flow to authenticate users and also require session cookies to get generated after the authentication has taken place. After either Google or GitHub authenticates the current user, the user gets returned to the /signed-in area for authenticated users, for example, a secured resource path on the JAX-RS endpoint.

Finally, to complete the default tenant resolution, set the following configuration property:

quarkus.http.auth.permission.login.paths=/google,/github
quarkus.http.auth.permission.login.policy=authenticated

If the endpoint is running on http://localhost:8080, you can also provide UI options for users to log in to either http://localhost:8080/google or http://localhost:8080/github, without having to add specific`/google` or /github JAX-RS resource paths. Tenant identifiers are also recorded in the session cookie names after the authentication is completed. Therefore, authenticated users can access the secured application area without requiring either the google or github path values to be included in the secured URL.

Default resolution can also work for Bearer token authentication but it might be less practical in this case because a tenant identifier will always need to be set as the last path segment value.

Resolve with TenantResolver

The following application.properties example shows how you can resolve the tenant identifier of two tenants named a and b by using the TenantResolver method:

# Tenant 'a' configuration
quarkus.oidc.a.auth-server-url=http://localhost:8180/realms/quarkus-a
quarkus.oidc.a.client-id=client-a
quarkus.oidc.a.credentials.secret=client-a-secret

# Tenant 'b' configuration
quarkus.oidc.b.auth-server-url=http://localhost:8180/realms/quarkus-b
quarkus.oidc.b.client-id=client-b
quarkus.oidc.b.credentials.secret=client-b-secret

You can return the tenant ID of either a or b from quarkus.oidc.TenantResolver:

import quarkus.oidc.TenantResolver;

public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        String path = context.request().path();
        if (path.endsWith("a")) {
            return "a";
        } else if (path.endsWith("b")) {
            return "b";
        } else {
            // default tenant
            return null;
        }
    }
}

In this example, the value of the last request path segment is a tenant ID, but if required, you can implement a more complex tenant identifier resolution logic.

Resolve with annotations

You can use the io.quarkus.oidc.Tenant annotation for resolving the tenant identifiers as an alternative to using io.quarkus.oidc.TenantResolver.

Proactive HTTP authentication must be disabled (quarkus.http.auth.proactive=false) for this to work. For more information, see Proactive authentication.

Assuming your application supports two OIDC tenants (hr, and default), all resource methods and classes carrying @Tenant("hr") will be authenticated using the OIDC provider configured by quarkus.oidc.hr.auth-server-url, while all other classes and methods will still be authenticated using the default OIDC provider.

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.oidc.Tenant;
import io.quarkus.security.Authenticated;

@Authenticated
@Path("/api/hello")
public class HelloResource {

    @Tenant("hr") (1)
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String sayHello() {
        return "Hello!";
    }
}
1 The io.quarkus.oidc.Tenant annotation must be placed either on resource class or resource method.

Dynamic tenant configuration resolution

サポートしたいさまざまなテナントに対して、より動的な設定が必要で、設定ファイルに複数のエントリーを入れたくない場合は、 io.quarkus.oidc.TenantConfigResolver が利用出来ます。

このインターフェイスを使用すると、実行時にテナント設定を動的に作成することができます。

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;

import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantConfigResolver;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String path = context.request().path();
        String[] parts = path.split("/");

        if (parts.length == 0) {
            // resolve to default tenant configuration
            return null;
        }

        if ("tenant-c".equals(parts[1])) {
            // Do 'return requestContext.runBlocking(createTenantConfig());'
            // if a blocking call is required to create a tenant config
            return Uni.createFromItem(createTenantConfig());
        }

        // resolve to default tenant configuration
        return null;
    }

    private Supplier<OidcTenantConfig> createTenantConfig() {
        final OidcTenantConfig config = new OidcTenantConfig();

        config.setTenantId("tenant-c");
        config.setAuthServerUrl("http://localhost:8180/realms/tenant-c");
        config.setClientId("multi-tenant-client");
        OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials();

        credentials.setSecret("my-secret");

        config.setCredentials(credentials);

        // any other setting support by the quarkus-oidc extension

        return () -> config;
    }
}

このメソッドから返される OidcTenantConfig は、application.properties から oidc 名前空間設定を解析するために使用されるものと同じです。quarkus-oidc エクステンションでサポートされている任意の設定を使用してデータを入力できます。

Tenant resolution for OIDC web-app applications

The simplest option for resolving OIDC web-app application configuration is to follow the steps described in the Default resolution section.

Try one of the options suggested below if the default resolution strategy does not work for your application setup.

serviceweb-app の両方の OIDC アプリケーションの現在の HTTP リクエストを保護するために使用する必要があるテナント設定を選択する際には、次のようないくつかのオプションを使用できます:

  • URL パスを確認します。たとえば、"/service" パスには tenant-service 設定を使用する必要がありますが、 "/management" パスには tenant-manage 設定を使用する必要があります

  • たとえば、URL パスが常に '/service' である HTTP ヘッダーを確認します。"Realm: service" や "Realm: management" などのヘッダーは、tenant-service 設定や tenant-manage 設定のいずれかを選択する場合に役立ちます。

  • URL クエリーパラメーターを確認します。ヘッダーを使用してテナント設定を選択するのと同じように機能します

これらのオプションはすべて、OIDC service アプリケーションのカスタムの TenantResolver 実装や TenantConfigResolver 実装を使用して簡単に実装できます。

ただし、OIDC web-app アプリケーションのコード認証フローを完了するために HTTP リダイレクトが必要なため、次の理由により、このリダイレクトリクエストの前後に同じテナント設定を選択するためにカスタム HTTP Cookie が必要になる場合があります。

  • 単一のリダイレクト URL が OIDC プロバイダーに登録されている場合、リダイレクトリクエスト後の URL パスは同じではない可能性があります。元のリクエストパスは復元できますが、それはテナント設定が解決された後です。

  • 元のリクエスト中に使用された HTTP ヘッダーは、リダイレクト後に使用できなくなります。

  • カスタム URL クエリーパラメーターは、リダイレクト後、テナント設定が解決された後に復元されます。

リダイレクトの前後に web-app アプリケーションのテナント設定を解決するための情報を確実に利用できるようにするための 1 つのオプションは、Cookie を使用することです。以下はその例です。

package org.acme.quickstart.oidc;

import java.util.List;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.TenantResolver;
import io.vertx.core.http.Cookie;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        List<String> tenantIdQuery = context.queryParam("tenantId");
        if (!tenantIdQuery.isEmpty()) {
            String tenantId = tenantIdQuery.get(0);
            context.addCookie(Cookie.cookie("tenant", tenantId));
            return tenantId;
        } else if (context.cookieMap().containsKey("tenant")) {
            return context.getCookie("tenant").getValue();
        }

        return null;
    }
}

テナント設定を無効にする

カスタムの TenantResolver および TenantConfigResolver の実装では、現在のリクエストからテナントを推測できず、デフォルトのテナント設定へのフォールバックが必要な場合は null を返すことがあります。

If you expect that the custom resolvers will always infer a tenant then you do not need to configure the default tenant resolution.

  • To disable the default tenant configuration, set quarkus.oidc.tenant-enabled=false.

The default tenant configuration is automatically disabled when quarkus.oidc.auth-server-url is not configured but either custom tenant configurations are available or TenantConfigResolver is registered.

テナント固有の設定を無効にすることもできます。例: quarkus.oidc.tenant-a.tenant-enabled=false