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) アプリケーションがマルチテナントをサポートして、単一のアプリケーションから複数のテナントにサービスを提供する方法を説明します。 これらのテナントは、同じ OIDC プロバイダー内の異なるレルムまたはセキュリティードメイン、あるいは異なる OIDC プロバイダーである場合もあります。

SaaS 環境など、同じアプリケーションから複数の顧客にサービスを提供する場合、各顧客は個別のテナントとして機能します。 アプリケーションに対してマルチテナンシーサポートを有効にすると、Keycloak や Google などの異なる OIDC プロバイダーに対して認証する場合でも、テナントごとに異なる認証ポリシーをサポートできます。

Bearer Token Authorization を使用してテナントを認可するには、OpenID Connect (OIDC) ベアラートークン認証 ガイドを参照してください。

OIDC 認可コードフローを使用してテナントを認証および認可するには、Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム ガイドを参照してください。

また、OpenID Connect (OIDC) 設定プロパティー のリファレンスガイドも参照してください。

要件

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

  • 約15分

  • IDE

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

  • Apache Maven 3.9.9

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

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

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

  • jq tool

アーキテクチャー

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

  • /{tenant}

このリソースは、認証されたユーザーと現在のテナントについて、OIDC プロバイダーによって発行された ID トークンから取得した情報を返します。

  • /{tenant}/bearer

このリソースは、認証されたユーザーと現在のテナントについて、OIDC プロバイダーによって発行された Access Token から取得した情報を返します。

ソリューション

完全に理解するために、次の手順に従ってアプリケーションをビルドすることを推奨します。

あるいは、完成した例から始める場合は、Git リポジトリーをクローン (git clone https://github.com/quarkusio/quarkus-quickstarts.git) するか、https://github.com/quarkusio/quarkus-quickstarts/archive/main.zip[アーカイブ] をダウンロードします。

ソリューションは、security-openid-connect-multi-tenancy-quickstart ディレクトリーにあります。

Maven プロジェクトの作成

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

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

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

Quarkus CLIのインストールと使用方法の詳細については、 Quarkus CLI ガイドを参照してください。

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

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

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-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 ファイルに以下が追加されます:

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 OIDC provider.
     */
    @Inject
    @IdToken
    JsonWebToken idToken;

    /**
     * Injection point for the Access Token issued by the OIDC provider.
     */
    @Inject
    JsonWebToken accessToken;

    /**
     * Returns the ID Token info.
     * This endpoint exists only for demonstration purposes.
     * Do 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.
     * Do 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.quarkus.oidc.runtime.OidcUtils;
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 = OidcTenantConfig
                    .authServerUrl(keycloakUrl + "/realms/tenant-a")
                    .tenantId("tenant-a")
                    .clientId("multi-tenant-client")
                    .credentials("secret")
                    .applicationType(ApplicationType.HYBRID)
                    .build();
            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.credentials.secret=secret
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

最初の設定は、リクエストからテナントを推測できない場合に使用するデフォルトのテナント設定です。 %prod` プロファイル接頭辞は、Dev Services For Keycloak を使用したマルチテナントアプリケーションのテストをサポートするために、quarkus.oidc.auth-server-url とともに使用される点に注意してください。 この設定では、Keycloak インスタンスを使用してユーザーを認証します。

TenantConfigResolver によって提供される 2 番目の設定は、受信リクエストが 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.credentials.secret=secret
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.credentials.secret=secret
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 を使用したエンドポイントのテストには機能しません。これは、リクエストが個々のテナントにどのようにマッピングされるかがわからず、テナント固有の quarkus.oidc.<tenant-id>.auth-server-url の値を動的に提供できないためです。したがって、application.properties 内のテナント固有の URL に %prod 接頭辞を付けると、テストモードと開発モードの両方で機能しません。

現在のテナントが OIDC の web-app アプリケーションを表す場合、カスタムテナントリゾルバーがすべてのリクエストに対して呼び出されるまでに、現在の io.vertx.ext.web.RoutingContext には tenant-id 属性が含まれ、テナント固有の状態またはセッション Cookie のいずれかがすでに存在する場合に、コード認証フローとすでに認証されたリクエストを完了します。 したがって、複数の OIDC プロバイダーを使用する場合、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];
        }
    }
}

これは、カスタム TenantResolver が登録されていない場合に、Quarkus OIDC が静的カスタムテナントを解決する方法です。

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

Hibernate ORM マルチテナントまたは MongoDB with Panache マルチテナントも使用しており、両方のテナント ID が同じである場合、 RoutingContext 属性から tenant-id を使用してテナント ID を取得できます。詳細は、次の URL を参照してください。

Keycloak サーバーの起動と設定

Keycloak サーバーを起動するには、Docker を使用して次のコマンドを実行します。

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

ここで、keycloak.version26.0.7 以降に設定されます。

localhost:8180 で Keycloak サーバーにアクセスします。

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

次に、2 つのテナントのレルムをインポートします。

詳細は、https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm[新規レルムの作成] 方法に関する 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.native.enabled=true

しばらくすると、このバイナリーを直接実行できるようになります。

./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>org.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("org.htmlunit:htmlunit")

quarkus-test-keycloak-server は、レルム固有のアクセストークンを取得するためのユーティリティクラス io.quarkus.test.keycloak.client.KeycloakTestClient を提供します。これは、ベアラアクセストークンを想定する /{tenant}/bearer エンドポイントのテストに RestAssured と共に使用できます。 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.htmlunit.SilentCssErrorHandler;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.junit.jupiter.api.Test;

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.getButtonByName("login").click();

            assertTrue(page.asNormalizedText().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.getButtonByName("login").click();

            assertTrue(page.asNormalizedText().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 ログインページで次の認証情報を入力します。

  • ユーザー名: alice

  • パスわ=ド: alice

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

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

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

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

テナントの解決

テナント解決の順序

OIDC テナントは、以下の順序で解決されます。

  1. プロアクティブ認証が無効になっている場合は、最初に io.quarkus.oidc.Tenant アノテーションがチェックされます。

  2. カスタム TenantConfigResolver を使用した動的なテナント解決。

  3. 静的テナント解決は、カスタム TenantResolver、設定されたテナントパス、およびテナント ID として最後のリクエストパスセグメントをデフォルト設定するオプションのいずれかを使用します。

上記手順の後にテナント ID が解決されていない場合は、最後にデフォルトの OIDC テナントが選択されます。

詳細は、以下のセクションを参照してください。

さらに、OIDC `web-app`アプリケーションの場合、状態とセッションの Cookie は、認可コードフローが開始された時点で上記のオプションのいずれかで解決されたテナントに関するヒントも提供します。詳細は、OIDC web-app アプリケーションのテナント解決 セクションを参照してください。

アノテーションで解決する

io.quarkus.oidc.TenantResolver を使用する代わりに、io.quarkus.oidc.Tenant アノテーションを使用してテナント識別子を解決できます。

これを機能させるには、プロアクティブ HTTP 認証を無効にする必要があります (quarkus.http.auth.proactive=false)。 詳細は、プロアクティブ認証 ガイドを参照してください。

アプリケーションが 2 つの OIDC テナント (hr テナントとデフォルトテナント) をサポートしていると仮定すると、@Tenant("hr") を持つすべてのリソースメソッドとクラスは、quarkus.oidc.hr.auth-server-url によって設定された OIDC プロバイダーを使用して認証されます。 対照的に、他のすべてのクラスとメソッドは、引き続きデフォルトの OIDC プロバイダーを使用して認証されます。

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 io.quarkus.oidc.Tenant アノテーションは、リソースクラスまたはリソースメソッドのいずれかに配置する必要があります。

上記の例では、sayHello エンドポイントの認証が @Authenticated アノテーションによって適用されます。

または、HTTP セキュリティーポリシー を使用する場合 エンドポイントを保護するには、@Tenant アノテーションを有効にするために、次の例に示すように、このポリシーの権限チェックを遅延する必要があります。

quarkus.http.auth.permission.authenticated.paths=/api/hello
quarkus.http.auth.permission.authenticated.methods=GET
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.http.auth.permission.authenticated.applies-to=JAXRS (1)
1 @Tenant アノテーションを使用してテナントを選択した後、Quarkus に HTTP 権限チェックを実行するように指示します。

動的なテナント設定の解決

サポートするさまざまなテナントに対して、より動的な設定が必要で、設定ファイルに複数のエントリーを入れたくない場合は、 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.createFrom().item(createTenantConfig());
        }

        //Resolve to default tenant configuration
        return null;
    }

    private Supplier<OidcTenantConfig> createTenantConfig() {
        final OidcTenantConfig config = OidcTenantConfig
                .authServerUrl("http://localhost:8180/realms/tenant-c")
                .tenantId("tenant-c")
                .clientId("multi-tenant-client")
                .credentials("my-secret")
                .build();

        // Any other setting supported by the quarkus-oidc extension

        return () -> config;
    }
}

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

動的テナントリゾルバーが null を返す場合、次に 静的テナント設定の解決 が試行されます。

静的テナント設定の解決

application.properties ファイルで複数のテナント設定を行う場合は、テナント識別子の解決方法を指定するだけで済みます。 テナント識別子の解決を設定するには、次のいずれかのオプションを使用します。

これらのテナント解決オプションは、テナント ID が解決されるまで、リストされている順序で試行されます。 テナント ID が未解決のまま (null) の場合、デフォルト (名前なし) のテナント設定が選択されます。

TenantResolver で解決する

次の application.properties の例は、TenantResolver メソッドを使用して、a および b という名前の 2 つのテナントのテナント ID を解決する方法を示しています。

# 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

io.quarkus.oidc.TenantResolver から a または b のテナント ID を返すことができます。

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

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

この例では、最後のリクエストパスセグメントの値はテナント ID ですが、必要に応じて、より複雑なテナント ID 解決ロジックを実装できます。

テナントパスの設定

io.quarkus.oidc.TenantResolver を使用する代わりに、quarkus.oidc.tenant-paths 設定プロパティーを使用してテナント識別子を解決することができます。 前の例で使用した HelloResource リソースの sayHello エンドポイントに hr テナントを選択する方法は次のとおりです。

quarkus.oidc.hr.tenant-paths=/api/hello (1)
quarkus.oidc.a.tenant-paths=/api/* (2)
quarkus.oidc.b.tenant-paths=/*/hello (3)
1 前の例の quarkus.http.auth.permission.authenticated.paths=/api/hello 設定プロパティーと同じパス一致ルールが適用されます。
2 パスの末尾に置かれたワイルドカードは、任意の数のパスセグメントを表します。ただし、パスは /api/hello よりも具体的ではないため、sayHello エンドポイントを保護するために hr テナントが使用されます。
3 /*/hello のワイルドカードは、1 つのパスセグメントのみを表します。ただし、ワイルドカードは api ほど具体的ではないため、hr テナントが使用されます。
パス一致メカニズムは、設定を使用した認可 とまったく同じように動作します。

最後のリクエストパスセグメントをテナント ID として使用する

テナント識別子のデフォルトの解決は規則に基づいており、認証要求には要求パスの最後のセグメントにテナント識別子が含まれている必要があります。

次の application.properties の例は、googlegithub という名前の 2 つのテナントを設定する方法を示しています。

# 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=github
quarkus.oidc.github.client-id=${github-client-id}
quarkus.oidc.github.credentials.secret=${github-client-secret}
quarkus.oidc.github.authentication.redirect-path=/signed-in

提供された例では、両方のテナントが OIDC web-app アプリケーションを設定して、認可コードフローを使用してユーザーを認証し、認証後にセッション Cookie を生成することを要求します。 Google または GitHub が現在のユーザーを認証すると、ユーザーは、JAX-RS エンドポイント上のセキュアなリソースパスなど、認証されたユーザーの /signed-in 領域に戻されます。

最後に、デフォルトのテナント解決を完了するには、次の設定プロパティーを設定します。

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

エンドポイントが http://localhost:8080 で実行されている場合は、特定の /google または /github JAX-RS リソースパスを追加せずに、ユーザーが http://localhost:8080/google または http://localhost:8080/github のいずれかにログインするための UI オプションを提供することもできます。 認証が完了すると、テナント識別子もセッション Cookie 名に記録されます。 したがって、認証されたユーザーは、セキュアな URL に google または github パス値を含める必要なく、セキュアなアプリケーション領域にアクセスできます。

デフォルトの解決は、ベアラートークン認証にも機能します。 ただし、テナント識別子は常に最後のパスセグメント値として設定する必要があるため、あまり実用的ではない可能性があります。

トークン issuer のクレームでテナントを解決する

ベアラートークン認証をサポートする OIDC テナントは、アクセストークンの issuer を使用して解決できます。 issuer ベースの解決が機能するには、次の条件を満たす必要があります。

  • アクセストークンが JWT 形式で、issuer (iss) トークン要求を含んでいる必要があります。

  • アプリケーションタイプが service または hybrid である OIDC テナントのみが考慮されます。これらのテナントでは、トークン issuer が検出または設定されている必要があります。

issuer ベースの解決は、quarkus.oidc.resolve-tenants-with-issuer プロパティーで有効になります。例:

quarkus.oidc.resolve-tenants-with-issuer=true (1)

quarkus.oidc.tenant-a.auth-server-url=${tenant-a-oidc-provider} (2)
quarkus.oidc.tenant-a.client-id=${tenant-a-client-id}
quarkus.oidc.tenant-a.credentials.secret=${tenant-a-client-secret}

quarkus.oidc.tenant-b.auth-server-url=${tenant-b-oidc-provider} (3)
quarkus.oidc.tenant-b.discover-enabled=false
quarkus.oidc.tenant-b.token.issuer=${tenant-b-oidc-provider}/issuer
quarkus.oidc.tenant-b.jwks-path=/jwks
quarkus.oidc.tenant-b.token-path=/tokens
quarkus.oidc.tenant-b.client-id=${tenant-b-client-id}
quarkus.oidc.tenant-b.credentials.secret=${tenant-b-client-secret}
1 テナント tenant-atenant-b は、JWT アクセストークンの issuer iss クレーム値を使用して解決されます。
2 テナント tenant-a は、OIDC プロバイダーの既知の設定エンドポイントから issuer を検出します。
3 テナント tenant-b は、OIDC プロバイダーが検出をサポートしていないため、issuer を設定します。

OIDC web-app アプリケーションのテナント解決

OIDC web-app アプリケーションのテナント解決は、OIDC テナント固有の設定が次の各手順の実行方法に影響する場合、認可コードフロー中に少なくとも 3 回実行する必要があります。

手順 1: 認証されていないユーザーがエンドポイントにアクセスし、OIDC プロバイダーにリダイレクトされます。

認証されていないユーザーがセキュアなパスにアクセスすると、ユーザーは認証のために OIDC プロバイダーにリダイレクトされ、テナント設定を使用してリダイレクト URI が構築されます。

静的テナント設定の解決動的なテナント設定の解決 セクションに記載されているすべての静的および動的テナント解決オプションを使用して、テナントを解決できます。

手順 2: ユーザーはエンドポイントにリダイレクトされます。

プロバイダー認証後、ユーザーは Quarkus エンドポイントにリダイレクトされ、テナント設定を使用して認可コードフローが完了します。

静的および動的テナント解決オプションはすべて、静的テナント設定の解決 に記載されています。また、動的なテナント設定の解決 セクションを使用して、テナントを解決できます。テナント解決が始まる前に、認可コードフローの state cookie を使用して、すでに解決されたテナント設定 ID を RoutingContext の tenant-id 属性として設定します。カスタムの動的 TenantConfigResolver テナントリゾルバーと静的 TenantResolver テナントリゾルバーの両方でこれを確認できます。

ステップ 3: 認証されたユーザーがセッション Cookie を使用して保護されたパスにアクセスする

テナント設定によって、セッション Cookie の検証方法と更新方法が決まります。テナント解決が始まる前に、認可コードフロー session cookie を使用して、すでに解決されたテナント設定 ID を RoutingContext tenant-id 属性として設定します。カスタム動的 TenantConfigResolver テナントリゾルバーと静的 TenantResolver テナントリゾルバーの両方がこれを確認できます。

たとえば、カスタム TenantConfigResolver が、すでに解決されているテナント設定の作成を回避する方法は以下に示すとおりです。この作成を回避しない場合は、データベースやその他のリモートソースからの読み取りをブロックする必要がある可能性があります。

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
        if (resolvedTenantId != null) { (1)
            return null;
        }

        String path = context.request().path(); (2)
        if (path.endsWith("tenant-a")) {
            return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a"));
        } else if (path.endsWith("tenant-b")) {
            return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b"));
        }

        // Default tenant id
        return null;
    }

    private OidcTenantConfig createTenantConfig(String tenantId, String clientId, String secret) {
        final OidcTenantConfig config = OidcTenantConfig
                .authServerUrl("http://localhost:8180/realms/"  + tenantId)
                .tenantId(tenantId)
                .clientId(clientId)
                .credentials(secret)
                .applicationType(ApplicationType.WEB_APP)
                .build();
        return config;
    }
}
1 以前に解決されている場合は、すでに解決されたテナント設定を Quarkus が使用するようにします。
2 テナント設定を作成するためのリクエストパスを確認します。

デフォルトの設定は次のようになります。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/default
quarkus.oidc.client-id=client-default
quarkus.oidc.credentials.secret=secret-default
quarkus.oidc.application-type=web-app

上記の例では、tenant-atenant-b、およびデフォルトのテナントがすべて同じエンドポイントパスを保護するために使用されていることを前提としています。つまり、ユーザーが tenant-a 設定で認証された後、このユーザーはログアウトしてセッション Cookie がクリアされるか期限切れになるまで、tenant-b またはデフォルトの設定で認証することを選択できません。

複数の OIDC web-app テナントがテナント固有のパスを保護する状況はあまり一般的ではなく、特別な注意も必要です。 tenant-atenant-b、およびデフォルトテナントなどの複数の OIDC web-app テナントを使用してテナント固有のパスへのアクセスを制御する場合、1 つの OIDC プロバイダーで認証されたユーザーは、別のプロバイダーによる認証が必要なパスにアクセスできません。アクセスできてしまうと、結果が予測不可能になり、予期しない認証エラーが発生する可能性が高くなります。 たとえば、tenant-a 認証に Keycloak 認証が必要で、tenant-b 認証に Auth0 認証が必要な場合、tenant-a で認証済みユーザーが tenant-b 設定で保護されたパスにアクセスしようとすると、Auth0 公開検証キーを使用して Keycloak によって署名されたトークンを検証できないため、セッション Cookie は検証されません。 複数の web-app テナントが互いに競合するのを回避するための簡単で推奨される方法は、次の例に示すように、テナント固有のセッションパスを設定することです。

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
        if (resolvedTenantId != null) { (1)
            return null;
        }

        String path = context.request().path(); (2)
        if (path.endsWith("tenant-a")) {
            return Uni.createFrom().item(createTenantConfig("tenant-a", "/tenant-a", "client-a", "secret-a"));
        } else if (path.endsWith("tenant-b")) {
            return Uni.createFrom().item(createTenantConfig("tenant-b", "/tenant-b", "client-b", "secret-b"));
        }

        // Default tenant id
        return null;
    }

    private OidcTenantConfig createTenantConfig(String tenantId, String cookiePath, String clientId, String secret) {
        final OidcTenantConfig config = OidcTenantConfig
                .authServerUrl("http://localhost:8180/realms/" + tenantId)
                .tenantId(tenantId)
                .clientId(clientId)
                .credentials(secret)
                .applicationType(ApplicationType.WEB_APP)
                .authentication().cookiePath(cookiePath).end()  (3)
                .build();
        return config;
    }
}
1 以前に解決されている場合は、すでに解決されたテナント設定を Quarkus が使用するようにします。
2 テナント設定を作成するためのリクエストパスを確認します。
3 テナント固有の Cookie パスを設定し、セッション Cookie がそれを作成したテナントにのみ表示されるようにします。

デフォルトのテナント設定は、次のように調整する必要があります。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/default
quarkus.oidc.client-id=client-default
quarkus.oidc.credentials.secret=secret-default
quarkus.oidc.authentication.cookie-path=/default
quarkus.oidc.application-type=web-app

複数の OIDC web-app テナントがテナント固有のパスを保護するときに同じセッション Cookie パスを持つことは推奨されず、避ける必要があります。 これは、カスタムリゾルバーがさらに注意する必要があるためです。例:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
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(); (1)
        if (path.endsWith("tenant-a")) {
            String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
	    if (resolvedTenantId != null) {
	        if ("tenant-a".equals(resolvedTenantId)) { (2)
	            return null;
	        } else {
	           // Require a "tenant-a" authentication
                   context.remove(OidcUtils.TENANT_ID_ATTRIBUTE); (3)
	        }
            }
            return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a"));
        } else if (path.endsWith("tenant-b")) {
            String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
	    if (resolvedTenantId != null) {
	        if ("tenant-b".equals(resolvedTenantId)) { (2)
	            return null;
	        } else {
	            // Require a "tenant-b" authentication
                   context.remove(OidcUtils.TENANT_ID_ATTRIBUTE); (3)
	        }
            }
            return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b"));
        }

        // Set default tenant id
        context.put(OidcUtils.TENANT_ID_ATTRIBUTE, OidcUtils.DEFAULT_TENANT_ID); (4)
        return null;
    }

    private OidcTenantConfig createTenantConfig(String tenantId, String clientId, String secret) {
        final OidcTenantConfig config = OidcTenantConfig
            .authServerUrl("http://localhost:8180/realms/"  + tenantId)
            .tenantId(tenantId)
            .clientId(clientId)
            .credentials(secret)
            .applicationType(ApplicationType.WEB_APP)
            .build();
        return config;
    }
}
1 テナント設定を作成するためのリクエストパスを確認します。
2 現在のパスに対してすでに解決されたテナントが予想される場合は、Quarkus がすでに解決されたテナント設定を使用するようにします。
3 すでに解決されたテナント設定が現在のパスに対して予想されていない場合は、tenant-id 属性を削除します。
4 他のすべてのパスにはデフォルトのテナントを使用します。これは、tenant-id 属性を削除するのと同じです。

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

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

カスタムリゾルバーが常にテナントを解決することを期待する場合は、デフォルトのテナント解決を設定する必要はありません。

  • デフォルトのテナント設定をオフにするには、quarkus.oidc.tenant-enabled=false を設定します。

quarkus.oidc.auth-server-url が設定されていない場合、デフォルトのテナント設定は自動的に無効になりますが、カスタムテナント設定が使用可能であるか、TenantConfigResolver が登録されています。

テナント固有の設定も無効にできる点に注意してください (例: quarkus.oidc.tenant-a.tenant-enabled=false)。

複数のテナント向けのプログラムによる OIDC の起動

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

package io.quarkus.it.oidc;

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

public class OidcStartup {

    void observe(@Observes Oidc oidc) { (1)
        oidc.create(OidcTenantConfig.authServerUrl("http://localhost:8180/realms/tenant-one").tenantId("tenant-one").build()); (2)
        oidc.create(OidcTenantConfig.authServerUrl("http://localhost:8180/realms/tenant-two").tenantId("tenant-two").build()); (3)
    }

}
1 OIDC イベントを確認します。
2 OIDC テナント tenant-one を作成します。
3 OIDC テナント tenant-two を作成します。

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

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

関連コンテンツ