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)をインストールし、 適切に設定していること
アーキテクチャ
この例では、2つのリソースメソッドをサポートする非常にシンプルなアプリケーションを構築します:
-
/{tenant}
OpenID Providerが発行するIDトークンから取得した、認証されたユーザと現在のテナントに関する情報を返すリソースです。
-
/{tenant}/bearer
OpenID Providerが発行するアクセストークンから取得した、認証されたユーザと現在のテナントに関する情報を返すリソースです。
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローンするか git clone -b 3.2 https://github.com/quarkusio/quarkus-quickstarts.git 、 アーカイブ をダウンロードします。
ソリューションは、 security-openid-connect-multi-tenancy-quickstart ディレクトリーにあります。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
すでにQuarkusプロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトに oidc エクステンションを追加することができます。
quarkus extension add 'oidc'
./mvnw quarkus:add-extension -Dextensions='oidc'
./gradlew addExtension --extensions='oidc'
これにより、 pom.xml に以下が追加されます:
<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 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 の
In fact, this is how Quarkus OIDC resolves static custom tenants itself if no custom 同様の手法は、 |
|
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
|
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.version は 17.0.0 以上に設定する必要があります。
localhost:8180 で Keycloak サーバーにアクセスできるはずです。
Keycloak 管理コンソールにアクセスするには、 admin ユーザーとしてログインしてください。ユーザー名は admin 、パスワードは admin です。
さて、以下の手順に従って、2つのテナントのためのレルムをインポートします。
-
Import the default-tenant-realm.json to create the default realm
-
tenant-a-realm.json をインポートし、テナント
tenant-aのレルムを作成します。
詳細は、 新規レルムの作成 方法に関する Keycloak ドキュメントを参照してください。
アプリケーションの実行と使用
デベロッパーモードでの実行
マイクロサービスをdevモードで実行するには:
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.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 を計算します。
まず、以下の依存関係を追加する必要があります:
<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>
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 は両方のテナントに存在しますが、アプリケーションにおいて、それらは異なるレルム/テナントに属する別個のユーザーです。
アノテーション付きテナント識別子の解決
quarkus.oidc.TenantResolver を使用する代わりに、アノテーションと CDI インターセプターを使用してテナント識別子を解決できます。これは、現在の RoutingContext のキー OidcUtils.TENANT_ID_ATTRIBUTE の値を設定することで実行できます。
アプリケーションが 2 つの OIDC テナント (hr とデフォルト) をサポートしていると仮定した場合、最初にデフォルト以外のテナント ID ごとに 1 つのアノテーションを定義する必要があります。
|
Proactive HTTP authentication must be disabled ( |
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface HrTenant {
}
次に、これらのアノテーションごとに 1 つのインターセプターが必要になります。
@Interceptor
@HrTenant
public class HrTenantInterceptor {
@Inject
RoutingContext routingContext;
@AroundInvoke
Object setTenant(InvocationContext context) throws Exception {
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, "hr");
return context.proceed();
}
}
これで、 @HrTenant を運ぶすべてのメソッドとクラスは、 quarkus.oidc.hr.auth-server-url によって設定された OIDC プロバイダーを使用して認証されますが、他のすべてのクラスとメソッドは、デフォルトの OIDC プロバイダーを使用して認証されます。
テナントの設定をプログラムで解決
サポートしたいさまざまなテナントに対して、より動的な設定が必要で、設定ファイルに複数のエントリーを入れたくない場合は、 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 エクステンションでサポートされている任意の設定を使用してデータを入力できます。
OIDCの "web-app" アプリケーションのためのテナント解決
service と web-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 を返すことがあります。
カスタムリゾルバが常にテナントを推論することが予想される場合、デフォルトのテナント設定は必要ありません。 quarkus.oidc.tenant-enabled=false の設定で無効にすることができます。
テナント固有の設定を無効にすることもできます。例: quarkus.oidc.tenant-a.tenant-enabled=false