diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc index b2e24e4553944..315afd99f55be 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication.adoc @@ -1464,6 +1464,37 @@ public class DiscoveryEndpointResponseFilter implements OidcResponseFilter { <3> Use `OidcRequestContextProperties` request properties to get the tenant id. <4> Get the response data as String. +== Programmatic OIDC start-up + +OIDC tenants can be created programmatically like in the example below: + +[source,java] +---- +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) { + oidc.create(OidcTenantConfig.authServerUrl("http://localhost:8180/realms/quarkus").build()); + } + +} +---- + +The code above is a programmatic equivalent to the following configuration in the `application.properties` file: + +[source,properties] +---- +quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus +---- + +For more complex setup involving multiple tenants please see the xref:security-openid-connect-multitenancy.adoc#programmatic-startup[Programmatic OIDC start-up for multitenant application] +section of the OpenID Connect Multi-Tenancy guide. + == References * xref:security-oidc-configuration-properties-reference.adoc[OIDC configuration properties] diff --git a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc index 967ac4335e01b..af9ff2ad8d372 100644 --- a/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc +++ b/docs/src/main/asciidoc/security-oidc-code-flow-authentication.adoc @@ -2049,6 +2049,47 @@ quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE From the `quarkus dev` console, type `j` to change the application global log level. +== Programmatic OIDC start-up + +OIDC tenants can be created programmatically like in the example below: + +[source,java] +---- +package io.quarkus.it.oidc; + +import io.quarkus.oidc.Oidc; +import io.quarkus.oidc.OidcTenantConfig; +import jakarta.enterprise.event.Observes; + +import static io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.WEB_APP; + +public class OidcStartup { + + void observe(@Observes Oidc oidc) { + oidc.create(OidcTenantConfig + .authServerUrl("http://localhost:8180/realms/quarkus") + .applicationType(WEB_APP) + .clientId("quarkus-app") + .credentials().clientSecret("mysecret").end() + .build()); + } + +} +---- + +The code above is a programmatic equivalent to the following configuration in the `application.properties` file: + +[source,properties] +---- +quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus +quarkus.oidc.application-type=web-app +quarkus.oidc.client-id=quarkus-app +quarkus.oidc.credentials.secret=mysecret +---- + +For more complex setup involving multiple tenants please see the xref:security-openid-connect-multitenancy.adoc#programmatic-startup[Programmatic OIDC start-up for multitenant application] +section of the OpenID Connect Multi-Tenancy guide. + == References * xref:security-oidc-configuration-properties-reference.adoc[OIDC configuration properties] diff --git a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc index b30bfd2ebce64..8549ed1f7e203 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -1110,6 +1110,111 @@ The default tenant configuration is automatically disabled when `quarkus.oidc.au Be aware that tenant-specific configurations can also be disabled, for example: `quarkus.oidc.tenant-a.tenant-enabled=false`. +[[programmatic-startup]] +== Programmatic OIDC start-up for multitenant application + +Static OIDC tenants can be created programmatically like in the example below: + +[source,java] +---- +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()); + oidc.create(OidcTenantConfig.authServerUrl("http://localhost:8180/realms/tenant-two").tenantId("tenant-two").build()); + } + +} +---- +<1> Static tenants must be created in a synchronous manner from the observer method. + +The code above is a programmatic equivalent to the following configuration in the `application.properties` file: + +[source,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 +---- + +You can also create static tenants based on the tenants configured in the `application.properties` file. +For example, if you need to add only tenant paths programmatically, you can: + +.Static application.properties +[source,properties] +---- +quarkus.oidc.tenant-two.auth-server-url=http://localhost:8180/realms/tenant-two +quarkus.oidc.tenant-two.tenant-paths=/api/tenant-two/* +---- + +.Additional property set programmatically +[source,java] +---- +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, OidcConfig config) { + oidc.create(OidcTenantConfig + .builder(config.namedTenants().get("tenant-two")) + .tenantPaths(getAdditionalTenantPath()) <1> + .build() + ); + } + + private String getAdditionalTenantPath() { + return "/additional-tenant-path"; + } + +} +---- +<1> The tenant `tenant-two` will have two paths, `/additional-tenant-path` and `/api/tenant-two/*`. + +Dynamic OIDC tenants resolved with the <> can be created whenever necessary: + +[source,java] +---- +package io.quarkus.it.oidc; + +import io.quarkus.oidc.Oidc; +import io.quarkus.oidc.OidcTenantConfig; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class OidcConfigurationService { + + @Inject + Oidc oidc; + + public void createDynamicTenant(String authServerUrl) { + oidc.create(createConfig(authServerUrl)); + } + + public Uni createDynamicTenantNonBlocking(String authServerUrl) { + return oidc.createAsync(createConfig(authServerUrl)); <1> + } + + private static OidcTenantConfig createConfig(String authServerUrl) { + return OidcTenantConfig.authServerUrl(authServerUrl).tenantId("tenant-three").build(); <2> + } +} +---- +<1> Use the `createAsync` method to create a dynamic tenant on an IO thread. +<2> The tenant `tenant-three` will not be available until you create it. +Please make sure you create tenants during the application start-up. +Creating tenants after your application has started would typically be a bad idea, only useful in special cases. + == References * xref:security-oidc-configuration-properties-reference.adoc[OIDC configuration properties] diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java index fdc01c61b6543..71f8b9afe237b 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/DefaultPolicyEnforcerResolver.java @@ -8,8 +8,8 @@ import java.util.function.Function; import java.util.function.Supplier; +import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; -import jakarta.inject.Singleton; import org.keycloak.adapters.authorization.PolicyEnforcer; @@ -19,14 +19,14 @@ import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.common.runtime.OidcTlsSupport; import io.quarkus.oidc.runtime.BlockingTaskRunner; -import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.vertx.http.runtime.HttpConfiguration; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; -@Singleton +@ApplicationScoped public class DefaultPolicyEnforcerResolver implements PolicyEnforcerResolver { private final TenantPolicyConfigResolver dynamicConfigResolver; @@ -36,7 +36,7 @@ public class DefaultPolicyEnforcerResolver implements PolicyEnforcerResolver { private final long readTimeout; private final OidcTlsSupport tlsSupport; - DefaultPolicyEnforcerResolver(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config, + DefaultPolicyEnforcerResolver(TenantConfigBean tenantConfigBean, KeycloakPolicyEnforcerConfig config, HttpConfiguration httpConfiguration, BlockingSecurityExecutor blockingSecurityExecutor, Instance configResolver, InjectableInstance tlsConfigRegistryInstance) { @@ -48,11 +48,11 @@ public class DefaultPolicyEnforcerResolver implements PolicyEnforcerResolver { this.tlsSupport = OidcTlsSupport.empty(); } - var defaultTenantConfig = OidcConfig.getDefaultTenant(oidcConfig); + var defaultTenantConfig = tenantConfigBean.getDefaultTenant().oidcConfig(); var defaultTenantTlsSupport = tlsSupport.forConfig(defaultTenantConfig.tls()); this.defaultPolicyEnforcer = createPolicyEnforcer(defaultTenantConfig, config.defaultTenant(), defaultTenantTlsSupport); - this.namedPolicyEnforcers = createNamedPolicyEnforcers(oidcConfig, config, tlsSupport); + this.namedPolicyEnforcers = createNamedPolicyEnforcers(tenantConfigBean, config, tlsSupport); if (configResolver.isResolvable()) { this.dynamicConfigResolver = configResolver.get(); this.requestContext = new BlockingTaskRunner<>(blockingSecurityExecutor); @@ -105,7 +105,7 @@ public PolicyEnforcer apply(KeycloakPolicyEnforcerTenantConfig tenant) { }); } - private static Map createNamedPolicyEnforcers(OidcConfig oidcConfig, + private static Map createNamedPolicyEnforcers(TenantConfigBean tenantConfigBean, KeycloakPolicyEnforcerConfig config, OidcTlsSupport tlsSupport) { if (config.namedTenants().isEmpty()) { return Map.of(); @@ -113,7 +113,7 @@ private static Map createNamedPolicyEnforcers(OidcConfig Map policyEnforcerTenants = new HashMap<>(); for (Map.Entry tenant : config.namedTenants().entrySet()) { - var oidcTenantConfig = getOidcTenantConfig(oidcConfig, tenant.getKey()); + var oidcTenantConfig = getOidcTenantConfig(tenantConfigBean, tenant.getKey()); policyEnforcerTenants.put(tenant.getKey(), createPolicyEnforcer(oidcTenantConfig, tenant.getValue(), tlsSupport.forConfig(oidcTenantConfig.tls()))); } diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java index 39b84b9971088..e9134fab7f01b 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerUtil.java @@ -19,8 +19,8 @@ import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.common.runtime.OidcTlsSupport.TlsConfigSupport; import io.quarkus.oidc.common.runtime.config.OidcCommonConfig; -import io.quarkus.oidc.runtime.OidcConfig; import io.quarkus.oidc.runtime.OidcTenantConfig; +import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.runtime.configuration.ConfigurationException; public final class KeycloakPolicyEnforcerUtil { @@ -224,15 +224,15 @@ private static boolean isNotComplexConfigKey(String key) { return !key.contains("."); } - static OidcTenantConfig getOidcTenantConfig(OidcConfig oidcConfig, String tenant) { + static OidcTenantConfig getOidcTenantConfig(TenantConfigBean tenantConfigBean, String tenant) { if (tenant == null || DEFAULT_TENANT_ID.equals(tenant)) { - return OidcConfig.getDefaultTenant(oidcConfig); + return tenantConfigBean.getDefaultTenant().getOidcTenantConfig(); } - var oidcTenantConfig = oidcConfig.namedTenants().get(tenant); - if (oidcTenantConfig == null) { + var staticTenant = tenantConfigBean.getStaticTenant(tenant); + if (staticTenant == null || staticTenant.oidcConfig() == null) { throw new ConfigurationException("Failed to find a matching OidcTenantConfig for tenant: " + tenant); } - return oidcTenantConfig; + return staticTenant.oidcConfig(); } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index 04515540e12d3..c402c2f658a91 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -29,13 +29,13 @@ import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem; import io.quarkus.arc.deployment.InjectionPointTransformerBuildItem; import io.quarkus.arc.deployment.QualifierRegistrarBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; -import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem; import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuildExtension; @@ -51,13 +51,14 @@ import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.annotations.Consume; import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Produce; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigBuilderBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; +import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.oidc.AuthorizationCodeFlow; import io.quarkus.oidc.BearerTokenAuthentication; import io.quarkus.oidc.IdToken; @@ -76,6 +77,7 @@ import io.quarkus.oidc.runtime.OidcConfig; import io.quarkus.oidc.runtime.OidcConfigurationMetadataProducer; import io.quarkus.oidc.runtime.OidcIdentityProvider; +import io.quarkus.oidc.runtime.OidcImpl; import io.quarkus.oidc.runtime.OidcJsonWebTokenProducer; import io.quarkus.oidc.runtime.OidcRecorder; import io.quarkus.oidc.runtime.OidcSessionImpl; @@ -84,9 +86,9 @@ import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer; -import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem; import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem; import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig; @@ -167,8 +169,7 @@ AdditionalBeanBuildItem jwtClaimIntegration(Capabilities capabilities) { } @BuildStep - public void additionalBeans(BuildProducer additionalBeans, - BuildProducer reflectiveClasses) { + public void additionalBeans(BuildProducer additionalBeans) { AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder().setUnremovable(); builder.addBeanClass(OidcAuthenticationMechanism.class) @@ -180,7 +181,9 @@ public void additionalBeans(BuildProducer additionalBea .addBeanClass(DefaultTokenStateManager.class) .addBeanClass(OidcSessionImpl.class) .addBeanClass(BackChannelLogoutHandler.class) - .addBeanClass(AzureAccessTokenCustomizer.class); + .addBeanClass(AzureAccessTokenCustomizer.class) + .addBeanClass(TenantConfigBean.class) + .addBeanClass(OidcImpl.class); additionalBeans.produce(builder.build()); } @@ -303,28 +306,21 @@ private static boolean isTenantIdentityProviderType(InjectionPointInfo ip) { return TENANT_IDENTITY_PROVIDER_NAME.equals(ip.getRequiredType().name()); } - @Record(ExecutionTime.RUNTIME_INIT) + @Record(ExecutionTime.STATIC_INIT) @BuildStep - public SyntheticBeanBuildItem setup( - BeanRegistrationPhaseBuildItem beanRegistration, - OidcConfig config, - OidcRecorder recorder, - CoreVertxBuildItem vertxBuildItem, - TlsRegistryBuildItem tlsRegistryBuildItem) { - return SyntheticBeanBuildItem.configure(TenantConfigBean.class).unremovable().types(TenantConfigBean.class) - .supplier(recorder.createTenantConfigBean(config, vertxBuildItem.getVertx(), - tlsRegistryBuildItem.registry(), detectUserInfoRequired(beanRegistration))) - .destroyer(TenantConfigBean.Destroyer.class) - .scope(Singleton.class) // this should have been @ApplicationScoped but fails for some reason - .setRuntimeInit() - .done(); + void detectIfUserInfoRequired(OidcRecorder recorder, BeanRegistrationPhaseBuildItem beanRegistration) { + recorder.setUserInfoInjectionPointDetected(detectUserInfoRequired(beanRegistration)); } - @Consume(SyntheticBeansRuntimeInitBuildItem.class) + // this ensures we initialize OIDC before HTTP router is finalized + // because we need TenantConfigBean in the BackChannelLogoutHandler + @Produce(FilterBuildItem.class) + @Consume(RuntimeConfigSetupCompleteBuildItem.class) + @Consume(BeanContainerBuildItem.class) @Record(ExecutionTime.RUNTIME_INIT) @BuildStep - void initTenantConfigBean(OidcRecorder recorder) { - recorder.initTenantConfigBean(); + void initOidc(OidcRecorder recorder) { + recorder.initOidc(); } @BuildStep diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProgrammaticDynamicTenantTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProgrammaticDynamicTenantTest.java new file mode 100644 index 0000000000000..9f4e886beba8c --- /dev/null +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProgrammaticDynamicTenantTest.java @@ -0,0 +1,90 @@ +package io.quarkus.oidc.test; + +import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.oidc.Oidc; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.Tenant; +import io.quarkus.security.Authenticated; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.keycloak.client.KeycloakTestClient; +import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; +import io.restassured.RestAssured; +import io.vertx.ext.web.RoutingContext; + +@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) +public class ProgrammaticDynamicTenantTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClass(DynamicTenantResource.class) + .addAsResource(new StringAsset(""" + quarkus.keycloak.devservices.enabled=false + quarkus.http.auth.proactive=false + quarkus.log.category."io.quarkus.oidc.runtime.DefaultTenantConfigResolver".level=DEBUG + """), "application.properties")) + // 'dynamic-tenant' doesn't exist at first, so expect it being logged + .setLogRecordPredicate(logRecord -> logRecord.getMessage() != null && logRecord.getMessage() + .contains("not provided the configuration for tenant") + && "dynamic-tenant".equals(logRecord.getParameters()[0])) + .assertLogRecords(logRecords -> Assertions.assertFalse(logRecords.isEmpty())); + + @Test + public void testDynamicTenantCreation() { + // get tenant - now the tenant is not created, so let's verify the default tenant is used + // we also expect that log message asserted above saying the dynamic-tenant was resolved but not found + RestAssured.given().auth().oauth2(getAccessToken()).get("/dynamic-tenant").then().statusCode(200) + .body(Matchers.is(DEFAULT_TENANT_ID)); + // now we create the dynamic tenant + RestAssured.given().post("/dynamic-tenant").then().statusCode(204); + RestAssured.given().auth().oauth2(getAccessToken()).get("/dynamic-tenant").then().statusCode(200) + .body(Matchers.is("dynamic-tenant")); + } + + private static String getAccessToken() { + return new KeycloakTestClient().getAccessToken("alice", "alice", "quarkus-service-app", "secret", List.of("openid")); + } + + @Path("dynamic-tenant") + public static class DynamicTenantResource { + + @Inject + Oidc oidc; + + @ConfigProperty(name = "quarkus.oidc.auth-server-url") + String serverUrl; + + @Inject + RoutingContext routingContext; + + @POST + public void create() { + // this could use the synchronous method, but want to test the async one and RESTEasy doesn't support Mutiny + oidc.createAsync(OidcTenantConfig.authServerUrl(serverUrl).tenantId("dynamic-tenant").build()).await() + .indefinitely(); + } + + @Tenant("dynamic-tenant") + @Authenticated + @GET + public String getTenantId() { + return routingContext. get(OidcTenantConfig.class.getName()).tenantId().get(); + } + + } +} diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/UserInfoRequiredDetectionTest.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/UserInfoRequiredDetectionTest.java index b809ea73d3822..8ac08349c896d 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/UserInfoRequiredDetectionTest.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/UserInfoRequiredDetectionTest.java @@ -7,13 +7,17 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import org.eclipse.microprofile.config.inject.ConfigProperty; import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.oidc.Oidc; import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.UserInfo; +import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.security.PermissionsAllowed; import io.quarkus.test.QuarkusDevModeTest; @@ -22,6 +26,7 @@ import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager; import io.restassured.RestAssured; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; @QuarkusTestResource(KeycloakTestResourceLifecycleManager.class) public class UserInfoRequiredDetectionTest { @@ -29,15 +34,11 @@ public class UserInfoRequiredDetectionTest { @RegisterExtension static final QuarkusDevModeTest test = new QuarkusDevModeTest() .withApplicationRoot((jar) -> jar - .addClasses(UserInfoResource.class, UserInfoEndpoint.class) + .addClasses(UserInfoResource.class, UserInfoEndpoint.class, OidcStartup.class) .addAsResource( new StringAsset( """ - quarkus.oidc.tenant-paths=/user-info/default-tenant quarkus.oidc.user-info-path=http://${quarkus.http.host}:${quarkus.http.port}/user-info-endpoint - quarkus.oidc.named.auth-server-url=${quarkus.oidc.auth-server-url} - quarkus.oidc.named.tenant-paths=/user-info/named-tenant - quarkus.oidc.named.user-info-path=http://${quarkus.http.host}:${quarkus.http.port}/user-info-endpoint quarkus.oidc.named-2.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc.named-2.tenant-paths=/user-info/named-tenant-2 quarkus.oidc.named-2.discovery-enabled=false @@ -54,13 +55,13 @@ public class UserInfoRequiredDetectionTest { @Test public void testDefaultTenant() { - RestAssured.given().auth().oauth2(getAccessToken()).get("/user-info/default-tenant").then().statusCode(200) + RestAssured.given().auth().oauth2(getAccessToken()).get("/user-info/default-tenant-random").then().statusCode(200) .body(Matchers.is("alice")); } @Test public void testNamedTenant() { - RestAssured.given().auth().oauth2(getAccessToken()).get("/user-info/named-tenant").then().statusCode(200) + RestAssured.given().auth().oauth2(getAccessToken()).get("/user-info/named-tenant-random").then().statusCode(200) .body(Matchers.is("alice")); } @@ -98,23 +99,36 @@ public static class UserInfoResource { @Inject TenantConfigBean tenantConfigBean; + @Inject + RoutingContext routingContext; + @PermissionsAllowed("openid") - @Path("default-tenant") + @Path("default-tenant-random") @GET public String getDefaultTenantName() { if (!tenantConfigBean.getDefaultTenant().oidcConfig().authentication.userInfoRequired.orElse(false)) { throw new IllegalStateException("Default tenant user info should be required"); } + String tenantId = routingContext.get(OidcUtils.TENANT_ID_ATTRIBUTE); + if (!OidcUtils.DEFAULT_TENANT_ID.equals(tenantId)) { + throw new IllegalStateException( + "Incorrect tenant resolved based on the path - expected default tenant, got " + tenantId); + } return userInfo.getPreferredUserName(); } @PermissionsAllowed("openid") - @Path("named-tenant") + @Path("named-tenant-random") @GET public String getNamedTenantName() { if (!getNamedTenantConfig("named").authentication.userInfoRequired.orElse(false)) { throw new IllegalStateException("Named tenant user info should be required"); } + String tenantId = routingContext.get(OidcUtils.TENANT_ID_ATTRIBUTE); + if (!"named".equals(tenantId)) { + throw new IllegalStateException( + "Incorrect tenant resolved based on the path - expected 'named', got " + tenantId); + } return userInfo.getPreferredUserName(); } @@ -137,4 +151,29 @@ private OidcTenantConfig getNamedTenantConfig(String configName) { } } + public static class OidcStartup { + + void observe(@Observes Oidc oidc, OidcConfig oidcConfig, + @ConfigProperty(name = "quarkus.http.host") String host, + @ConfigProperty(name = "quarkus.http.port") String port, + @ConfigProperty(name = "quarkus.oidc.auth-server-url") String authServerUrl) { + oidc.create(createDefaultTenant(oidcConfig)); + oidc.create(createNamedTenant(authServerUrl, host, port)); + } + + private static OidcTenantConfig createDefaultTenant(OidcConfig oidcConfig) { + // this enhances 'application.properties' configuration with a tenant path + return OidcTenantConfig.builder(OidcConfig.getDefaultTenant(oidcConfig)) + .tenantPaths("/user-info/default-tenant-random") + .build(); + } + + private static OidcTenantConfig createNamedTenant(String authServerUrl, String host, String port) { + return OidcTenantConfig.authServerUrl(authServerUrl) + .tenantId("named") + .tenantPaths("/user-info/named-tenant-random") + .userInfoPath("http://%s:%s/user-info-endpoint".formatted(host, port)) + .build(); + } + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Oidc.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Oidc.java new file mode 100644 index 0000000000000..3be435193225b --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Oidc.java @@ -0,0 +1,60 @@ +package io.quarkus.oidc; + +import io.smallrye.mutiny.Uni; + +/** + * A CDI bean that facilitates programmatic OIDC extension setup. + * Static OIDC tenants must be created in a synchronous manner from an observer method like in the example below: + * + *
+ * {@code
+ * public class OidcSetup {
+ *
+ *     void createStaticTenant(@Observes Oidc oidc) {
+ *         var defaultTenant = OidcTenantConfig.authServerUrl("https://oidc-provider-hostname/").build();
+ *         oidc.create(defaultTenant);
+ *     }
+ * }
+ * }
+ * 
+ * + * The example above is equivalent to configuring {@code quarkus.oidc.auth-server-url=https://oidc-provider-hostname/} + * in the application.properties. + *

+ * If necessary, it is also possible to create dynamic OIDC tenants when the application is running: + * + *

+ * {
+ *     @code
+ *     @ApplicationScoped
+ *     public class OidcSetup {
+ *
+ *         @Inject
+ *         Oidc oidc;
+ *
+ *         public void createDynamicTenant() {
+ *             var myTenant = OidcTenantConfig.authServerUrl("https://oidc-provider-hostname/").tenantId("my-tenant").build();
+ *             oidc.create(myTenant);
+ *         }
+ *     }
+ * }
+ * 
+ */ +public interface Oidc { + + /** + * Creates OIDC tenant. This method can also be used to create static tenants based on the tenants + * configured in the 'application.properties' file or any other configuration source. + * + * @param tenantConfig tenant config; must not be null + */ + void create(OidcTenantConfig tenantConfig); + + /** + * Creates OIDC tenant. + * Use this method if tenant must be created on an IO thread. + * + * @param tenantConfig tenant config; must not be null + */ + Uni createAsync(OidcTenantConfig tenantConfig); +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfigBuilder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfigBuilder.java index 2600affd4c7fa..16d0cfc26601f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfigBuilder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfigBuilder.java @@ -27,6 +27,7 @@ import io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager; import io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager.EncryptionAlgorithm; import io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager.Strategy; +import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.oidc.runtime.builders.AuthenticationConfigBuilder; import io.quarkus.oidc.runtime.builders.LogoutConfigBuilder; import io.quarkus.oidc.runtime.builders.TokenConfigBuilder; @@ -662,6 +663,9 @@ public OidcTenantConfigBuilder provider(Provider provider) { * @return build {@link io.quarkus.oidc.OidcTenantConfig} */ public io.quarkus.oidc.OidcTenantConfig build() { + if (tenantId.isEmpty()) { + tenantId(OidcUtils.DEFAULT_TENANT_ID); + } var mapping = new OidcTenantConfigImpl(this); return io.quarkus.oidc.OidcTenantConfig.of(mapping); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java index 41edeac8c2104..9365a87a13f9b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/BackChannelLogoutHandler.java @@ -4,7 +4,6 @@ import java.util.function.Consumer; import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; import org.eclipse.microprofile.jwt.Claims; import org.jboss.logging.Logger; @@ -26,37 +25,30 @@ public class BackChannelLogoutHandler { private static final Logger LOG = Logger.getLogger(BackChannelLogoutHandler.class); private static final String SLASH = "/"; - @Inject - DefaultTenantConfigResolver resolver; - - private final OidcConfig oidcConfig; - - public BackChannelLogoutHandler(OidcConfig oidcConfig) { - this.oidcConfig = oidcConfig; - } - - void setup(@Observes Router router) { - addRoute(router, OidcConfig.getDefaultTenant(oidcConfig)); - for (var nameToOidcTenantConfig : oidcConfig.namedTenants().entrySet()) { - if (OidcConfig.DEFAULT_TENANT_KEY.equals(nameToOidcTenantConfig.getKey())) { - continue; + void setup(@Observes Router router, DefaultTenantConfigResolver resolver) { + final TenantConfigBean tenantConfigBean = resolver.getTenantConfigBean(); + addRoute(router, tenantConfigBean.getDefaultTenant().oidcConfig(), resolver); + for (var nameToOidcTenantConfig : tenantConfigBean.getStaticTenantsConfig().values()) { + if (nameToOidcTenantConfig.oidcConfig() != null) { + addRoute(router, nameToOidcTenantConfig.oidcConfig(), resolver); } - addRoute(router, nameToOidcTenantConfig.getValue()); } } - private void addRoute(Router router, OidcTenantConfig oidcTenantConfig) { + private static void addRoute(Router router, OidcTenantConfig oidcTenantConfig, DefaultTenantConfigResolver resolver) { if (oidcTenantConfig.tenantEnabled() && oidcTenantConfig.logout().backchannel().path().isPresent()) { router.route(oidcTenantConfig.logout().backchannel().path().get()) - .handler(new RouteHandler(oidcTenantConfig)); + .handler(new RouteHandler(oidcTenantConfig, resolver)); } } - class RouteHandler implements Handler { + static class RouteHandler implements Handler { private final OidcTenantConfig oidcTenantConfig; + private final DefaultTenantConfigResolver resolver; - RouteHandler(OidcTenantConfig oidcTenantConfig) { + RouteHandler(OidcTenantConfig oidcTenantConfig, DefaultTenantConfigResolver resolver) { this.oidcTenantConfig = oidcTenantConfig; + this.resolver = resolver; } @Override @@ -168,16 +160,16 @@ private boolean isMatchingTenant(String requestPath, TenantConfigContext tenant) && tenant.oidcConfig().tenantId().get().equals(oidcTenantConfig.tenantId().get()) && requestPath.equals(getRootPath() + tenant.oidcConfig().logout().backchannel().path().orElse(null)); } - } - private String getRootPath() { - // Prepend '/' if it is not present - String rootPath = OidcCommonUtils.prependSlash(resolver.getRootPath()); - // Strip trailing '/' if the length is > 1 - if (rootPath.length() > 1 && rootPath.endsWith("/")) { - rootPath = rootPath.substring(rootPath.length() - 1); + private String getRootPath() { + // Prepend '/' if it is not present + String rootPath = OidcCommonUtils.prependSlash(resolver.getRootPath()); + // Strip trailing '/' if the length is > 1 + if (rootPath.length() > 1 && rootPath.endsWith("/")) { + rootPath = rootPath.substring(rootPath.length() - 1); + } + // if it is only '/' then return an empty value + return SLASH.equals(rootPath) ? "" : rootPath; } - // if it is only '/' then return an empty value - return SLASH.equals(rootPath) ? "" : rootPath; } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java index 781e0b6c2822b..5193acef59c56 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTenantConfigResolver.java @@ -111,7 +111,7 @@ public Uni apply(OidcTenantConfig oidcTenantConfig) { } final String tenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE); - if (tenantId != null && !isTenantSetByAnnotation(context, tenantId)) { + if (tenantId != null) { TenantConfigContext tenantContext = tenantConfigBean.getDynamicTenant(tenantId); if (tenantContext != null) { return Uni.createFrom().item(tenantContext.getOidcTenantConfig()); @@ -265,7 +265,7 @@ public Uni apply(OidcTenantConfig tenantConfig) { .orElseThrow(() -> new OIDCException("Tenant configuration must have tenant id")); var tenantContext = tenantConfigBean.getDynamicTenant(tenantId); if (tenantContext == null) { - return tenantConfigBean.createDynamicTenantContext(tenantConfig); + return tenantConfigBean.createDynamicTenantContextInternal(tenantConfig); } else { return Uni.createFrom().item(tenantContext); } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcImpl.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcImpl.java new file mode 100644 index 0000000000000..772452e35abc4 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcImpl.java @@ -0,0 +1,90 @@ +package io.quarkus.oidc.runtime; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import jakarta.inject.Singleton; + +import io.quarkus.oidc.Oidc; +import io.quarkus.oidc.OidcTenantConfig; +import io.smallrye.mutiny.Uni; + +@Singleton +public final class OidcImpl implements Oidc { + + record StaticOidcTenantsResult(Map staticTenantConfigs, OidcTenantConfig defaultTenantConfig) { + } + + private Map staticTenantConfigs; + private OidcTenantConfig defaultTenantConfig; + private volatile boolean oidcStarted; + private volatile TenantConfigBean tenantConfigBean; + + OidcImpl(OidcConfig config) { + this.oidcStarted = false; + this.defaultTenantConfig = OidcTenantConfig.of(OidcConfig.getDefaultTenant(config)); + this.staticTenantConfigs = getStaticTenants(config); + } + + StaticOidcTenantsResult start(TenantConfigBean tenantConfigBean) { + this.tenantConfigBean = tenantConfigBean; + // this is not thread-safe, but we declare that static OIDC tenants may + // only be created from the OIDC observer method in a synchronous manner + // hence we don't need it to be thread-safe + this.oidcStarted = true; + var result = new StaticOidcTenantsResult(Collections.unmodifiableMap(staticTenantConfigs), defaultTenantConfig); + // for now, we don't use this bean as single source of truth once the application is up and running + this.staticTenantConfigs = null; + this.defaultTenantConfig = null; + return result; + } + + @Override + public void create(OidcTenantConfig tenantConfig) { + Objects.requireNonNull(tenantConfig); + if (oidcStarted) { + createDynamicTenant(tenantConfig).await().indefinitely(); + } else { + putStaticTenantConfig(tenantConfig); + } + } + + @Override + public Uni createAsync(OidcTenantConfig tenantConfig) { + Objects.requireNonNull(tenantConfig); + if (oidcStarted) { + return createDynamicTenant(tenantConfig); + } else { + putStaticTenantConfig(tenantConfig); + return Uni.createFrom().voidItem(); + } + } + + private Uni createDynamicTenant(OidcTenantConfig tenantConfig) { + return tenantConfigBean.createDynamicTenantContextInternal(tenantConfig).replaceWithVoid(); + } + + private void putStaticTenantConfig(OidcTenantConfig tenantConfig) { + final String tenantId = tenantConfig.tenantId().get(); + if (defaultTenantConfig.tenantId().get().equals(tenantId)) { + defaultTenantConfig = tenantConfig; + } else { + staticTenantConfigs.put(tenantId, tenantConfig); + } + } + + private static Map getStaticTenants(OidcConfig config) { + Map tenantConfigs = new HashMap<>(); + for (var tenant : config.namedTenants().entrySet()) { + String tenantKey = tenant.getKey(); + if (OidcConfig.DEFAULT_TENANT_KEY.equals(tenantKey)) { + continue; + } + var namedTenantConfig = OidcTenantConfig.of(tenant.getValue()); + tenantConfigs.put(tenantKey, namedTenantConfig); + } + return tenantConfigs; + } +} diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index a2803969505bf..bc677ca3bd6f1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -1,79 +1,43 @@ package io.quarkus.oidc.runtime; -import static io.quarkus.oidc.SecurityEvent.AUTH_SERVER_URL; -import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_AVAILABLE; -import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_NOT_AVAILABLE; import static io.quarkus.oidc.runtime.OidcConfig.getDefaultTenant; import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID; import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRoutingContextAttribute; -import java.security.Key; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import jakarta.enterprise.event.Event; import jakarta.enterprise.inject.CreationException; -import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; -import org.jose4j.jwk.JsonWebKey; -import org.jose4j.jwk.PublicJsonWebKey; import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.OIDCException; -import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.Oidc; import io.quarkus.oidc.OidcTenantConfig; -import io.quarkus.oidc.SecurityEvent; -import io.quarkus.oidc.TenantConfigResolver; import io.quarkus.oidc.TenantIdentityProvider; -import io.quarkus.oidc.common.OidcEndpoint; -import io.quarkus.oidc.common.OidcRequestContextProperties; -import io.quarkus.oidc.common.OidcRequestFilter; -import io.quarkus.oidc.common.OidcResponseFilter; -import io.quarkus.oidc.common.runtime.OidcCommonUtils; -import io.quarkus.oidc.common.runtime.OidcTlsSupport; -import io.quarkus.oidc.common.runtime.config.OidcCommonConfig; -import io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType; -import io.quarkus.oidc.runtime.OidcTenantConfig.Roles.Source; -import io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager.Strategy; -import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.runtime.annotations.RuntimeInit; +import io.quarkus.runtime.annotations.StaticInit; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.TokenAuthenticationRequest; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; -import io.quarkus.security.spi.runtime.SecurityEventHelper; -import io.quarkus.tls.TlsConfigurationRegistry; -import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; -import io.smallrye.jwt.util.KeyUtils; -import io.smallrye.mutiny.TimeoutException; import io.smallrye.mutiny.Uni; import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; -import io.vertx.core.net.ProxyOptions; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.client.WebClientOptions; -import io.vertx.mutiny.ext.web.client.WebClient; @Recorder public class OidcRecorder { - private static final Logger LOG = Logger.getLogger(OidcRecorder.class); - private static final String SECURITY_EVENTS_ENABLED_CONFIG_KEY = "quarkus.security.events.enabled"; - - private static final Set tenantsExpectingServerAvailableEvents = ConcurrentHashMap.newKeySet(); - private static volatile boolean userInfoInjectionPointDetected = false; + static final Logger LOG = Logger.getLogger(OidcRecorder.class); public Supplier setupTokenCache(OidcConfig config, Supplier vertx) { return new Supplier() { @@ -84,21 +48,24 @@ public DefaultTokenIntrospectionUserInfoCache get() { }; } - public Supplier createTenantConfigBean(OidcConfig config, Supplier vertx, - Supplier registrySupplier, - boolean userInfoInjectionPointDetected) { - return new Supplier() { - @Override - public TenantConfigBean get() { - return setup(config, vertx.get(), OidcTlsSupport.of(registrySupplier), userInfoInjectionPointDetected); - } - }; + @StaticInit + public void setUserInfoInjectionPointDetected(boolean userInfoInjectionPointDetected) { + TenantContextFactory.userInfoInjectionPointDetected = userInfoInjectionPointDetected; + } + + @RuntimeInit + public void initOidc() { + final ArcContainer container = Arc.container(); + final Oidc oidc = initSingletonCdiBean(container, Oidc.class); + final Event oidcPreStartEvent = container.beanManager().getEvent().select(Oidc.class); + oidcPreStartEvent.fire(oidc); + // this triggers OIDC start-up and static tenants validation + initSingletonCdiBean(container, TenantConfigBean.class); } - public void initTenantConfigBean() { + private static T initSingletonCdiBean(ArcContainer container, Class beanClass) { try { - // makes sure that config of static tenants is validated during app startup and create static tenant contexts - Arc.container().instance(TenantConfigBean.class).get(); + return container.instance(beanClass).get(); } catch (CreationException wrapper) { if (wrapper.getCause() instanceof RuntimeException runtimeException) { // so that users see ConfigurationException etc. without noise @@ -108,545 +75,6 @@ public void initTenantConfigBean() { } } - public TenantConfigBean setup(OidcConfig config, Vertx vertxValue, OidcTlsSupport tlsSupport, - boolean userInfoInjectionPointDetected) { - OidcRecorder.userInfoInjectionPointDetected = userInfoInjectionPointDetected; - - var defaultTenant = OidcTenantConfig.of(getDefaultTenant(config)); - String defaultTenantId = defaultTenant.tenantId().get(); - var defaultTenantInitializer = createStaticTenantContextCreator(vertxValue, defaultTenant, - !config.namedTenants().isEmpty(), defaultTenantId, tlsSupport); - TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, defaultTenant, - !config.namedTenants().isEmpty(), defaultTenantId, tlsSupport, defaultTenantInitializer); - - Map staticTenantsConfig = new HashMap<>(); - for (var tenant : config.namedTenants().entrySet()) { - if (OidcConfig.DEFAULT_TENANT_KEY.equals(tenant.getKey())) { - continue; - } - var namedTenantConfig = OidcTenantConfig.of(tenant.getValue()); - OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), namedTenantConfig.tenantId()); - var staticTenantInitializer = createStaticTenantContextCreator(vertxValue, namedTenantConfig, false, - tenant.getKey(), tlsSupport); - staticTenantsConfig.put(tenant.getKey(), - createStaticTenantContext(vertxValue, namedTenantConfig, false, tenant.getKey(), tlsSupport, - staticTenantInitializer)); - } - - return new TenantConfigBean(staticTenantsConfig, defaultTenantContext, - new TenantConfigBean.TenantContextFactory() { - @Override - public Uni create(OidcTenantConfig config) { - return createDynamicTenantContext(vertxValue, config, tlsSupport); - } - }); - } - - private Uni createDynamicTenantContext(Vertx vertx, - OidcTenantConfig oidcConfig, OidcTlsSupport tlsSupport) { - - var tenantId = oidcConfig.tenantId().orElseThrow(); - if (oidcConfig.logout().backchannel().path().isPresent()) { - throw new ConfigurationException( - "BackChannel Logout is currently not supported for dynamic tenants"); - } - return createTenantContext(vertx, oidcConfig, false, tenantId, tlsSupport) - .onFailure().transform(new Function() { - @Override - public Throwable apply(Throwable t) { - return logTenantConfigContextFailure(t, tenantId); - } - }); - } - - private TenantConfigContext createStaticTenantContext(Vertx vertx, - OidcTenantConfig oidcConfig, boolean checkNamedTenants, String tenantId, - OidcTlsSupport tlsSupport, Supplier> staticTenantCreator) { - - Uni uniContext = createTenantContext(vertx, oidcConfig, checkNamedTenants, tenantId, tlsSupport); - try { - return uniContext.onFailure() - .recoverWithItem(new Function() { - @Override - public TenantConfigContext apply(Throwable t) { - if (t instanceof OIDCException) { - LOG.warnf("Tenant '%s': '%s'." - + " OIDC server is not available yet, an attempt to connect will be made during the first request." - + " Access to resources protected by this tenant may fail" - + " if OIDC server will not become available", - tenantId, t.getMessage()); - return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); - } - logTenantConfigContextFailure(t, tenantId); - if (t instanceof ConfigurationException - && !oidcConfig.authServerUrl().isPresent() - && LaunchMode.DEVELOPMENT == LaunchMode.current()) { - // Let it start if it is a DEV mode and auth-server-url has not been configured yet - return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); - } - // fail in all other cases - throw new OIDCException(t); - } - }) - .await().atMost(oidcConfig.connectionTimeout()); - } catch (TimeoutException t2) { - LOG.warnf("Tenant '%s': OIDC server is not available after a %d seconds timeout, an attempt to connect will be made" - + " during the first request. Access to resources protected by this tenant may fail if OIDC server" - + " will not become available", tenantId, oidcConfig.connectionTimeout().getSeconds()); - return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); - } - } - - private Supplier> createStaticTenantContextCreator(Vertx vertx, OidcTenantConfig oidcConfig, - boolean checkNamedTenants, String tenantId, OidcTlsSupport tlsSupport) { - return new Supplier>() { - @Override - public Uni get() { - return createTenantContext(vertx, oidcConfig, checkNamedTenants, tenantId, tlsSupport) - .onFailure().transform(new Function() { - @Override - public Throwable apply(Throwable t) { - return logTenantConfigContextFailure(t, tenantId); - } - }); - } - }; - } - - private static Throwable logTenantConfigContextFailure(Throwable t, String tenantId) { - LOG.debugf( - "'%s' tenant is not initialized: '%s'. Access to resources protected by this tenant will fail.", - tenantId, t.getMessage()); - return t; - } - - @SuppressWarnings("resource") - private Uni createTenantContext(Vertx vertx, OidcTenantConfig oidcTenantConfig, - boolean checkNamedTenants, String tenantId, OidcTlsSupport tlsSupport) { - final OidcTenantConfig oidcConfig = OidcUtils.resolveProviderConfig(oidcTenantConfig); - - if (!oidcConfig.tenantEnabled()) { - LOG.debugf("'%s' tenant configuration is disabled", tenantId); - return Uni.createFrom().item(TenantConfigContext.createReady(new OidcProvider(null, null, null, null), oidcConfig)); - } - - if (oidcConfig.authServerUrl().isEmpty()) { - if (oidcConfig.publicKey().isPresent() && oidcConfig.certificateChain().trustStoreFile().isPresent()) { - throw new ConfigurationException("Both public key and certificate chain verification modes are enabled"); - } - if (oidcConfig.publicKey().isPresent()) { - return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig)); - } - - if (oidcConfig.certificateChain().trustStoreFile().isPresent()) { - return Uni.createFrom().item(createTenantContextToVerifyCertChain(oidcConfig)); - } - } - - try { - if (oidcConfig.authServerUrl().isEmpty()) { - if (DEFAULT_TENANT_ID.equals(oidcConfig.tenantId().get())) { - ArcContainer container = Arc.container(); - if (container != null - && (container.instance(TenantConfigResolver.class).isAvailable() || checkNamedTenants)) { - LOG.debugf("Default tenant is not configured and will be disabled" - + " because either 'TenantConfigResolver' which will resolve tenant configurations is registered" - + " or named tenants are configured."); - oidcConfig.tenantEnabled = false; - return Uni.createFrom() - .item(TenantConfigContext.createReady(new OidcProvider(null, null, null, null), oidcConfig)); - } - } - throw new ConfigurationException( - "'" + getConfigPropertyForTenant(tenantId, "auth-server-url") + "' property must be configured"); - } - OidcCommonUtils.verifyEndpointUrl(oidcConfig.authServerUrl().get()); - OidcCommonUtils.verifyCommonConfiguration(oidcConfig, OidcUtils.isServiceApp(oidcConfig), true); - } catch (ConfigurationException t) { - return Uni.createFrom().failure(t); - } - - if (oidcConfig.roles().source().orElse(null) == Source.userinfo && !enableUserInfo(oidcConfig)) { - throw new ConfigurationException( - "UserInfo is not required but UserInfo is expected to be the source of authorization roles"); - } - if (oidcConfig.token().verifyAccessTokenWithUserInfo().orElse(false) && !OidcUtils.isWebApp(oidcConfig) - && !enableUserInfo(oidcConfig)) { - throw new ConfigurationException( - "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"); - } - if (!oidcConfig.authentication().idTokenRequired().orElse(true) && !enableUserInfo(oidcConfig)) { - throw new ConfigurationException( - "UserInfo is not required but it will be needed to verify a code flow access token"); - } - - if (!oidcConfig.discoveryEnabled().orElse(true)) { - if (!OidcUtils.isServiceApp(oidcConfig)) { - if (oidcConfig.authorizationPath().isEmpty() || oidcConfig.tokenPath().isEmpty()) { - String authorizationPathProperty = getConfigPropertyForTenant(tenantId, "authorization-path"); - String tokenPathProperty = getConfigPropertyForTenant(tenantId, "token-path"); - throw new ConfigurationException( - "'web-app' applications must have '" + authorizationPathProperty + "' and '" + tokenPathProperty - + "' properties " - + "set when the discovery is disabled.", - Set.of(authorizationPathProperty, tokenPathProperty)); - } - } - // JWK and introspection endpoints have to be set for both 'web-app' and 'service' applications - if (oidcConfig.jwksPath().isEmpty() && oidcConfig.introspectionPath().isEmpty()) { - if (!oidcConfig.authentication().idTokenRequired().orElse(true) - && oidcConfig.authentication().userInfoRequired().orElse(false)) { - LOG.debugf("tenant %s supports only UserInfo", oidcConfig.tenantId().get()); - } else { - throw new ConfigurationException( - "Either 'jwks-path' or 'introspection-path' properties must be set when the discovery is disabled.", - Set.of("quarkus.oidc.jwks-path", "quarkus.oidc.introspection-path")); - } - } - if (oidcConfig.authentication().userInfoRequired().orElse(false) && oidcConfig.userInfoPath().isEmpty()) { - String configProperty = getConfigPropertyForTenant(tenantId, "user-info-path"); - throw new ConfigurationException( - "UserInfo is required but '" + configProperty + "' is not configured.", - Set.of(configProperty)); - } - } - - if (OidcUtils.isServiceApp(oidcConfig)) { - if (oidcConfig.token().refreshExpired()) { - throw new ConfigurationException( - "The '" + getConfigPropertyForTenant(tenantId, "token.refresh-expired") - + "' property can only be enabled for " + ApplicationType.WEB_APP - + " application types"); - } - if (oidcConfig.token().refreshTokenTimeSkew().isPresent()) { - throw new ConfigurationException( - "The '" + getConfigPropertyForTenant(tenantId, "token.refresh-token-time-skew") - + "' property can only be enabled for " + ApplicationType.WEB_APP - + " application types"); - } - if (oidcConfig.logout().path().isPresent()) { - throw new ConfigurationException( - "The '" + getConfigPropertyForTenant(tenantId, "logout.path") + "' property can only be enabled for " - + ApplicationType.WEB_APP + " application types"); - } - if (oidcConfig.roles().source().isPresent() && oidcConfig.roles().source().get() == Source.idtoken) { - throw new ConfigurationException( - "The '" + getConfigPropertyForTenant(tenantId, "roles.source") - + "' property can only be set to 'idtoken' for " + ApplicationType.WEB_APP - + " application types"); - } - } else { - if (oidcConfig.token().refreshTokenTimeSkew().isPresent()) { - oidcConfig.token.setRefreshExpired(true); - } - } - - if (oidcConfig.tokenStateManager().strategy() != Strategy.KEEP_ALL_TOKENS) { - - if (oidcConfig.authentication().userInfoRequired().orElse(false) - || oidcConfig.roles().source().orElse(null) == Source.userinfo) { - throw new ConfigurationException( - "UserInfo is required but DefaultTokenStateManager is configured to not keep the access token"); - } - if (oidcConfig.roles().source().orElse(null) == Source.accesstoken) { - throw new ConfigurationException( - "Access token is required to check the roles but DefaultTokenStateManager is configured to not keep the access token"); - } - } - - if (oidcConfig.token().verifyAccessTokenWithUserInfo().orElse(false)) { - if (!oidcConfig.discoveryEnabled().orElse(true)) { - if (oidcConfig.userInfoPath().isEmpty()) { - throw new ConfigurationException( - "UserInfo path is missing but 'verifyAccessTokenWithUserInfo' is enabled"); - } - if (oidcConfig.introspectionPath().isPresent()) { - throw new ConfigurationException( - "Introspection path is configured and 'verifyAccessTokenWithUserInfo' is enabled, these options are mutually exclusive"); - } - } - } - - if (!oidcConfig.token().issuedAtRequired() && oidcConfig.token().age().isPresent()) { - String tokenIssuedAtRequired = getConfigPropertyForTenant(tenantId, "token.issued-at-required"); - String tokenAge = getConfigPropertyForTenant(tenantId, "token.age"); - throw new ConfigurationException( - "The '" + tokenIssuedAtRequired + "' can only be set to false if '" + tokenAge + "' is not set." + - " Either set '" + tokenIssuedAtRequired + "' to true or do not set '" + tokenAge + "'.", - Set.of(tokenIssuedAtRequired, tokenAge)); - } - - return createOidcProvider(oidcConfig, vertx, tlsSupport) - .onItem().transform(new Function() { - @Override - public TenantConfigContext apply(OidcProvider p) { - return TenantConfigContext.createReady(p, oidcConfig); - } - }); - } - - private static String getConfigPropertyForTenant(String tenantId, String configSubKey) { - if (DEFAULT_TENANT_ID.equals(tenantId)) { - return "quarkus.oidc." + configSubKey; - } else { - return "quarkus.oidc." + tenantId + "." + configSubKey; - } - } - - private static boolean enableUserInfo(OidcTenantConfig oidcConfig) { - Optional userInfoRequired = oidcConfig.authentication().userInfoRequired(); - if (userInfoRequired.isPresent()) { - if (!userInfoRequired.get()) { - return false; - } - } else { - oidcConfig.authentication.setUserInfoRequired(true); - } - return true; - } - - private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantConfig oidcConfig) { - if (!OidcUtils.isServiceApp(oidcConfig)) { - throw new ConfigurationException("'public-key' property can only be used with the 'service' applications"); - } - LOG.debug("'public-key' property for the local token verification is set," - + " no connection to the OIDC server will be created"); - - return TenantConfigContext.createReady( - new OidcProvider(oidcConfig.publicKey().get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); - } - - private static TenantConfigContext createTenantContextToVerifyCertChain(OidcTenantConfig oidcConfig) { - if (!OidcUtils.isServiceApp(oidcConfig)) { - throw new ConfigurationException( - "Currently only 'service' applications can be used to verify tokens with inlined certificate chains"); - } - - return TenantConfigContext.createReady( - new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); - } - - public static Optional toProxyOptions(OidcCommonConfig.Proxy proxyConfig) { - return OidcCommonUtils.toProxyOptions(proxyConfig); - } - - protected static OIDCException toOidcException(Throwable cause, String authServerUrl, String tenantId) { - final String message = OidcCommonUtils.formatConnectionErrorMessage(authServerUrl); - LOG.warn(message); - fireOidcServerNotAvailableEvent(authServerUrl, tenantId); - return new OIDCException("OIDC Server is not available", cause); - } - - protected static Uni createOidcProvider(OidcTenantConfig oidcConfig, Vertx vertx, - OidcTlsSupport tlsSupport) { - return createOidcClientUni(oidcConfig, vertx, tlsSupport) - .flatMap(new Function>() { - @Override - public Uni apply(OidcProviderClient client) { - if (oidcConfig.jwks().resolveEarly() - && client.getMetadata().getJsonWebKeySetUri() != null - && !oidcConfig.token().requireJwtIntrospectionOnly()) { - return getJsonWebSetUni(client, oidcConfig).onItem() - .transform(new Function() { - @Override - public OidcProvider apply(JsonWebKeySet jwks) { - return new OidcProvider(client, oidcConfig, jwks, - readTokenDecryptionKey(oidcConfig)); - } - }); - } else { - return Uni.createFrom() - .item(new OidcProvider(client, oidcConfig, null, readTokenDecryptionKey(oidcConfig))); - } - } - }); - } - - private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) { - if (oidcConfig.token().decryptionKeyLocation().isPresent()) { - try { - Key key = null; - - String keyContent = KeyUtils.readKeyContent(oidcConfig.token().decryptionKeyLocation().get()); - if (keyContent != null) { - List keys = KeyUtils.loadJsonWebKeys(keyContent); - if (keys != null && keys.size() == 1 && - (keys.get(0).getAlgorithm() == null - || keys.get(0).getAlgorithm().equals(KeyEncryptionAlgorithm.RSA_OAEP.getAlgorithm())) - && ("enc".equals(keys.get(0).getUse()) || keys.get(0).getUse() == null)) { - key = PublicJsonWebKey.class.cast(keys.get(0)).getPrivateKey(); - } - } - if (key == null) { - key = KeyUtils.decodeDecryptionPrivateKey(keyContent); - } - return key; - } catch (Exception ex) { - throw new ConfigurationException( - String.format("Token decryption key for tenant %s can not be read from %s", - oidcConfig.tenantId().get(), oidcConfig.token().decryptionKeyLocation().get()), - ex); - } - } else { - return null; - } - } - - protected static Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) { - if (!oidcConfig.discoveryEnabled().orElse(true)) { - String tenantId = oidcConfig.tenantId().orElse(DEFAULT_TENANT_ID); - if (shouldFireOidcServerAvailableEvent(tenantId)) { - return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig) - .invoke(new Runnable() { - @Override - public void run() { - fireOidcServerAvailableEvent(oidcConfig.authServerUrl().get(), tenantId); - } - }); - } - return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig); - } else { - return client.getJsonWebKeySet(null); - } - } - - private static Uni getJsonWebSetUniWhenDiscoveryDisabled(OidcProviderClient client, - OidcTenantConfig oidcConfig) { - final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); - return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) - .retry() - .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION) - .expireIn(connectionDelayInMillisecs) - .onFailure() - .transform(new Function() { - @Override - public Throwable apply(Throwable t) { - return toOidcException(t, oidcConfig.authServerUrl().get(), - oidcConfig.tenantId().orElse(DEFAULT_TENANT_ID)); - } - }) - .onFailure() - .invoke(client::close); - } - - protected static Uni createOidcClientUni(OidcTenantConfig oidcConfig, Vertx vertx, - OidcTlsSupport tlsSupport) { - - String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig); - - WebClientOptions options = new WebClientOptions(); - options.setFollowRedirects(oidcConfig.followRedirects()); - OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsSupport.forConfig(oidcConfig.tls())); - var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx); - WebClient client = WebClient.create(mutinyVertx, options); - - Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); - Map> oidcResponseFilters = OidcCommonUtils.getOidcResponseFilters(); - - Uni metadataUni = null; - if (!oidcConfig.discoveryEnabled().orElse(true)) { - metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString)); - } else { - final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); - OidcRequestContextProperties contextProps = new OidcRequestContextProperties( - Map.of(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.tenantId().orElse(OidcUtils.DEFAULT_TENANT_ID))); - metadataUni = OidcCommonUtils - .discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, authServerUriString, - connectionDelayInMillisecs, - mutinyVertx, - oidcConfig.useBlockingDnsLookup()) - .onItem() - .transform(new Function() { - @Override - public OidcConfigurationMetadata apply(JsonObject json) { - return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString), - OidcCommonUtils.getDiscoveryUri(authServerUriString)); - } - }); - } - return metadataUni.onItemOrFailure() - .transformToUni(new BiFunction>() { - - @Override - public Uni apply(OidcConfigurationMetadata metadata, Throwable t) { - String tenantId = oidcConfig.tenantId().orElse(DEFAULT_TENANT_ID); - if (t != null) { - client.close(); - return Uni.createFrom().failure(toOidcException(t, authServerUriString, tenantId)); - } - if (shouldFireOidcServerAvailableEvent(tenantId)) { - fireOidcServerAvailableEvent(authServerUriString, tenantId); - } - if (metadata == null) { - client.close(); - return Uni.createFrom().failure(new ConfigurationException( - "OpenId Connect Provider configuration metadata is not configured and can not be discovered")); - } - if (oidcConfig.logout().path().isPresent()) { - if (oidcConfig.endSessionPath().isEmpty() && metadata.getEndSessionUri() == null) { - client.close(); - return Uni.createFrom().failure(new ConfigurationException( - "The application supports RP-Initiated Logout but the OpenID Provider does not advertise the end_session_endpoint")); - } - } - if (userInfoInjectionPointDetected && metadata.getUserInfoUri() != null) { - enableUserInfo(oidcConfig); - } - if (oidcConfig.authentication().userInfoRequired().orElse(false) && metadata.getUserInfoUri() == null) { - client.close(); - return Uni.createFrom().failure(new ConfigurationException( - "UserInfo is required but the OpenID Provider UserInfo endpoint is not configured." - + " Use 'quarkus.oidc.user-info-path' if the discovery is disabled.")); - } - return Uni.createFrom() - .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters, - oidcResponseFilters)); - } - - }); - } - - private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oidcConfig, String authServerUriString) { - String tokenUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath()); - String introspectionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, - oidcConfig.introspectionPath()); - String authorizationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, - oidcConfig.authorizationPath()); - String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath()); - String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath()); - String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath()); - String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath()); - return new OidcConfigurationMetadata(tokenUri, - introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri, - oidcConfig.token().issuer().orElse(null)); - } - - private static void fireOidcServerNotAvailableEvent(String authServerUrl, String tenantId) { - if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_NOT_AVAILABLE)) { - tenantsExpectingServerAvailableEvents.add(tenantId); - } - } - - private static void fireOidcServerAvailableEvent(String authServerUrl, String tenantId) { - if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_AVAILABLE)) { - tenantsExpectingServerAvailableEvents.remove(tenantId); - } - } - - private static boolean shouldFireOidcServerAvailableEvent(String tenantId) { - return tenantsExpectingServerAvailableEvents.contains(tenantId); - } - - private static boolean fireOidcServerEvent(String authServerUrl, SecurityEvent.Type eventType) { - if (ConfigProvider.getConfig().getOptionalValue(SECURITY_EVENTS_ENABLED_CONFIG_KEY, boolean.class).orElse(true)) { - SecurityEventHelper.fire( - Arc.container().beanManager().getEvent().select(SecurityEvent.class), - new SecurityEvent(eventType, Map.of(AUTH_SERVER_URL, authServerUrl))); - return true; - } - return false; - } - public Function> tenantResolverInterceptorCreator() { return new Function>() { @Override diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java index 14a39cadf304a..699834a6fff61 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigBean.java @@ -4,43 +4,56 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.annotation.PreDestroy; +import jakarta.inject.Singleton; -import io.quarkus.arc.BeanDestroyer; +import io.quarkus.oidc.OIDCException; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.tls.TlsConfigurationRegistry; import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; -public class TenantConfigBean { +@Singleton +public final class TenantConfigBean { private final Map staticTenantsConfig; private final Map dynamicTenantsConfig; private final TenantConfigContext defaultTenant; private final TenantContextFactory tenantContextFactory; + private final String defaultTenantId; - @FunctionalInterface - public interface TenantContextFactory { - Uni create(OidcTenantConfig oidcTenantConfig); - } - - public TenantConfigBean( - Map staticTenantsConfig, - TenantConfigContext defaultTenant, - TenantContextFactory tenantContextFactory) { - this.staticTenantsConfig = Map.copyOf(staticTenantsConfig); + TenantConfigBean(Vertx vertx, TlsConfigurationRegistry tlsConfigurationRegistry, OidcImpl oidc) { + this.tenantContextFactory = new TenantContextFactory(vertx, tlsConfigurationRegistry); this.dynamicTenantsConfig = new ConcurrentHashMap<>(); - this.defaultTenant = defaultTenant; - this.tenantContextFactory = tenantContextFactory; + + var staticOidcTenants = oidc.start(this); + this.defaultTenantId = staticOidcTenants.defaultTenantConfig().tenantId().get(); + this.staticTenantsConfig = tenantContextFactory.createStaticTenantConfigs(staticOidcTenants); + this.defaultTenant = tenantContextFactory.createDefaultTenantConfig(staticOidcTenants); } + /** + * @deprecated create tenants with the {@link io.quarkus.oidc.Oidc} bean + */ + @Deprecated(since = "3.18", forRemoval = true) public Uni createDynamicTenantContext(OidcTenantConfig oidcConfig) { + return createDynamicTenantContextInternal(oidcConfig); + } + + // TODO: drop 'Internal' postfix when the deprecated method is removed + Uni createDynamicTenantContextInternal(OidcTenantConfig oidcConfig) { var tenantId = oidcConfig.tenantId().orElseThrow(); + if (defaultTenantId.equals(tenantId)) { + return Uni.createFrom().failure(new OIDCException("Dynamic tenants must not use the default tenant ID")); + } + var tenant = dynamicTenantsConfig.get(tenantId); if (tenant != null) { return Uni.createFrom().item(tenant); } - return tenantContextFactory.create(oidcConfig).onItem().transform( + return tenantContextFactory.createDynamic(oidcConfig).onItem().transform( new Function() { @Override public TenantConfigContext apply(TenantConfigContext t) { @@ -66,23 +79,19 @@ public TenantConfigContext getDynamicTenant(String tenantId) { return dynamicTenantsConfig.get(tenantId); } - public static class Destroyer implements BeanDestroyer { - - @Override - public void destroy(TenantConfigBean instance, CreationalContext creationalContext, - Map params) { - if (instance.defaultTenant != null && instance.defaultTenant.provider() != null) { - instance.defaultTenant.provider().close(); - } - for (var i : instance.staticTenantsConfig.values()) { - if (i.provider() != null) { - i.provider().close(); - } + @PreDestroy + public void destroy() { + if (defaultTenant != null && defaultTenant.provider() != null) { + defaultTenant.provider().close(); + } + for (var i : staticTenantsConfig.values()) { + if (i.provider() != null) { + i.provider().close(); } - for (var i : instance.dynamicTenantsConfig.values()) { - if (i.provider() != null) { - i.provider().close(); - } + } + for (var i : dynamicTenantsConfig.values()) { + if (i.provider() != null) { + i.provider().close(); } } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java new file mode 100644 index 0000000000000..1846b21950538 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java @@ -0,0 +1,594 @@ +package io.quarkus.oidc.runtime; + +import static io.quarkus.oidc.SecurityEvent.AUTH_SERVER_URL; +import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_AVAILABLE; +import static io.quarkus.oidc.SecurityEvent.Type.OIDC_SERVER_NOT_AVAILABLE; +import static io.quarkus.oidc.runtime.OidcRecorder.LOG; +import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID; + +import java.security.Key; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.PublicJsonWebKey; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.oidc.OIDCException; +import io.quarkus.oidc.OidcConfigurationMetadata; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.SecurityEvent; +import io.quarkus.oidc.TenantConfigResolver; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.OidcResponseFilter; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; +import io.quarkus.oidc.common.runtime.OidcTlsSupport; +import io.quarkus.runtime.LaunchMode; +import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.security.spi.runtime.SecurityEventHelper; +import io.quarkus.tls.TlsConfigurationRegistry; +import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; +import io.smallrye.jwt.util.KeyUtils; +import io.smallrye.mutiny.TimeoutException; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.ext.web.client.WebClient; + +final class TenantContextFactory { + + private static final String SECURITY_EVENTS_ENABLED_CONFIG_KEY = "quarkus.security.events.enabled"; + + private static final Set tenantsExpectingServerAvailableEvents = ConcurrentHashMap.newKeySet(); + static volatile boolean userInfoInjectionPointDetected = false; + + private final Vertx vertx; + private final OidcTlsSupport tlsSupport; + + TenantContextFactory(Vertx vertx, TlsConfigurationRegistry tlsConfigurationRegistry) { + this.vertx = vertx; + this.tlsSupport = OidcTlsSupport.of(tlsConfigurationRegistry); + } + + TenantConfigContext createDefaultTenantConfig(OidcImpl.StaticOidcTenantsResult staticOidcTenants) { + var defaultTenant = staticOidcTenants.defaultTenantConfig(); + String defaultTenantId = defaultTenant.tenantId().get(); + boolean foundNamedStaticTenants = !staticOidcTenants.staticTenantConfigs().isEmpty(); + var defaultTenantInitializer = createStaticTenantContextCreator(defaultTenant, foundNamedStaticTenants, + defaultTenantId); + return createStaticTenantContext(defaultTenant, foundNamedStaticTenants, defaultTenantId, defaultTenantInitializer); + } + + Map createStaticTenantConfigs(OidcImpl.StaticOidcTenantsResult staticOidcTenants) { + final String defaultTenantId = staticOidcTenants.defaultTenantConfig().tenantId().get(); + Map staticTenantsConfig = new HashMap<>(); + for (var tenant : staticOidcTenants.staticTenantConfigs().entrySet()) { + createStaticTenantConfig(defaultTenantId, tenant.getKey(), tenant.getValue(), staticTenantsConfig); + } + return Map.copyOf(staticTenantsConfig); + } + + Uni createDynamic(OidcTenantConfig oidcConfig) { + var tenantId = oidcConfig.tenantId().orElseThrow(); + if (oidcConfig.logout().backchannel().path().isPresent()) { + throw new ConfigurationException( + "BackChannel Logout is currently not supported for dynamic tenants"); + } + return createTenantContext(oidcConfig, false, tenantId) + .onFailure().transform(new Function() { + @Override + public Throwable apply(Throwable t) { + return logTenantConfigContextFailure(t, tenantId); + } + }); + } + + private void createStaticTenantConfig(String defaultTenantId, String tenantKey, OidcTenantConfig namedTenantConfig, + Map staticTenantsConfig) { + OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenantKey, namedTenantConfig.tenantId()); + var staticTenantInitializer = createStaticTenantContextCreator(namedTenantConfig, false, tenantKey); + staticTenantsConfig.put(tenantKey, + createStaticTenantContext(namedTenantConfig, false, tenantKey, staticTenantInitializer)); + } + + private TenantConfigContext createStaticTenantContext( + OidcTenantConfig oidcConfig, boolean checkNamedTenants, String tenantId, + Supplier> staticTenantCreator) { + + Uni uniContext = createTenantContext(oidcConfig, checkNamedTenants, tenantId); + try { + return uniContext.onFailure() + .recoverWithItem(new Function() { + @Override + public TenantConfigContext apply(Throwable t) { + if (t instanceof OIDCException) { + LOG.warnf("Tenant '%s': '%s'." + + " OIDC server is not available yet, an attempt to connect will be made during the first request." + + " Access to resources protected by this tenant may fail" + + " if OIDC server will not become available", + tenantId, t.getMessage()); + return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); + } + logTenantConfigContextFailure(t, tenantId); + if (t instanceof ConfigurationException + && !oidcConfig.authServerUrl().isPresent() + && LaunchMode.DEVELOPMENT == LaunchMode.current()) { + // Let it start if it is a DEV mode and auth-server-url has not been configured yet + return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); + } + // fail in all other cases + throw new OIDCException(t); + } + }) + .await().atMost(oidcConfig.connectionTimeout()); + } catch (TimeoutException t2) { + LOG.warnf("Tenant '%s': OIDC server is not available after a %d seconds timeout, an attempt to connect will be made" + + " during the first request. Access to resources protected by this tenant may fail if OIDC server" + + " will not become available", tenantId, oidcConfig.connectionTimeout().getSeconds()); + return TenantConfigContext.createNotReady(null, oidcConfig, staticTenantCreator); + } + } + + private Supplier> createStaticTenantContextCreator(OidcTenantConfig oidcConfig, + boolean checkNamedTenants, String tenantId) { + return new Supplier>() { + @Override + public Uni get() { + return createTenantContext(oidcConfig, checkNamedTenants, tenantId) + .onFailure().transform(new Function() { + @Override + public Throwable apply(Throwable t) { + return logTenantConfigContextFailure(t, tenantId); + } + }); + } + }; + } + + private Throwable logTenantConfigContextFailure(Throwable t, String tenantId) { + LOG.debugf( + "'%s' tenant is not initialized: '%s'. Access to resources protected by this tenant will fail.", + tenantId, t.getMessage()); + return t; + } + + @SuppressWarnings("resource") + private Uni createTenantContext(OidcTenantConfig oidcTenantConfig, + boolean checkNamedTenants, String tenantId) { + final OidcTenantConfig oidcConfig = OidcUtils.resolveProviderConfig(oidcTenantConfig); + + if (!oidcConfig.tenantEnabled()) { + LOG.debugf("'%s' tenant configuration is disabled", tenantId); + return Uni.createFrom().item(TenantConfigContext.createReady(new OidcProvider(null, null, null, null), oidcConfig)); + } + + if (oidcConfig.authServerUrl().isEmpty()) { + if (oidcConfig.publicKey().isPresent() && oidcConfig.certificateChain().trustStoreFile().isPresent()) { + throw new ConfigurationException("Both public key and certificate chain verification modes are enabled"); + } + if (oidcConfig.publicKey().isPresent()) { + return Uni.createFrom().item(createTenantContextFromPublicKey(oidcConfig)); + } + + if (oidcConfig.certificateChain().trustStoreFile().isPresent()) { + return Uni.createFrom().item(createTenantContextToVerifyCertChain(oidcConfig)); + } + } + + try { + if (oidcConfig.authServerUrl().isEmpty()) { + if (DEFAULT_TENANT_ID.equals(oidcConfig.tenantId().get())) { + ArcContainer container = Arc.container(); + if (container != null + && (container.instance(TenantConfigResolver.class).isAvailable() || checkNamedTenants)) { + LOG.debugf("Default tenant is not configured and will be disabled" + + " because either 'TenantConfigResolver' which will resolve tenant configurations is registered" + + " or named tenants are configured."); + oidcConfig.tenantEnabled = false; + return Uni.createFrom() + .item(TenantConfigContext.createReady(new OidcProvider(null, null, null, null), oidcConfig)); + } + } + throw new ConfigurationException( + "'" + getConfigPropertyForTenant(tenantId, "auth-server-url") + "' property must be configured"); + } + OidcCommonUtils.verifyEndpointUrl(oidcConfig.authServerUrl().get()); + OidcCommonUtils.verifyCommonConfiguration(oidcConfig, OidcUtils.isServiceApp(oidcConfig), true); + } catch (ConfigurationException t) { + return Uni.createFrom().failure(t); + } + + if (oidcConfig.roles().source().orElse(null) == io.quarkus.oidc.runtime.OidcTenantConfig.Roles.Source.userinfo + && !enableUserInfo(oidcConfig)) { + throw new ConfigurationException( + "UserInfo is not required but UserInfo is expected to be the source of authorization roles"); + } + if (oidcConfig.token().verifyAccessTokenWithUserInfo().orElse(false) && !OidcUtils.isWebApp(oidcConfig) + && !enableUserInfo(oidcConfig)) { + throw new ConfigurationException( + "UserInfo is not required but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (!oidcConfig.authentication().idTokenRequired().orElse(true) && !enableUserInfo(oidcConfig)) { + throw new ConfigurationException( + "UserInfo is not required but it will be needed to verify a code flow access token"); + } + + if (!oidcConfig.discoveryEnabled().orElse(true)) { + if (!OidcUtils.isServiceApp(oidcConfig)) { + if (oidcConfig.authorizationPath().isEmpty() || oidcConfig.tokenPath().isEmpty()) { + String authorizationPathProperty = getConfigPropertyForTenant(tenantId, "authorization-path"); + String tokenPathProperty = getConfigPropertyForTenant(tenantId, "token-path"); + throw new ConfigurationException( + "'web-app' applications must have '" + authorizationPathProperty + "' and '" + tokenPathProperty + + "' properties " + + "set when the discovery is disabled.", + Set.of(authorizationPathProperty, tokenPathProperty)); + } + } + // JWK and introspection endpoints have to be set for both 'web-app' and 'service' applications + if (oidcConfig.jwksPath().isEmpty() && oidcConfig.introspectionPath().isEmpty()) { + if (!oidcConfig.authentication().idTokenRequired().orElse(true) + && oidcConfig.authentication().userInfoRequired().orElse(false)) { + LOG.debugf("tenant %s supports only UserInfo", oidcConfig.tenantId().get()); + } else { + throw new ConfigurationException( + "Either 'jwks-path' or 'introspection-path' properties must be set when the discovery is disabled.", + Set.of("quarkus.oidc.jwks-path", "quarkus.oidc.introspection-path")); + } + } + if (oidcConfig.authentication().userInfoRequired().orElse(false) && oidcConfig.userInfoPath().isEmpty()) { + String configProperty = getConfigPropertyForTenant(tenantId, "user-info-path"); + throw new ConfigurationException( + "UserInfo is required but '" + configProperty + "' is not configured.", + Set.of(configProperty)); + } + } + + if (OidcUtils.isServiceApp(oidcConfig)) { + if (oidcConfig.token().refreshExpired()) { + throw new ConfigurationException( + "The '" + getConfigPropertyForTenant(tenantId, "token.refresh-expired") + + "' property can only be enabled for " + + io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.WEB_APP + + " application types"); + } + if (oidcConfig.token().refreshTokenTimeSkew().isPresent()) { + throw new ConfigurationException( + "The '" + getConfigPropertyForTenant(tenantId, "token.refresh-token-time-skew") + + "' property can only be enabled for " + + io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.WEB_APP + + " application types"); + } + if (oidcConfig.logout().path().isPresent()) { + throw new ConfigurationException( + "The '" + getConfigPropertyForTenant(tenantId, "logout.path") + "' property can only be enabled for " + + io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.WEB_APP + " application types"); + } + if (oidcConfig.roles().source().isPresent() + && oidcConfig.roles().source().get() == io.quarkus.oidc.runtime.OidcTenantConfig.Roles.Source.idtoken) { + throw new ConfigurationException( + "The '" + getConfigPropertyForTenant(tenantId, "roles.source") + + "' property can only be set to 'idtoken' for " + + io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.WEB_APP + + " application types"); + } + } else { + if (oidcConfig.token().refreshTokenTimeSkew().isPresent()) { + oidcConfig.token.setRefreshExpired(true); + } + } + + if (oidcConfig.tokenStateManager() + .strategy() != io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager.Strategy.KEEP_ALL_TOKENS) { + + if (oidcConfig.authentication().userInfoRequired().orElse(false) + || oidcConfig.roles().source() + .orElse(null) == io.quarkus.oidc.runtime.OidcTenantConfig.Roles.Source.userinfo) { + throw new ConfigurationException( + "UserInfo is required but DefaultTokenStateManager is configured to not keep the access token"); + } + if (oidcConfig.roles().source().orElse(null) == io.quarkus.oidc.runtime.OidcTenantConfig.Roles.Source.accesstoken) { + throw new ConfigurationException( + "Access token is required to check the roles but DefaultTokenStateManager is configured to not keep the access token"); + } + } + + if (oidcConfig.token().verifyAccessTokenWithUserInfo().orElse(false)) { + if (!oidcConfig.discoveryEnabled().orElse(true)) { + if (oidcConfig.userInfoPath().isEmpty()) { + throw new ConfigurationException( + "UserInfo path is missing but 'verifyAccessTokenWithUserInfo' is enabled"); + } + if (oidcConfig.introspectionPath().isPresent()) { + throw new ConfigurationException( + "Introspection path is configured and 'verifyAccessTokenWithUserInfo' is enabled, these options are mutually exclusive"); + } + } + } + + if (!oidcConfig.token().issuedAtRequired() && oidcConfig.token().age().isPresent()) { + String tokenIssuedAtRequired = getConfigPropertyForTenant(tenantId, "token.issued-at-required"); + String tokenAge = getConfigPropertyForTenant(tenantId, "token.age"); + throw new ConfigurationException( + "The '" + tokenIssuedAtRequired + "' can only be set to false if '" + tokenAge + "' is not set." + + " Either set '" + tokenIssuedAtRequired + "' to true or do not set '" + tokenAge + "'.", + Set.of(tokenIssuedAtRequired, tokenAge)); + } + + return createOidcProvider(oidcConfig) + .onItem().transform(new Function() { + @Override + public TenantConfigContext apply(OidcProvider p) { + return TenantConfigContext.createReady(p, oidcConfig); + } + }); + } + + private String getConfigPropertyForTenant(String tenantId, String configSubKey) { + if (DEFAULT_TENANT_ID.equals(tenantId)) { + return "quarkus.oidc." + configSubKey; + } else { + return "quarkus.oidc." + tenantId + "." + configSubKey; + } + } + + private boolean enableUserInfo(OidcTenantConfig oidcConfig) { + Optional userInfoRequired = oidcConfig.authentication().userInfoRequired(); + if (userInfoRequired.isPresent()) { + if (!userInfoRequired.get()) { + return false; + } + } else { + oidcConfig.authentication.setUserInfoRequired(true); + } + return true; + } + + private TenantConfigContext createTenantContextFromPublicKey(OidcTenantConfig oidcConfig) { + if (!OidcUtils.isServiceApp(oidcConfig)) { + throw new ConfigurationException("'public-key' property can only be used with the 'service' applications"); + } + LOG.debug("'public-key' property for the local token verification is set," + + " no connection to the OIDC server will be created"); + + return TenantConfigContext.createReady( + new OidcProvider(oidcConfig.publicKey().get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); + } + + private TenantConfigContext createTenantContextToVerifyCertChain(OidcTenantConfig oidcConfig) { + if (!OidcUtils.isServiceApp(oidcConfig)) { + throw new ConfigurationException( + "Currently only 'service' applications can be used to verify tokens with inlined certificate chains"); + } + + return TenantConfigContext.createReady( + new OidcProvider(null, oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); + } + + private OIDCException toOidcException(Throwable cause, String authServerUrl, String tenantId) { + final String message = OidcCommonUtils.formatConnectionErrorMessage(authServerUrl); + LOG.warn(message); + fireOidcServerNotAvailableEvent(authServerUrl, tenantId); + return new OIDCException("OIDC Server is not available", cause); + } + + private Uni createOidcProvider(OidcTenantConfig oidcConfig) { + return createOidcClientUni(oidcConfig) + .flatMap(new Function>() { + @Override + public Uni apply(OidcProviderClient client) { + if (oidcConfig.jwks().resolveEarly() + && client.getMetadata().getJsonWebKeySetUri() != null + && !oidcConfig.token().requireJwtIntrospectionOnly()) { + return getJsonWebSetUni(client, oidcConfig).onItem() + .transform(new Function() { + @Override + public OidcProvider apply(JsonWebKeySet jwks) { + return new OidcProvider(client, oidcConfig, jwks, + readTokenDecryptionKey(oidcConfig)); + } + }); + } else { + return Uni.createFrom() + .item(new OidcProvider(client, oidcConfig, null, readTokenDecryptionKey(oidcConfig))); + } + } + }); + } + + private Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) { + if (oidcConfig.token().decryptionKeyLocation().isPresent()) { + try { + Key key = null; + + String keyContent = KeyUtils.readKeyContent(oidcConfig.token().decryptionKeyLocation().get()); + if (keyContent != null) { + List keys = KeyUtils.loadJsonWebKeys(keyContent); + if (keys != null && keys.size() == 1 && + (keys.get(0).getAlgorithm() == null + || keys.get(0).getAlgorithm().equals(KeyEncryptionAlgorithm.RSA_OAEP.getAlgorithm())) + && ("enc".equals(keys.get(0).getUse()) || keys.get(0).getUse() == null)) { + key = PublicJsonWebKey.class.cast(keys.get(0)).getPrivateKey(); + } + } + if (key == null) { + key = KeyUtils.decodeDecryptionPrivateKey(keyContent); + } + return key; + } catch (Exception ex) { + throw new ConfigurationException( + String.format("Token decryption key for tenant %s can not be read from %s", + oidcConfig.tenantId().get(), oidcConfig.token().decryptionKeyLocation().get()), + ex); + } + } else { + return null; + } + } + + private Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) { + if (!oidcConfig.discoveryEnabled().orElse(true)) { + String tenantId = oidcConfig.tenantId().orElse(DEFAULT_TENANT_ID); + if (shouldFireOidcServerAvailableEvent(tenantId)) { + return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig) + .invoke(new Runnable() { + @Override + public void run() { + fireOidcServerAvailableEvent(oidcConfig.authServerUrl().get(), tenantId); + } + }); + } + return getJsonWebSetUniWhenDiscoveryDisabled(client, oidcConfig); + } else { + return client.getJsonWebKeySet(null); + } + } + + private Uni getJsonWebSetUniWhenDiscoveryDisabled(OidcProviderClient client, + OidcTenantConfig oidcConfig) { + final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); + return client.getJsonWebKeySet(null).onFailure(OidcCommonUtils.oidcEndpointNotAvailable()) + .retry() + .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION, OidcCommonUtils.CONNECTION_BACKOFF_DURATION) + .expireIn(connectionDelayInMillisecs) + .onFailure() + .transform(new Function() { + @Override + public Throwable apply(Throwable t) { + return toOidcException(t, oidcConfig.authServerUrl().get(), + oidcConfig.tenantId().orElse(DEFAULT_TENANT_ID)); + } + }) + .onFailure() + .invoke(client::close); + } + + private Uni createOidcClientUni(OidcTenantConfig oidcConfig) { + + String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig); + + WebClientOptions options = new WebClientOptions(); + options.setFollowRedirects(oidcConfig.followRedirects()); + OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsSupport.forConfig(oidcConfig.tls())); + var mutinyVertx = new io.vertx.mutiny.core.Vertx(vertx); + WebClient client = WebClient.create(mutinyVertx, options); + + Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters(); + Map> oidcResponseFilters = OidcCommonUtils.getOidcResponseFilters(); + + Uni metadataUni = null; + if (!oidcConfig.discoveryEnabled().orElse(true)) { + metadataUni = Uni.createFrom().item(createLocalMetadata(oidcConfig, authServerUriString)); + } else { + final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); + OidcRequestContextProperties contextProps = new OidcRequestContextProperties( + Map.of(OidcUtils.TENANT_ID_ATTRIBUTE, oidcConfig.tenantId().orElse(OidcUtils.DEFAULT_TENANT_ID))); + metadataUni = OidcCommonUtils + .discoverMetadata(client, oidcRequestFilters, contextProps, oidcResponseFilters, authServerUriString, + connectionDelayInMillisecs, + mutinyVertx, + oidcConfig.useBlockingDnsLookup()) + .onItem() + .transform(new Function() { + @Override + public OidcConfigurationMetadata apply(JsonObject json) { + return new OidcConfigurationMetadata(json, createLocalMetadata(oidcConfig, authServerUriString), + OidcCommonUtils.getDiscoveryUri(authServerUriString)); + } + }); + } + return metadataUni.onItemOrFailure() + .transformToUni(new BiFunction>() { + + @Override + public Uni apply(OidcConfigurationMetadata metadata, Throwable t) { + String tenantId = oidcConfig.tenantId().orElse(DEFAULT_TENANT_ID); + if (t != null) { + client.close(); + return Uni.createFrom().failure(toOidcException(t, authServerUriString, tenantId)); + } + if (shouldFireOidcServerAvailableEvent(tenantId)) { + fireOidcServerAvailableEvent(authServerUriString, tenantId); + } + if (metadata == null) { + client.close(); + return Uni.createFrom().failure(new ConfigurationException( + "OpenId Connect Provider configuration metadata is not configured and can not be discovered")); + } + if (oidcConfig.logout().path().isPresent()) { + if (oidcConfig.endSessionPath().isEmpty() && metadata.getEndSessionUri() == null) { + client.close(); + return Uni.createFrom().failure(new ConfigurationException( + "The application supports RP-Initiated Logout but the OpenID Provider does not advertise the end_session_endpoint")); + } + } + if (userInfoInjectionPointDetected && metadata.getUserInfoUri() != null) { + enableUserInfo(oidcConfig); + } + if (oidcConfig.authentication().userInfoRequired().orElse(false) && metadata.getUserInfoUri() == null) { + client.close(); + return Uni.createFrom().failure(new ConfigurationException( + "UserInfo is required but the OpenID Provider UserInfo endpoint is not configured." + + " Use 'quarkus.oidc.user-info-path' if the discovery is disabled.")); + } + return Uni.createFrom() + .item(new OidcProviderClient(client, vertx, metadata, oidcConfig, oidcRequestFilters, + oidcResponseFilters)); + } + + }); + } + + private OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oidcConfig, String authServerUriString) { + String tokenUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.tokenPath()); + String introspectionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, + oidcConfig.introspectionPath()); + String authorizationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, + oidcConfig.authorizationPath()); + String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath()); + String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath()); + String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath()); + String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath()); + return new OidcConfigurationMetadata(tokenUri, + introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri, + oidcConfig.token().issuer().orElse(null)); + } + + private void fireOidcServerNotAvailableEvent(String authServerUrl, String tenantId) { + if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_NOT_AVAILABLE)) { + tenantsExpectingServerAvailableEvents.add(tenantId); + } + } + + private void fireOidcServerAvailableEvent(String authServerUrl, String tenantId) { + if (fireOidcServerEvent(authServerUrl, OIDC_SERVER_AVAILABLE)) { + tenantsExpectingServerAvailableEvents.remove(tenantId); + } + } + + private boolean shouldFireOidcServerAvailableEvent(String tenantId) { + return tenantsExpectingServerAvailableEvents.contains(tenantId); + } + + private boolean fireOidcServerEvent(String authServerUrl, SecurityEvent.Type eventType) { + if (ConfigProvider.getConfig().getOptionalValue(SECURITY_EVENTS_ENABLED_CONFIG_KEY, boolean.class).orElse(true)) { + SecurityEventHelper.fire( + Arc.container().beanManager().getEvent().select(SecurityEvent.class), + new SecurityEvent(eventType, Map.of(AUTH_SERVER_URL, authServerUrl))); + return true; + } + return false; + } +} diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcRecorderTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcRecorderTest.java index ac74e246904c8..6f823f9622b1f 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcRecorderTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcRecorderTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import io.quarkus.oidc.common.runtime.OidcCommonConfig.Proxy; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; public class OidcRecorderTest { @@ -14,13 +15,13 @@ public class OidcRecorderTest { public void testtoProxyOptionsWithHostCheckPresent() { Proxy proxy = new Proxy(); proxy.host = Optional.of("server.example.com"); - assertTrue(OidcRecorder.toProxyOptions(proxy).isPresent()); + assertTrue(OidcCommonUtils.toProxyOptions(proxy).isPresent()); } @Test public void testtoProxyOptionsWithoutHostCheckNonPresent() { Proxy proxy = new Proxy(); - assertFalse(OidcRecorder.toProxyOptions(proxy).isPresent()); + assertFalse(OidcCommonUtils.toProxyOptions(proxy).isPresent()); } }