Skip to content

Commit 8535990

Browse files
authored
Merge pull request quarkusio#48278 from sberyozkin/update_resolved_dynamic_oidc_config
Allow to update resolved OIDC dynamic tenant configurations
2 parents a94a565 + 8066339 commit 8535990

File tree

11 files changed

+316
-31
lines changed

11 files changed

+316
-31
lines changed

docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,97 @@ You can populate it by using any settings supported by the `quarkus-oidc` extens
735735

736736
If the dynamic tenant resolver returns `null`, a <<static-tenant-resolution>> is attempted next.
737737

738+
==== Update resolved dynamic tenant configuration
739+
740+
It may be necessary to update the already resolved tenant configuration.
741+
For example, a client secret may have to be updated following a client secret update in the registered OIDC application.
742+
743+
To update the configuration, use `OidcTenantConfigBuilder` to create a new instance of `OidcTenantConfig` and modify it as required before returning it:
744+
745+
[source,java]
746+
----
747+
package org.acme;
748+
749+
import jakarta.enterprise.context.ApplicationScoped;
750+
import jakarta.inject.Inject;
751+
752+
import io.quarkus.oidc.OidcRequestContext;
753+
import io.quarkus.oidc.OidcTenantConfig;
754+
import io.quarkus.oidc.TenantConfigResolver;
755+
import io.quarkus.oidc.runtime.TenantConfigBean;
756+
import io.smallrye.mutiny.Uni;
757+
import io.vertx.ext.web.RoutingContext;
758+
759+
@ApplicationScoped
760+
public class CustomTenantConfigResolver implements TenantConfigResolver {
761+
762+
@Inject
763+
TenantConfigBean tenantConfigBean;
764+
765+
@Override
766+
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
767+
768+
// Check request path or request context `tenant-id` property to determine the tenant id.
769+
770+
var currentTenantConfig = tenantConfigBean.getDynamicTenant("some-dynamic-tenant-id").getOidcTenantConfig(); <1>
771+
if (currentTenantConfig != null
772+
&& "name".equals(currentTenantConfig.token().principalClaim().get())) { <2>
773+
// This is an original configuration, update it now:
774+
OidcTenantConfig updatedConfig = OidcTenantConfig.builder(currentTenantConfig) <3>
775+
.token().principalClaim("email").end()
776+
.build();
777+
778+
return Uni.createFrom().item(updatedConfig);
779+
}
780+
// create an initial configuration for the tenant
781+
OidcTenantConfig config = OidcTenantConfig.builder() <4>
782+
.token().principalClaim("name").end()
783+
// set other properties
784+
.build();
785+
return Uni.createFrom().item(config);
786+
}
787+
}
788+
----
789+
<1> Use `io.quarkus.oidc.runtime.TenantConfigBean` to get the already resolved tenant configuration. Alternatively, you can use the tenant `OidcTenantConfig` cached in your resolver.
790+
<2> You may want to check if this configuration has already been updated, to avoid multiple redundant updates, for example, due to multliple redirects.
791+
<3> Use the resolved configuration to create a builder and update it as required.
792+
<4> Create an initial configuration if no configuration already exists.
793+
794+
This is all you have to do update the already resolved dynamic tenantr configuration, without having to reconnect to the provider.
795+
796+
If reconnecting is necessary, for example, the `UserInfo` endpoint address may have changed for the tenant to rediscover it,
797+
then simply set a `RoutingContext` `replace-tenant-configuration-context` property to `true`:
798+
799+
[source,java]
800+
----
801+
@Override
802+
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
803+
OidcTenantConfig updatedConfig = OidcTenantConfig.builder(currentTenantConfig)
804+
.token().principalClaim("email").end()
805+
.build();
806+
context.put("replace-tenant-configuration-context", "true"); <1>
807+
808+
return Uni.createFrom().item(updatedConfig);
809+
}
810+
----
811+
<1> Replace the resolved tenant configuration and re-connect to the provider
812+
813+
Finally, if you decide to update the resoved configuration while the existing OIDC session is still active, you may to have the session cookie removed and the user re-authenticated to align with the latest tenant configuration requirements. Set a `RoutingContext` `remove-session-cookie` property to `true` if it is necessary:
814+
815+
[source,java]
816+
----
817+
@Override
818+
public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
819+
OidcTenantConfig updatedConfig = OidcTenantConfig.builder(currentTenantConfig)
820+
.token().principalClaim("email").end()
821+
.build();
822+
context.put("remove-session-cookie", "true"); <1>
823+
824+
return Uni.createFrom().item(updatedConfig);
825+
}
826+
----
827+
<1> Update the tenant configuration, remove the session cookie and triger the user re-authentication. If possible, prefer to get the user log-out first, instead of triggering the re-authentication at the tenant resolution time.
828+
738829
[[static-tenant-resolution]]
739830
=== Static tenant configuration resolution
740831

@@ -812,9 +903,9 @@ quarkus.oidc.b.tenant-paths=/*/hello <3>
812903
TIP: Path-matching mechanism works exactly same as in the xref:security-authorize-web-endpoints-reference.adoc#authorization-using-configuration[Authorization using configuration].
813904

814905
[[default-tenant-resolver]]
815-
==== Use last request path segment as tenant id
906+
==== Use request path segments to find tenant id
816907

817-
The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in the last segment of the request path.
908+
The default resolution for a tenant identifier is convention based, whereby the authentication request must include the tenant identifier in one of the path segments of the request path.
818909

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

@@ -845,6 +936,7 @@ quarkus.http.auth.permission.login.policy=authenticated
845936
----
846937

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

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3035,11 +3035,9 @@ public static OidcTenantConfigBuilder builder() {
30353035
}
30363036

30373037
/**
3038-
* Creates {@link OidcTenantConfigBuilder} builder populated with {@code staticTenantMapping} values.
3039-
* You want to use this constructor when you have configured static tenant in the application.properties
3040-
* and your dynamic tenant only differ in a couple of the configuration properties.
3038+
* Creates {@link OidcTenantConfigBuilder} builder from the existing {@link io.quarkus.oidc.runtime.OidcTenantConfig}
30413039
*
3042-
* @param mapping OidcTenantConfig created by the SmallRye Config; must not be null
3040+
* @param mapping existing io.quarkus.oidc.runtime.OidcTenantConfig
30433041
*/
30443042
public static OidcTenantConfigBuilder builder(io.quarkus.oidc.runtime.OidcTenantConfig mapping) {
30453043
return new OidcTenantConfigBuilder(mapping);
@@ -3049,15 +3047,15 @@ public static OidcTenantConfigBuilder builder(io.quarkus.oidc.runtime.OidcTenant
30493047
* Creates {@link OidcTenantConfig} from the {@code mapping}. This method is more efficient than
30503048
* the {@link #builder()} method if you don't need to modify the {@code mapping}.
30513049
*
3052-
* @param mapping tenant config as returned from the SmallRye Config; must not be null
3050+
* @param mapping existing io.quarkus.oidc.runtime.OidcTenantConfig
30533051
* @return OidcTenantConfig
30543052
*/
30553053
public static OidcTenantConfig of(io.quarkus.oidc.runtime.OidcTenantConfig mapping) {
30563054
return new OidcTenantConfig(mapping);
30573055
}
30583056

30593057
/**
3060-
* Creates {@link OidcTenantConfigBuilder} builder populated with documented default values.
3058+
* Creates {@link OidcTenantConfigBuilder} builder populated with documented default values and the provided base URL.
30613059
*
30623060
* @param authServerUrl {@link #authServerUrl()}
30633061
* @return OidcTenantConfigBuilder builder
@@ -3067,7 +3065,8 @@ public static OidcTenantConfigBuilder authServerUrl(String authServerUrl) {
30673065
}
30683066

30693067
/**
3070-
* Creates {@link OidcTenantConfigBuilder} builder populated with documented default values.
3068+
* Creates {@link OidcTenantConfigBuilder} builder populated with documented default values and the provided client
3069+
* registration path.
30713070
*
30723071
* @param registrationPath {@link #registrationPath()}
30733072
* @return OidcTenantConfigBuilder builder
@@ -3077,7 +3076,7 @@ public static OidcTenantConfigBuilder registrationPath(String registrationPath)
30773076
}
30783077

30793078
/**
3080-
* Creates {@link OidcTenantConfigBuilder} builder populated with documented default values.
3079+
* Creates {@link OidcTenantConfigBuilder} builder populated with documented default values and the provided token path.
30813080
*
30823081
* @param tokenPath {@link #tokenPath()}
30833082
* @return OidcTenantConfigBuilder builder

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TenantConfigResolver.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
* Instead of implementing a {@link TenantResolver} that maps the tenant configuration based on an identifier and its
1212
* corresponding entry in the application configuration file, beans implementing this interface can dynamically construct the
1313
* tenant configuration without having to define each tenant in the application configuration file.
14+
* <p>
15+
* If the resolved tenant configuration must be updated, do not modify it in the resolver because it is not thread-safe.
16+
* Use {@link OidcTenantConfig#builder(io.quarkus.oidc.runtime.OidcTenantConfig)} to copy the resolved configuration,
17+
* modify it as required, and build a new configuration instance instead.
1418
*/
1519
public interface TenantConfigResolver {
1620

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.quarkus.oidc.TokenStateManager;
2525
import io.quarkus.oidc.UserInfo;
2626
import io.quarkus.oidc.UserInfoCache;
27+
import io.quarkus.security.AuthenticationFailedException;
2728
import io.quarkus.security.identity.SecurityIdentity;
2829
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
2930
import io.quarkus.security.spi.runtime.SecurityEventHelper;
@@ -37,6 +38,8 @@ public class DefaultTenantConfigResolver {
3738
private static final String CURRENT_STATIC_TENANT_ID = "static.tenant.id";
3839
private static final String CURRENT_STATIC_TENANT_ID_NULL = "static.tenant.id.null";
3940
private static final String CURRENT_DYNAMIC_TENANT_CONFIG = "dynamic.tenant.config";
41+
private static final String REPLACE_TENANT_CONFIG_CONTEXT = "replace-tenant-configuration-context";
42+
private static final String REMOVE_SESSION_COOKIE = "remove-session-cookie";
4043
private final ConcurrentHashMap<String, BackChannelLogoutTokenCache> backChannelLogoutTokens = new ConcurrentHashMap<>();
4144
private final BlockingTaskRunner<OidcTenantConfig> blockingRequestContext;
4245
private final boolean securityEventObserved;
@@ -259,13 +262,46 @@ private Uni<TenantConfigContext> getDynamicTenantContext(RoutingContext context)
259262

260263
return getDynamicTenantConfig(context).chain(new Function<OidcTenantConfig, Uni<? extends TenantConfigContext>>() {
261264
@Override
262-
public Uni<? extends TenantConfigContext> apply(OidcTenantConfig tenantConfig) {
265+
public Uni<TenantConfigContext> apply(OidcTenantConfig tenantConfig) {
263266
if (tenantConfig != null) {
264267
var tenantId = tenantConfig.tenantId()
265268
.orElseThrow(() -> new OIDCException("Tenant configuration must have tenant id"));
266269
var tenantContext = tenantConfigBean.getDynamicTenant(tenantId);
267270
if (tenantContext == null) {
268271
return tenantConfigBean.createDynamicTenantContext(tenantConfig);
272+
} else if (tenantContext.getOidcTenantConfig() != tenantConfig) {
273+
274+
Uni<TenantConfigContext> dynamicContextUni = null;
275+
if (Boolean.valueOf(context.get(REPLACE_TENANT_CONFIG_CONTEXT))) {
276+
// replace the context and reconnect
277+
dynamicContextUni = tenantConfigBean.replaceDynamicTenantContext(tenantConfig);
278+
} else {
279+
// update the context without reconnect
280+
dynamicContextUni = tenantConfigBean.updateDynamicTenantContext(tenantConfig);
281+
}
282+
final Uni<TenantConfigContext> contextUni = dynamicContextUni;
283+
if (Boolean.valueOf(context.get(REMOVE_SESSION_COOKIE))) {
284+
final String message = """
285+
Requesting re-authentication for the tenant %s to align with the new dynamic tenant context requirements.
286+
"""
287+
.formatted(tenantId);
288+
LOG.debug(message);
289+
// Clear the session cookie using the current configuration
290+
return Uni.createFrom().item(tenantContext.getOidcTenantConfig())
291+
.chain(new Function<OidcTenantConfig, Uni<? extends Void>>() {
292+
@Override
293+
public Uni<Void> apply(OidcTenantConfig oidcConfig) {
294+
OidcUtils.setClearSiteData(context, oidcConfig);
295+
return OidcUtils.removeSessionCookie(context, oidcConfig, tokenStateManager.get());
296+
}
297+
})
298+
// Deal with updating or replacing the dynamic context
299+
.chain(() -> contextUni)
300+
// Finally, request re-authentication
301+
.onItem().failWith(() -> new AuthenticationFailedException(message));
302+
} else {
303+
return dynamicContextUni;
304+
}
269305
} else {
270306
return Uni.createFrom().item(tenantContext);
271307
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/LazyTenantConfigContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.security.Key;
44
import java.util.List;
5+
import java.util.Map;
56
import java.util.function.Supplier;
67

78
import javax.crypto.SecretKey;
@@ -90,4 +91,9 @@ public Key getTokenDecryptionKey() {
9091
public List<OidcRedirectFilter> getOidcRedirectFilters(Redirect.Location loc) {
9192
return delegate.getOidcRedirectFilters(loc);
9293
}
94+
95+
@Override
96+
public Map<Redirect.Location, List<OidcRedirectFilter>> getLocationToRedirectFilters() {
97+
return delegate.getLocationToRedirectFilters();
98+
}
9399
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import jakarta.enterprise.context.spi.CreationalContext;
88

9+
import org.jboss.logging.Logger;
10+
911
import io.quarkus.arc.BeanDestroyer;
1012
import io.quarkus.oidc.OidcTenantConfig;
1113
import io.quarkus.tls.TlsConfigurationRegistry;
@@ -14,6 +16,8 @@
1416

1517
public final class TenantConfigBean {
1618

19+
private static final Logger LOG = Logger.getLogger(TenantConfigBean.class);
20+
1721
private final Map<String, TenantConfigContext> staticTenantsConfig;
1822
private final Map<String, TenantConfigContext> dynamicTenantsConfig;
1923
private final TenantConfigContext defaultTenant;
@@ -30,7 +34,7 @@ public final class TenantConfigBean {
3034
oidc.getDefaultTenantConfig());
3135
}
3236

33-
public Uni<TenantConfigContext> createDynamicTenantContext(OidcTenantConfig oidcConfig) {
37+
Uni<TenantConfigContext> createDynamicTenantContext(OidcTenantConfig oidcConfig) {
3438
var tenantId = oidcConfig.tenantId().orElseThrow();
3539

3640
var tenant = dynamicTenantsConfig.get(tenantId);
@@ -48,6 +52,26 @@ public TenantConfigContext apply(TenantConfigContext t) {
4852
});
4953
}
5054

55+
Uni<TenantConfigContext> updateDynamicTenantContext(OidcTenantConfig oidcConfig) {
56+
var tenantId = oidcConfig.tenantId().orElseThrow();
57+
var tenant = dynamicTenantsConfig.get(tenantId);
58+
if (tenant != null) {
59+
LOG.debugf("Updating the resolved tenant %s configuration with a new configuration", tenantId);
60+
var newTenant = new TenantConfigContextImpl(tenant, oidcConfig);
61+
dynamicTenantsConfig.put(tenantId, newTenant);
62+
return Uni.createFrom().item(newTenant);
63+
} else {
64+
return createDynamicTenantContext(oidcConfig);
65+
}
66+
}
67+
68+
Uni<TenantConfigContext> replaceDynamicTenantContext(OidcTenantConfig oidcConfig) {
69+
var tenantId = oidcConfig.tenantId().orElseThrow();
70+
LOG.debugf("Replacing the resolved tenant %s configuration with a new configuration", tenantId);
71+
dynamicTenantsConfig.remove(tenantId);
72+
return createDynamicTenantContext(oidcConfig);
73+
}
74+
5175
public Map<String, TenantConfigContext> getStaticTenantsConfig() {
5276
return staticTenantsConfig;
5377
}

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.security.Key;
44
import java.util.List;
5+
import java.util.Map;
56
import java.util.function.Supplier;
67

78
import javax.crypto.SecretKey;
@@ -42,6 +43,8 @@ public sealed interface TenantConfigContext permits TenantConfigContextImpl, Laz
4243

4344
List<OidcRedirectFilter> getOidcRedirectFilters(Redirect.Location loc);
4445

46+
Map<Redirect.Location, List<OidcRedirectFilter>> getLocationToRedirectFilters();
47+
4548
/**
4649
* Only static tenants that are not {@link #ready()} can and need to be initialized.
4750
*

extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContextImpl.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ final class TenantConfigContextImpl implements TenantConfigContext {
8181
tokenDecryptionKey = providerIsNoNull(provider) ? createTokenDecryptionKey(provider) : null;
8282
}
8383

84+
TenantConfigContextImpl(TenantConfigContext tenantConfigContext, OidcTenantConfig oidcConfig) {
85+
this.oidcConfig = oidcConfig;
86+
this.ready = tenantConfigContext.ready();
87+
this.provider = tenantConfigContext.provider();
88+
this.sessionCookieEncryptionKey = tenantConfigContext.getSessionCookieEncryptionKey();
89+
this.stateCookieEncryptionKey = tenantConfigContext.getStateCookieEncryptionKey();
90+
this.internalIdTokenSigningKey = tenantConfigContext.getInternalIdTokenSigningKey();
91+
this.redirectFilters = tenantConfigContext.getLocationToRedirectFilters();
92+
this.tokenDecryptionKey = tenantConfigContext.getTokenDecryptionKey();
93+
}
94+
95+
@Override
96+
public Map<Redirect.Location, List<OidcRedirectFilter>> getLocationToRedirectFilters() {
97+
return redirectFilters;
98+
}
99+
84100
private static boolean providerIsNoNull(OidcProvider provider) {
85101
return provider != null && provider.client != null;
86102
}

0 commit comments

Comments
 (0)