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.15
-
動作するコンテナランタイム(Docker, Podman)
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
アーキテクチャー
この例では、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 プロジェクトの作成
まず、新しいプロジェクトが必要です。 以下のコマンドで新しいプロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-Dパラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=security-openid-connect-multi-tenancy-quickstart"
Quarkus プロジェクトがすでに設定されている場合は、プロジェクトのベースディレクトリーで次のコマンドを実行して、oidc エクステンションをプロジェクトに追加します。
quarkus extension add oidc
./mvnw quarkus:add-extension -Dextensions='oidc'
./gradlew addExtension --extensions='oidc'
これにより、ビルドファイルに以下が追加されます。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
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 の
これは、カスタム 同様の手法は |
|
Hibernate ORM マルチテナンシーまたは Panache マルチテナンシーを備えた MongoDB も使用しており、両方のテナント ID が同じである場合、
|
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:26.5.7 start-dev
localhost:8180 で Keycloak サーバーにアクセスします。
Keycloak 管理コンソールにアクセスするには、admin ユーザーとしてログインします。
ユーザー名とパスワードはどちらも admin です。
次に、2 つのテナントのレルムをインポートします。
-
default-tenant-realm.json をインポートして、デフォルトのレルムを作成します。
-
tenant-a-realm.json をインポートして、テナント
tenant-aのレルムを作成します。
新規レルムの作成 方法の詳細については、Keycloak ドキュメントを参照してください。
アプリケーションの実行と使用
開発モードでの実行
マイクロサービスを開発モードで実行するには、次を使用します:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
JVM モードでの実行
開発モードでアプリケーションを試した後、標準の Java アプリケーションとして実行できます。
まず、コンパイルします:
quarkus build
./mvnw install
./gradlew build
次に、それを実行します:
java -jar target/quarkus-app/quarkus-run.jar
ネイティブモードでの実行
この同じデモはネイティブコードにコンパイルできます。変更は必要ありません。
これは、生成されたバイナリーにランタイムテクノロジーが含まれ、最小限のリソースで実行するように最適化されているため、実稼働環境に JVM をインストールする必要がなくなることを意味します。
コンパイルには少し時間がかかるため、この手順はデフォルトでオフになっています。ネイティブビルドを有効にして再度ビルドしてみましょう。
quarkus build --native
./mvnw install -Dnative
./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 を計算します。
まず、次の依存関係を追加します。
<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>
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 テナントは、次の順序で解決されます:
-
プロアクティブ認証が無効になっている場合は、最初に
io.quarkus.oidc.Tenantアノテーションがチェックされます。 -
カスタム
TenantConfigResolverを使用した動的なテナント解決。 -
静的テナント解決は、カスタム
TenantResolver、設定されたテナントパス、およびテナント ID として最後のリクエストパスセグメントをデフォルト設定するオプションのいずれかを使用します。
上記手順の後にテナント ID が解決されていない場合は、最後にデフォルトの OIDC テナントが選択されます。
詳細は、次のセクションを参照してください:
さらに、OIDC web-app アプリケーションの場合、状態とセッションの Cookie は、認可コードフローが開始された時点で上記のオプションのいずれかで解決されたテナントに関するヒントも提供します。詳細は、OIDC web-app アプリケーションのテナント解決 セクションを参照してください。
アノテーションで解決する
io.quarkus.oidc.TenantResolver を使用する代わりに、io.quarkus.oidc.Tenant アノテーションを使用してテナント識別子を解決できます。
|
これを機能させるには、プロアクティブ HTTP 認証を無効にする必要があります ( |
アプリケーションが 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 アノテーションは、リソースクラスまたはリソースメソッドのいずれかに配置する必要があります。 |
|
上記の例では、 あるいは、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 名前空間設定を解析するために使用されるものと同じです。quarkus-oidc エクステンションでサポートされている任意の設定を使用してデータを入力できます。
動的テナントリゾルバーが null を返す場合、次に 静的テナント設定の解決 が試行されます。
解決された動的テナント設定を更新
すでに解決されたテナント設定を更新する必要がある場合があります。たとえば、登録済みの OIDC アプリケーションでクライアントクレデンシャルが更新された場合、クライアントクレデンシャルも更新する必要があるかもしれません。
設定を更新するには、OidcTenantConfigBuilder を使用して OidcTenantConfig の新しいインスタンスを作成し、必要に応じて変更してから返すようにします。
package org.acme;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.TenantConfigBean;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {
@Inject
TenantConfigBean tenantConfigBean;
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
// Check request path or request context `tenant-id` property to determine the tenant id.
var currentTenantConfig = tenantConfigBean.getDynamicTenant("some-dynamic-tenant-id").getOidcTenantConfig(); (1)
if (currentTenantConfig != null
&& "name".equals(currentTenantConfig.token().principalClaim().get())) { (2)
// This is an original configuration, update it now:
OidcTenantConfig updatedConfig = OidcTenantConfig.builder(currentTenantConfig) (3)
.token().principalClaim("email").end()
.build();
return Uni.createFrom().item(updatedConfig);
}
// create an initial configuration for the tenant
OidcTenantConfig config = OidcTenantConfig.builder() (4)
.token().principalClaim("name").end()
// set other properties
.build();
return Uni.createFrom().item(config);
}
}
| 1 | すでに解決されたテナント設定を取得するには、io.quarkus.oidc.runtime.TenantConfigBean を使用します。あるいは、リゾルバーにキャッシュされているテナント OidcTenantConfig を使用することもできます。 |
| 2 | 複数のリダイレクトなどにより、複数の冗長な更新を避けるために、この設定がすでに更新されているかどうかを確認したい場合があります。 |
| 3 | 解決された設定を使用してビルダーを作成し、必要に応じて更新します。 |
| 4 | 設定がまだ存在しない場合は、初期設定を作成します。 |
プロバイダーに再接続することなく、すでに解決された動的テナント設定を更新するために必要なことはこれだけです。
再接続が必要な場合、たとえば、テナントが UserInfo エンドポイントアドレスを再発見するために変更された可能性がある場合は、RoutingContext の replace-tenant-configuration-context プロパティーを true に設定するだけです。
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
OidcTenantConfig updatedConfig = OidcTenantConfig.builder(currentTenantConfig)
.token().principalClaim("email").end()
.build();
context.put("replace-tenant-configuration-context", "true"); (1)
return Uni.createFrom().item(updatedConfig);
}
| 1 | 解決済みのテナント設定を置き換え、プロバイダーに再接続する |
最後に、既存の OIDC セッションがまだアクティブな状態で解決済みの設定を更新することにした場合、最新のテナント設定要件に合わせるために、セッション Cookie を削除してユーザーを再認証する必要があるかもしれません。必要に応じて、RoutingContext の remove-session-cookie プロパティーを true に設定します。
@Override
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
OidcTenantConfig updatedConfig = OidcTenantConfig.builder(currentTenantConfig)
.token().principalClaim("email").end()
.build();
context.put("remove-session-cookie", "true"); (1)
return Uni.createFrom().item(updatedConfig);
}
| 1 | テナント設定を更新し、セッション Cookie を削除して、ユーザーの再認証をトリガーします。可能であれば、テナント解決時に再認証をトリガーするのではなく、まずユーザーをログアウトさせることをお勧めします。 |
静的テナント設定の解決
application.properties ファイルで複数のテナント設定を行う場合は、テナント識別子の解決方法を指定するだけで済みます。テナント識別子の解決を設定するには、次のいずれかのオプションを使用します。
これらのテナント解決オプションは、テナント ID が解決されるまでリストされている順序で試行されます。テナント ID が未解決のまま (null) の場合、デフォルト (名前なし) のテナント設定が選択されます。
TenantResolver で解決する
次の application.properties の例は、TenantResolver メソッドを使用して、a および b という名前の 2 つのテナントのテナント識別子を解決する方法を示しています。
# 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 ですが、必要に応じて、より複雑なテナント識別子解決ロジックを実装できます。
テナントパスの設定
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 テナントが使用されます。 |
| パス一致メカニズムは、設定を使用した認可 とまったく同じように動作します。 |
トークン 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-a と tenant-b は、JWT アクセストークンの issuer iss クレーム値を使用して解決されます。 |
| 2 | テナント tenant-a は、OIDC プロバイダーの既知の設定エンドポイントから issuer を検出します。 |
| 3 | テナント tenant-b は、OIDC プロバイダーが検出をサポートしていないため、issuer を設定します。 |
トークンヘッダー名でテナントを解決する
ベアラー認証をサポートする OIDC テナントは、トークンを含むカスタム HTTP ヘッダーを使用して解決できます。ヘッダーベースの解決は、quarkus.oidc.token.header プロパティーが Authorization 以外の値に設定されている場合に有効になります。例:
quarkus.oidc.tenant-a.token.header=Custom-Authorization-1 (1)
quarkus.oidc.tenant-b.token.header=Custom-Authorization-2 (2)
| 1 | テナント tenant-a は、受信 HTTP リクエストに Custom-Authorization-1 ヘッダーが含まれている場合に解決されます。 |
| 2 | テナント tenant-b は、リクエストに Custom-Authorization-2 ヘッダーが含まれている場合に解決されます。 |
リクエストパスセグメントを使用してテナント ID を見つけます
テナント識別子のデフォルトの解決は規則に基づいており、認証リクエストにはリクエストパスのパスセグメントのいずれかにテナント識別子を含める必要があります。
次の application.properties の例は、google と github という名前の 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 パス値を含める必要なく、セキュアなアプリケーション領域にアクセスできます。
デフォルトの解決は、ベアラー認証にも機能します。ただし、テナント識別子は常に最後のパスセグメント値として設定する必要があるため、あまり実用的ではない可能性があります。
OIDC リクエストおよびレスポンスフィルターを特定のテナントに制限する
io.quarkus.oidc.common.OidcRequestFilter と io.quarkus.oidc.common.OidcResponseFilter の両方のフィルターは、以下の例のように特定のテナントに制限できます。
package io.quarkus.it.oidc;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
@TenantFeature({ "tenant-one", "tenant-two" }) (1)
public class CustomOidcRequestFilter implements OidcRequestFilter {
@Override
public Uni<Void> filter(OidcRequestFilterContext requestContext) {
requestContext.request().putHeader("custom-header-name", "custom-header-value");
return Uni.createFrom().voidItem();
}
}
| 1 | CustomOidcRequestFilter フィルターを OIDC テナント tenant-one と tenant-two に制限します。 |
OIDC レスポンスフィルターは、quarkus.oidc.common.OidcEndpoint アノテーションを使用して、特定の OIDC エンドポイントまたは複数のエンドポイントに制限できます。
OIDC web-app アプリケーションのテナント解決
OIDC web-app アプリケーションのテナント解決は、OIDC テナント固有の設定が次の各手順の実行方法に影響する場合、認可コードフロー中に少なくとも 3 回実行する必要があります。
認証されていないユーザーがセキュアなパスにアクセスすると、ユーザーは認証のために OIDC プロバイダーにリダイレクトされ、テナント設定を使用してリダイレクト URI が構築されます。
静的テナント設定の解決 と 動的なテナント設定の解決 セクションに記載されているすべての静的および動的テナント解決オプションを使用して、テナントを解決できます。
プロバイダー認証後、ユーザーは Quarkus エンドポイントにリダイレクトされ、テナント設定を使用して認可コードフローが完了します。
静的テナント設定の解決 と 動的なテナント設定の解決 セクションに記載されているすべての静的および動的テナント解決オプションを使用して、テナントを解決できます。テナント解決が始まる前に、認可コードフローの state cookie を使用して、すでに解決されたテナント設定 ID を RoutingContext の tenant-id 属性として設定します。カスタムの動的 TenantConfigResolver テナントリゾルバーと静的 TenantResolver テナントリゾルバーの両方でこれを確認できます。
テナント設定によって、セッション 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-a、tenant-b、およびデフォルトのテナントがすべて同じエンドポイントパスを保護するために使用されていることを前提としています。つまり、ユーザーが tenant-a 設定で認証された後、このユーザーはログアウトしてセッション Cookie がクリアされるか期限切れになるまで、tenant-b またはデフォルトの設定で認証することを選択できません。
複数の OIDC web-app テナントがテナント固有のパスを保護する状況はあまり一般的ではなく、特別な注意も必要です。tenant-a、tenant-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.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