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..1e5bc86d05267 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,58 @@ 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 jakarta.enterprise.event.Observes; + +public class OidcStartup { + + void observe(@Observes Oidc oidc) { + oidc.createServiceApp("http://localhost:8180/realms/quarkus"); + } + +} +---- + +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 +---- + +Should you need to configure more OIDC tenant properties, use the `OidcTenantConfig` builder 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 createDefaultTenant(@Observes Oidc oidc) { + var defaultTenant = OidcTenantConfig + .authServerUrl("http://localhost:8180/realms/quarkus") + .token().requireJwtIntrospectionOnly().end() + .build(); + oidc.create(defaultTenant); + } +} +---- + +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..7f5495186009a 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,63 @@ 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 jakarta.enterprise.event.Observes; + +public class OidcStartup { + + void observe(@Observes Oidc oidc) { + oidc.createWebApp("http://localhost:8180/realms/quarkus", "quarkus-app", "mysecret"); + } + +} +---- + +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 +---- + +Should you need to configure more OIDC tenant properties, use the `OidcTenantConfig` builder like in the example below: + +[source,java] +---- +package io.quarkus.it.oidc; + +import io.quarkus.oidc.Oidc; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig.Credentials.Secret.Method; +import jakarta.enterprise.event.Observes; + +public class OidcStartup { + + void createDefaultTenant(@Observes Oidc oidc) { + var defaultTenant = OidcTenantConfig + .authServerUrl("http://localhost:8180/realms/quarkus/") + .clientId("quarkus-app") + .credentials().clientSecret("mysecret", Method.POST).end() + .build(); + oidc.create(defaultTenant); + } +} +---- + +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..2130649c0a901 100644 --- a/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-multitenancy.adoc @@ -1110,6 +1110,40 @@ 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 multiple tenants + +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()); <2> + oidc.create(OidcTenantConfig.authServerUrl("http://localhost:8180/realms/tenant-two").tenantId("tenant-two").build()); <3> + } + +} +---- +<1> Observe OIDC event. +<2> Create OIDC tenant 'tenant-one'. +<3> Create OIDC tenant 'tenant-two'. + +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 +---- + == 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..5cb43e82f21ca 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 @@ -2,6 +2,7 @@ import static io.quarkus.arc.processor.BuiltinScope.APPLICATION; import static io.quarkus.arc.processor.DotNames.DEFAULT; +import static io.quarkus.arc.processor.DotNames.EVENT; import static io.quarkus.arc.processor.DotNames.NAMED; import static io.quarkus.oidc.common.runtime.OidcConstants.BEARER_SCHEME; import static io.quarkus.oidc.common.runtime.OidcConstants.CODE_FLOW_CODE; @@ -24,11 +25,14 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; 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; @@ -51,16 +55,18 @@ 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; +import io.quarkus.oidc.Oidc; import io.quarkus.oidc.Tenant; import io.quarkus.oidc.TenantFeature; import io.quarkus.oidc.TenantIdentityProvider; @@ -84,9 +90,11 @@ import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer; +import io.quarkus.security.runtime.SecurityConfig; 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 +175,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) @@ -303,23 +310,17 @@ 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)); } + // 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) @Consume(SyntheticBeansRuntimeInitBuildItem.class) @Record(ExecutionTime.RUNTIME_INIT) @BuildStep @@ -327,6 +328,20 @@ void initTenantConfigBean(OidcRecorder recorder) { recorder.initTenantConfigBean(); } + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + SyntheticBeanBuildItem setup(OidcConfig config, OidcRecorder recorder, SecurityConfig securityConfig, + CoreVertxBuildItem vertxBuildItem, TlsRegistryBuildItem tlsRegistryBuildItem) { + return SyntheticBeanBuildItem.configure(TenantConfigBean.class).unremovable().types(TenantConfigBean.class) + .addInjectionPoint(ParameterizedType.create(EVENT, ClassType.create(Oidc.class))) + .createWith(recorder.createTenantConfigBean(config, vertxBuildItem.getVertx(), tlsRegistryBuildItem.registry(), + securityConfig)) + .destroyer(TenantConfigBean.Destroyer.class) + .scope(Singleton.class) // this should have been @ApplicationScoped but fails for some reason + .setRuntimeInit() + .done(); + } + @BuildStep @Record(ExecutionTime.STATIC_INIT) public void registerTenantResolverInterceptor(Capabilities capabilities, OidcRecorder recorder, 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..1af32ce10d75a 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,12 @@ 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.tenant-paths=/user-info/default-tenant-random 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 +56,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 +100,41 @@ 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); + } + // assert tenant path added in the observer method + assertTenantPathsContain("/extra-default-tenant-path"); + // assert tenant path added in the application.properties + assertTenantPathsContain("/user-info/default-tenant-random"); 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); + } + assertTenantPathsContain("/user-info/named-tenant-random"); return userInfo.getPreferredUserName(); } @@ -135,6 +155,38 @@ public boolean getNamed3TenantUserInfoRequired() { private OidcTenantConfig getNamedTenantConfig(String configName) { return tenantConfigBean.getStaticTenant(configName).oidcConfig(); } + + private void assertTenantPathsContain(String tenantPath) { + OidcTenantConfig tenantConfig = routingContext.get(OidcTenantConfig.class.getName()); + if (!tenantConfig.tenantPaths().get().contains(tenantPath)) { + throw new IllegalStateException("Tenant config does not contain the tenant path " + tenantPath); + } + } } + 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("/extra-default-tenant-path") + .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..ede66c4e7a67f --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Oidc.java @@ -0,0 +1,49 @@ +package io.quarkus.oidc; + +import io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig; +import io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType; + +/** + * A CDI event that facilitates programmatic OIDC tenant setup. + * OIDC tenants must be created from an observer method like in the example below: + * + *
+ * {@code
+ * public class OidcSetup {
+ *
+ *     void createDefaultTenant(@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. + */ +public interface Oidc { + + /** + * Creates OIDC tenant. + * + * @param tenantConfig tenant config; must not be null + */ + void create(OidcTenantConfig tenantConfig); + + /** + * Creates default OIDC tenant with the {@link ApplicationType#SERVICE} application type. + * + * @param authServerUrl {@link OidcTenantConfig#authServerUrl()} + */ + void createServiceApp(String authServerUrl); + + /** + * Creates default OIDC tenant with the {@link ApplicationType#WEB_APP} application type. + * + * @param authServerUrl {@link OidcTenantConfig#authServerUrl()} + * @param clientId {@link OidcTenantConfig#clientId()} + * @param clientSecret {@link OidcClientCommonConfig.Credentials#secret()} + */ + void createWebApp(String authServerUrl, String clientId, String clientSecret); +} 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..9d08fb179185b 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 { + private 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/OidcImpl.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcImpl.java new file mode 100644 index 0000000000000..23f92da74af3e --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcImpl.java @@ -0,0 +1,70 @@ +package io.quarkus.oidc.runtime; + +import static io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.SERVICE; +import static io.quarkus.oidc.runtime.OidcTenantConfig.ApplicationType.WEB_APP; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import io.quarkus.oidc.Oidc; +import io.quarkus.oidc.OidcTenantConfig; + +final class OidcImpl implements Oidc { + + private Map staticTenantConfigs; + private OidcTenantConfig defaultTenantConfig; + + OidcImpl(OidcConfig config) { + this.defaultTenantConfig = OidcTenantConfig.of(OidcConfig.getDefaultTenant(config)); + this.staticTenantConfigs = getStaticTenants(config); + } + + @Override + public void create(OidcTenantConfig tenantConfig) { + Objects.requireNonNull(tenantConfig); + putStaticTenantConfig(tenantConfig); + } + + @Override + public void createServiceApp(String authServerUrl) { + create(OidcTenantConfig.authServerUrl(authServerUrl).applicationType(SERVICE).build()); + } + + @Override + public void createWebApp(String authServerUrl, String clientId, String clientSecret) { + create(OidcTenantConfig.authServerUrl(authServerUrl).clientId(clientId).applicationType(WEB_APP) + .credentials(clientSecret).build()); + } + + Map getStaticTenantConfigs() { + return Collections.unmodifiableMap(staticTenantConfigs); + } + + OidcTenantConfig getDefaultTenantConfig() { + return defaultTenantConfig; + } + + 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..781341d041b48 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,46 @@ 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 jakarta.enterprise.util.TypeLiteral; -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.arc.SyntheticCreationalContext; 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.runtime.SecurityConfig; 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,13 +51,22 @@ public DefaultTokenIntrospectionUserInfoCache get() { }; } - public Supplier createTenantConfigBean(OidcConfig config, Supplier vertx, - Supplier registrySupplier, - boolean userInfoInjectionPointDetected) { - return new Supplier() { + @StaticInit + public void setUserInfoInjectionPointDetected(boolean userInfoInjectionPointDetected) { + TenantContextFactory.userInfoInjectionPointDetected = userInfoInjectionPointDetected; + } + + @RuntimeInit + public Function, TenantConfigBean> createTenantConfigBean( + OidcConfig config, Supplier vertx, Supplier registry, + SecurityConfig securityConfig) { + return new Function, TenantConfigBean>() { @Override - public TenantConfigBean get() { - return setup(config, vertx.get(), OidcTlsSupport.of(registrySupplier), userInfoInjectionPointDetected); + public TenantConfigBean apply(SyntheticCreationalContext ctx) { + final OidcImpl oidc = new OidcImpl(config); + ctx.getInjectedReference(new TypeLiteral>() { + }).fire(oidc); + return new TenantConfigBean(vertx.get(), registry.get(), oidc, securityConfig.events().enabled()); } }; } @@ -108,545 +84,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..18be087d303de 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 @@ -8,28 +8,26 @@ import io.quarkus.arc.BeanDestroyer; import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.tls.TlsConfigurationRegistry; import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; -public class TenantConfigBean { +public final class TenantConfigBean { private final Map staticTenantsConfig; private final Map dynamicTenantsConfig; private final TenantConfigContext defaultTenant; private final TenantContextFactory tenantContextFactory; - @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, + boolean securityEventsEnabled) { + this.tenantContextFactory = new TenantContextFactory(vertx, tlsConfigurationRegistry, securityEventsEnabled); this.dynamicTenantsConfig = new ConcurrentHashMap<>(); - this.defaultTenant = defaultTenant; - this.tenantContextFactory = tenantContextFactory; + + this.staticTenantsConfig = tenantContextFactory.createStaticTenantConfigs(oidc.getStaticTenantConfigs(), + oidc.getDefaultTenantConfig()); + this.defaultTenant = tenantContextFactory.createDefaultTenantConfig(oidc.getStaticTenantConfigs(), + oidc.getDefaultTenantConfig()); } public Uni createDynamicTenantContext(OidcTenantConfig oidcConfig) { @@ -40,7 +38,7 @@ public Uni createDynamicTenantContext(OidcTenantConfig oidc 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) { 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..0faf8f36e361f --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantContextFactory.java @@ -0,0 +1,597 @@ +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.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 { + + static volatile boolean userInfoInjectionPointDetected = false; + + private final Set tenantsExpectingServerAvailableEvents; + private final Vertx vertx; + private final OidcTlsSupport tlsSupport; + private final boolean securityEventsEnabled; + + TenantContextFactory(Vertx vertx, TlsConfigurationRegistry tlsConfigurationRegistry, boolean securityEventsEnabled) { + this.vertx = vertx; + this.tlsSupport = OidcTlsSupport.of(tlsConfigurationRegistry); + this.securityEventsEnabled = securityEventsEnabled; + this.tenantsExpectingServerAvailableEvents = ConcurrentHashMap.newKeySet(); + } + + TenantConfigContext createDefaultTenantConfig(Map staticTenants, OidcTenantConfig defaultTenant) { + String defaultTenantId = defaultTenant.tenantId().get(); + boolean foundNamedStaticTenants = !staticTenants.isEmpty(); + var defaultTenantInitializer = createStaticTenantContextCreator(defaultTenant, foundNamedStaticTenants, + defaultTenantId); + return createStaticTenantContext(defaultTenant, foundNamedStaticTenants, defaultTenantId, defaultTenantInitializer); + } + + Map createStaticTenantConfigs(Map staticTenants, + OidcTenantConfig defaultTenant) { + final String defaultTenantId = defaultTenant.tenantId().get(); + Map staticTenantsConfig = new HashMap<>(); + for (var tenant : staticTenants.entrySet()) { + createStaticTenantConfig(defaultTenantId, tenant.getKey(), tenant.getValue(), staticTenantsConfig); + } + return Map.copyOf(staticTenantsConfig); + } + + Uni createDynamic(OidcTenantConfig oidcConfig) { + var tenantId = oidcConfig.tenantId().orElseThrow(); + if (OidcUtils.DEFAULT_TENANT_ID.equals(tenantId)) { + throw new ConfigurationException("Dynamic tenant ID cannot be same as the default tenant ID: " + tenantId); + } + 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 (securityEventsEnabled) { + 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/OidcImplTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcImplTest.java new file mode 100644 index 0000000000000..0dff2a67b406f --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcImplTest.java @@ -0,0 +1,49 @@ +package io.quarkus.oidc.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +public class OidcImplTest { + + @Test + public void testCreateServiceApp() { + final OidcImpl oidc = new OidcImpl(getEmptyConfig()); + oidc.createServiceApp("auth-server-url-1"); + final OidcTenantConfig defaultTenantConfig = oidc.getDefaultTenantConfig(); + assertEquals("auth-server-url-1", defaultTenantConfig.authServerUrl().get()); + assertEquals(OidcTenantConfig.ApplicationType.SERVICE, defaultTenantConfig.applicationType().get()); + } + + @Test + public void testCreateWebApp() { + final OidcImpl oidc = new OidcImpl(getEmptyConfig()); + oidc.createWebApp("auth-server-url-1", "client5", "secret2"); + final OidcTenantConfig defaultTenantConfig = oidc.getDefaultTenantConfig(); + assertEquals("auth-server-url-1", defaultTenantConfig.authServerUrl().get()); + assertEquals(OidcTenantConfig.ApplicationType.WEB_APP, defaultTenantConfig.applicationType().get()); + assertEquals("client5", defaultTenantConfig.clientId().get()); + assertEquals("secret2", defaultTenantConfig.credentials().secret().get()); + } + + private static OidcConfig getEmptyConfig() { + return new OidcConfig() { + @Override + public Map namedTenants() { + return Map.of(OidcConfig.DEFAULT_TENANT_KEY, io.quarkus.oidc.OidcTenantConfig.builder().build()); + } + + @Override + public TokenCache tokenCache() { + return null; + } + + @Override + public boolean resolveTenantsWithIssuer() { + 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()); } }