Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select OIDC tenant using annotations #23086

Merged
merged 1 commit into from
Feb 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,53 @@ You should be redirected again to the login page at Keycloak. However, now you a
In both cases, if the user is successfully authenticated, the landing page will show the user's name and e-mail. Even though
user `alice` exists in both tenants, for the application they are distinct users belonging to different realms/tenants.

== Resolving Tenant Identifiers with Annotations

You can use the annotations and CDI interceptors for resolving the tenant identifiers as an alternative to using
`quarkus.oidc.TenantResolver`. This can be done by setting the value for the key `OidcUtils.TENANT_ID_ATTRIBUTE` on
the current `RoutingContext`.

Assuming your application supports two OIDC tenants (`hr`, and default) first you need to define one
annotation per tenant ID other than default:

[NOTE]
====
Proactive HTTP authentication needs to be disabled (`quarkus.http.auth.proactive=false`) for this to work. See
xref:security-built-in-authentication.adoc#proactive-authentication[Proactive Authentication] section for further details.
====

[source,java]
----
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface HrTenant {
}
----

Next, you'll need one interceptor for each of those annotations:

[source,java]
----
@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();
}
}
----

Now all methods and classes carrying `@HrTenant` 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.

== Programmatically Resolving Tenants Configuration

If you need a more dynamic configuration for the different tenants you want to support and don't want to end up with multiple
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,21 @@ public Uni<? extends TenantConfigContext> apply(TenantConfigContext tenantConfig

private TenantConfigContext getStaticTenantContext(RoutingContext context) {

String tenantId = null;
String tenantId = context.get(CURRENT_STATIC_TENANT_ID);

if (tenantResolver.isResolvable()) {
tenantId = context.get(CURRENT_STATIC_TENANT_ID);
if (tenantId == null && context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) {
if (tenantId == null && context.get(CURRENT_STATIC_TENANT_ID_NULL) == null) {
if (tenantResolver.isResolvable()) {
tenantId = tenantResolver.get().resolve(context);
if (tenantId != null) {
context.put(CURRENT_STATIC_TENANT_ID, tenantId);
} else {
context.put(CURRENT_STATIC_TENANT_ID_NULL, true);
}
}
if (tenantId == null) {
tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
}
}

if (tenantId != null) {
context.put(CURRENT_STATIC_TENANT_ID, tenantId);
} else {
context.put(CURRENT_STATIC_TENANT_ID_NULL, true);
}

TenantConfigContext configContext = tenantId != null ? tenantConfigBean.getStaticTenantsConfig().get(tenantId) : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.it.keycloak;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

import io.quarkus.oidc.runtime.OidcUtils;
import io.vertx.ext.web.RoutingContext;

@Interceptor
@HrTenant
public class HrInterceptor {
@Inject
RoutingContext routingContext;

@AroundInvoke
Object setTenant(InvocationContext context) throws Exception {
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, "hr");
return context.proceed();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.it.keycloak;

import java.lang.annotation.*;

import javax.interceptor.InterceptorBinding;

@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface HrTenant {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.quarkus.it.keycloak;

import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.oidc.runtime.OidcUtils;
import io.quarkus.security.Authenticated;
import io.vertx.ext.web.RoutingContext;

@HrTenant
@Authenticated
@Path("/api/tenant-echo")
public class TenantEchoResource {

@Inject
RoutingContext routingContext;

@GET
@Produces(MediaType.TEXT_PLAIN)
public Map<String, String> getTenant() {
return Stream.of(
"static.tenant.id",
OidcUtils.TENANT_ID_ATTRIBUTE)
.collect(Collectors.toMap(Function.identity(), key -> "" + routingContext.get(key)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.it.keycloak;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;

import org.hamcrest.core.StringContains;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;

@QuarkusTest
@TestProfile(AnnotationBasedTenantTest.NoProactiveAuthTestProfile.class)
@QuarkusTestResource(OidcWiremockTestResource.class)
public class AnnotationBasedTenantTest {
public static class NoProactiveAuthTestProfile implements QuarkusTestProfile {
public Map<String, String> getConfigOverrides() {
return Map.of("quarkus.http.auth.proactive", "false");
}
}

@Test
public void test() {
String token = OidcWiremockTestResource.getAccessToken("alice", new HashSet<>(Arrays.asList("user", "admin")));

// Server is starting now
WiremockTestResource server = new WiremockTestResource();
server.start();
try {
RestAssured.given().auth().oauth2(token)
.when().get("/api/tenant-echo")
.then().statusCode(200)
.body(StringContains.containsString("tenant-id=hr"))
.body(StringContains.containsString("static.tenant.id=hr"));
} finally {
server.stop();
}
}
}