From 981f8a697fc8a6e859905c52016cb54b57b10a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Thu, 31 Oct 2024 17:54:15 +0100 Subject: [PATCH] Migrate OIDC, OIDC client and registration to @ConfigMapping --- .../KeycloakPolicyEnforcerBuildStep.java | 4 +- .../DefaultPolicyEnforcerResolver.java | 6 +- .../runtime/KeycloakPolicyEnforcerUtil.java | 6 +- .../deployment/pom.xml | 3 - .../OidcClientRegistrationBuildStep.java | 2 +- ...OidcClientRegistrationBuildTimeConfig.java | 10 +- .../oidc-client-registration/runtime/pom.xml | 3 - .../OidcClientRegistrationConfig.java | 34 +- .../runtime/OidcClientRegistrationConfig.java | 65 + .../OidcClientRegistrationRecorder.java | 9 +- .../OidcClientRegistrationsConfig.java | 17 +- .../OidcClientRegistrationConfigImpl.java | 277 ++++ .../OidcClientRegistrationConfigTest.java | 23 + extensions/oidc-client/deployment/pom.xml | 3 - .../deployment/OidcClientBuildStep.java | 2 +- extensions/oidc-client/runtime/pom.xml | 3 - .../quarkus/oidc/client/OidcClientConfig.java | 48 +- .../runtime/AbstractTokensProducer.java | 6 +- .../runtime/OidcClientBuildTimeConfig.java | 12 +- .../oidc/client/runtime/OidcClientConfig.java | 168 +++ .../client/runtime/OidcClientRecorder.java | 12 +- .../client/runtime/OidcClientsConfig.java | 17 +- .../oidc/client/OidcClientConfigImpl.java | 561 ++++++++ .../oidc/client/OidcClientConfigTest.java | 23 + extensions/oidc-common/runtime/pom.xml | 3 - .../runtime/OidcClientCommonConfig.java | 96 +- .../oidc/common/runtime/OidcCommonConfig.java | 88 +- .../config/OidcClientCommonConfig.java | 269 ++++ .../runtime/config/OidcCommonConfig.java | 249 ++++ extensions/oidc/deployment/pom.xml | 3 - .../quarkus/oidc/deployment/DevUiConfig.java | 25 +- .../oidc/deployment/OidcBuildStep.java | 4 +- .../oidc/deployment/OidcBuildTimeConfig.java | 18 +- .../devservices/OidcDevUIProcessor.java | 7 +- .../keycloak/KeycloakDevUIProcessor.java | 6 +- .../quarkus/oidc/test/ProtectedResource.java | 2 +- .../ProtectedResourceWithJwtAccessToken.java | 2 +- ...rotectedResourceWithoutJwtAccessToken.java | 2 +- .../test/UserInfoRequiredDetectionTest.java | 19 +- extensions/oidc/runtime/pom.xml | 3 - .../io/quarkus/oidc/OidcTenantConfig.java | 287 ++-- .../runtime/BackChannelLogoutHandler.java | 6 +- ...efaultTokenIntrospectionUserInfoCache.java | 4 +- .../io/quarkus/oidc/runtime/OidcConfig.java | 40 +- .../io/quarkus/oidc/runtime/OidcRecorder.java | 22 +- .../oidc/runtime/OidcTenantConfig.java | 1142 +++++++++++++++ .../oidc/runtime/OidcTenantConfigImpl.java | 1238 +++++++++++++++++ .../oidc/runtime/OidcTenantConfigTest.java | 39 + .../runtime/TokenIntrospectionCacheTest.java | 13 +- ...nantSpecificJwtPreferredNameValidator.java | 3 +- .../it/keycloak/OidcEventResource.java | 2 +- 51 files changed, 4505 insertions(+), 401 deletions(-) create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationConfig.java create mode 100644 extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigImpl.java create mode 100644 extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigTest.java create mode 100644 extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java create mode 100644 extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java create mode 100644 extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigTest.java create mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java create mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java create mode 100644 extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java create mode 100644 extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java create mode 100644 extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigTest.java diff --git a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java index 3e7a54d0a37e1..0e06027a53539 100644 --- a/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java +++ b/extensions/keycloak-authorization/deployment/src/main/java/io/quarkus/keycloak/pep/deployment/KeycloakPolicyEnforcerBuildStep.java @@ -25,7 +25,7 @@ public class KeycloakPolicyEnforcerBuildStep { RequireBodyHandlerBuildItem requireBody(OidcBuildTimeConfig oidcBuildTimeConfig, KeycloakPolicyEnforcerRecorder recorder, KeycloakPolicyEnforcerConfig runtimeConfig) { - if (oidcBuildTimeConfig.enabled) { + if (oidcBuildTimeConfig.enabled()) { return new RequireBodyHandlerBuildItem(recorder.createBodyHandlerRequiredEvaluator(runtimeConfig)); } return null; @@ -33,7 +33,7 @@ RequireBodyHandlerBuildItem requireBody(OidcBuildTimeConfig oidcBuildTimeConfig, @BuildStep public AdditionalBeanBuildItem beans(OidcBuildTimeConfig oidcBuildTimeConfig) { - if (oidcBuildTimeConfig.enabled) { + if (oidcBuildTimeConfig.enabled()) { return AdditionalBeanBuildItem.builder().setUnremovable() .addBeanClass(KeycloakPolicyEnforcerAuthorizer.class) .addBeanClass(DefaultPolicyEnforcerResolver.class) 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 6ab41a3ffbc00..f7ec6d61f7215 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 @@ -20,6 +20,7 @@ import io.quarkus.oidc.common.runtime.OidcTlsSupport; import io.quarkus.oidc.runtime.BlockingTaskRunner; import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.oidc.runtime.OidcUtils; import io.quarkus.security.spi.runtime.BlockingSecurityExecutor; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.vertx.http.runtime.HttpConfiguration; @@ -48,8 +49,9 @@ public class DefaultPolicyEnforcerResolver implements PolicyEnforcerResolver { this.tlsSupport = OidcTlsSupport.empty(); } - var defaultTenantTlsSupport = tlsSupport.forConfig(oidcConfig.defaultTenant.tls); - this.defaultPolicyEnforcer = createPolicyEnforcer(oidcConfig.defaultTenant, config.defaultTenant(), + var defaultTenantConfig = new OidcTenantConfig(oidcConfig.defaultTenant(), OidcUtils.DEFAULT_TENANT_ID); + var defaultTenantTlsSupport = tlsSupport.forConfig(defaultTenantConfig.tls); + this.defaultPolicyEnforcer = createPolicyEnforcer(defaultTenantConfig, config.defaultTenant(), defaultTenantTlsSupport); this.namedPolicyEnforcers = createNamedPolicyEnforcers(oidcConfig, config, tlsSupport); if (configResolver.isResolvable()) { 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 ae3a94a39f840..47330c2f01caa 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 @@ -226,13 +226,13 @@ private static boolean isNotComplexConfigKey(String key) { static OidcTenantConfig getOidcTenantConfig(OidcConfig oidcConfig, String tenant) { if (tenant == null || DEFAULT_TENANT_ID.equals(tenant)) { - return oidcConfig.defaultTenant; + return new OidcTenantConfig(oidcConfig.defaultTenant(), DEFAULT_TENANT_ID); } - OidcTenantConfig oidcTenantConfig = oidcConfig.namedTenants.get(tenant); + var oidcTenantConfig = oidcConfig.namedTenants().get(tenant); if (oidcTenantConfig == null) { throw new ConfigurationException("Failed to find a matching OidcTenantConfig for tenant: " + tenant); } - return oidcTenantConfig; + return new OidcTenantConfig(oidcTenantConfig, tenant); } } diff --git a/extensions/oidc-client-registration/deployment/pom.xml b/extensions/oidc-client-registration/deployment/pom.xml index b4b162dd77f22..3ef0d8dbb10d7 100644 --- a/extensions/oidc-client-registration/deployment/pom.xml +++ b/extensions/oidc-client-registration/deployment/pom.xml @@ -58,9 +58,6 @@ ${project.version} - - -AlegacyConfigRoot=true - diff --git a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java index f0d71493b33d2..cbc6ac342ee32 100644 --- a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java +++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java @@ -61,7 +61,7 @@ public static class IsEnabled implements BooleanSupplier { OidcClientRegistrationBuildTimeConfig config; public boolean getAsBoolean() { - return config.enabled; + return config.enabled(); } } } diff --git a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java index 98d78afa9acf1..6da2cab23cdee 100644 --- a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java +++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java @@ -1,16 +1,18 @@ package io.quarkus.oidc.client.registration.deployment; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; /** * Build time configuration for OIDC client registration. */ +@ConfigMapping(prefix = "quarkus.oidc-client-registration") @ConfigRoot -public class OidcClientRegistrationBuildTimeConfig { +public interface OidcClientRegistrationBuildTimeConfig { /** * If the OIDC client registration extension is enabled. */ - @ConfigItem(defaultValue = "true") - public boolean enabled; + @WithDefault("true") + boolean enabled(); } diff --git a/extensions/oidc-client-registration/runtime/pom.xml b/extensions/oidc-client-registration/runtime/pom.xml index 5a7b0038d55bb..7661a208f4e82 100644 --- a/extensions/oidc-client-registration/runtime/pom.xml +++ b/extensions/oidc-client-registration/runtime/pom.xml @@ -57,9 +57,6 @@ ${project.version} - - -AlegacyConfigRoot=true - diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java index 871d3a515c4c7..40abcea94284e 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java @@ -5,72 +5,80 @@ import java.util.Optional; import io.quarkus.oidc.common.runtime.OidcCommonConfig; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; //https://datatracker.ietf.org/doc/html/rfc7592 //https://openid.net/specs/openid-connect-registration-1_0.html -@ConfigGroup public class OidcClientRegistrationConfig extends OidcCommonConfig { + public OidcClientRegistrationConfig() { + + } + + public OidcClientRegistrationConfig(io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationConfig mapping) { + super(mapping); + id = mapping.id(); + registrationEnabled = mapping.registrationEnabled(); + registerEarly = mapping.registerEarly(); + initialToken = mapping.initialToken(); + metadata.addConfigMappingValues(mapping.metadata()); + } + /** * OIDC Client Registration id */ - @ConfigItem public Optional id = Optional.empty(); /** * If this client registration configuration is enabled. */ - @ConfigItem(defaultValue = "true") public boolean registrationEnabled = true; /** * If the client configured with {@link #metadata} must be registered at startup. */ - @ConfigItem(defaultValue = "true") public boolean registerEarly = true; /** * Initial access token */ - @ConfigItem public Optional initialToken = Optional.empty(); /** * Client metadata */ - @ConfigItem public Metadata metadata = new Metadata(); /** * Client metadata */ - @ConfigGroup public static class Metadata { /** * Client name */ - @ConfigItem public Optional clientName = Optional.empty(); /** * Redirect URI */ - @ConfigItem public Optional redirectUri = Optional.empty(); /** * Post Logout URI */ - @ConfigItem public Optional postLogoutUri = Optional.empty(); /** * Additional metadata properties */ - @ConfigItem public Map extraProps = new HashMap<>(); + + private void addConfigMappingValues( + io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationConfig.Metadata mapping) { + this.clientName = mapping.clientName(); + this.redirectUri = mapping.redirectUri(); + this.postLogoutUri = mapping.postLogoutUri(); + this.extraProps.putAll(mapping.extraProps()); + } } } diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationConfig.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationConfig.java new file mode 100644 index 0000000000000..e7b461772ad41 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationConfig.java @@ -0,0 +1,65 @@ +package io.quarkus.oidc.client.registration.runtime; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.oidc.common.runtime.config.OidcCommonConfig; +import io.smallrye.config.WithDefault; + +//https://datatracker.ietf.org/doc/html/rfc7592 +//https://openid.net/specs/openid-connect-registration-1_0.html + +public interface OidcClientRegistrationConfig extends OidcCommonConfig { + + /** + * OIDC Client Registration id + */ + Optional id(); + + /** + * If this client registration configuration is enabled. + */ + @WithDefault("true") + boolean registrationEnabled(); + + /** + * If the client configured with {@link #metadata} must be registered at startup. + */ + @WithDefault("true") + boolean registerEarly(); + + /** + * Initial access token + */ + Optional initialToken(); + + /** + * Client metadata + */ + Metadata metadata(); + + /** + * Client metadata + */ + interface Metadata { + /** + * Client name + */ + Optional clientName(); + + /** + * Redirect URI + */ + Optional redirectUri(); + + /** + * Post Logout URI + */ + Optional postLogoutUri(); + + /** + * Additional metadata properties + */ + Map extraProps(); + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java index ea7cce1cfbcd1..de067dbb8aa30 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java @@ -41,15 +41,16 @@ public OidcClientRegistrations setup(OidcClientRegistrationsConfig oidcClientReg Supplier vertx, Supplier registrySupplier) { var tlsSupport = OidcTlsSupport.of(registrySupplier); - OidcClientRegistration defaultClientReg = createOidcClientRegistration(oidcClientRegsConfig.defaultClientRegistration, + var defaultClientRegistration = new OidcClientRegistrationConfig(oidcClientRegsConfig.defaultClientRegistration()); + OidcClientRegistration defaultClientReg = createOidcClientRegistration(defaultClientRegistration, tlsSupport, vertx); Map staticOidcClientRegs = new HashMap<>(); - for (Map.Entry config : oidcClientRegsConfig.namedClientRegistrations - .entrySet()) { + for (var config : oidcClientRegsConfig.namedClientRegistrations().entrySet()) { + var namedClientRegistration = new OidcClientRegistrationConfig(config.getValue()); staticOidcClientRegs.put(config.getKey(), - createOidcClientRegistration(config.getValue(), tlsSupport, vertx)); + createOidcClientRegistration(namedClientRegistration, tlsSupport, vertx)); } return new OidcClientRegistrationsImpl(defaultClientReg, staticOidcClientRegs, diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java index 5d2772fdc7fd7..976897060e91b 100644 --- a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java @@ -2,27 +2,28 @@ import java.util.Map; -import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigDocSection; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithParentName; -@ConfigRoot(name = "oidc-client-registration", phase = ConfigPhase.RUN_TIME) -public class OidcClientRegistrationsConfig { +@ConfigMapping(prefix = "quarkus.oidc-client-registration") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface OidcClientRegistrationsConfig { /** * The default client registration. */ - @ConfigItem(name = ConfigItem.PARENT) - public OidcClientRegistrationConfig defaultClientRegistration; + @WithParentName + OidcClientRegistrationConfig defaultClientRegistration(); /** * Additional named client registrations. */ @ConfigDocSection @ConfigDocMapKey("id") - @ConfigItem(name = ConfigItem.PARENT) - public Map namedClientRegistrations; + @WithParentName + Map namedClientRegistrations(); } diff --git a/extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigImpl.java b/extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigImpl.java new file mode 100644 index 0000000000000..2fd0dba336ef8 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigImpl.java @@ -0,0 +1,277 @@ +package io.quarkus.oidc.client.registration; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationConfig; + +final class OidcClientRegistrationConfigImpl implements OidcClientRegistrationConfig { + + enum ConfigMappingMethods { + ID, + REGISTRATION_ENABLED, + REGISTER_EARLY, + INITIAL_TOKEN, + METADATA, + METADATA_CLIENT_NAME, + METADATA_REDIRECT_URI, + METADATA_POST_LOGOUT_URI, + METADATA_EXTRA_PROPS, + AUTH_SERVER_URL, + DISCOVERY_ENABLED, + REGISTRATION_PATH, + CONNECTION_DELAY, + CONNECTION_RETRY_COUNT, + CONNECTION_TIMEOUT, + USE_BLOCKING_DNS_LOOKUP, + MAX_POOL_SIZE, + FOLLOW_REDIRECTS, + PROXY, + PROXY_HOST, + PROXY_PORT, + PROXY_USERNAME, + PROXY_PASSWORD, + TLS, + TLS_CONFIGURATION, + TLS_VERIFICATION, + TLS_KEYSTORE_FILE, + TLS_KEYSTORE_FILE_TYPE, + TLS_KEYSTORE_PROVIDER, + TLS_KEYSTORE_PASSWORD, + TLS_KEYSTORE_KEY_ALIAS, + TLS_KEYSTORE_KEY_PASSWORD, + TLS_TRUSTSTORE_PASSWORD, + TLS_TRUSTSTORE_FILE, + TLS_TRUSTSTORE_CERT_ALIAS, + TLS_TRUSTSTORE_FILE_TYPE, + TLS_TRUSTSTORE_PROVIDER + } + + final Map invocationsRecorder = new HashMap<>(); + + @Override + public Optional id() { + invocationsRecorder.put(ConfigMappingMethods.ID, true); + return Optional.empty(); + } + + @Override + public boolean registrationEnabled() { + invocationsRecorder.put(ConfigMappingMethods.REGISTRATION_ENABLED, true); + return false; + } + + @Override + public boolean registerEarly() { + invocationsRecorder.put(ConfigMappingMethods.REGISTER_EARLY, true); + return false; + } + + @Override + public Optional initialToken() { + invocationsRecorder.put(ConfigMappingMethods.INITIAL_TOKEN, true); + return Optional.empty(); + } + + @Override + public Metadata metadata() { + invocationsRecorder.put(ConfigMappingMethods.METADATA, true); + return new Metadata() { + @Override + public Optional clientName() { + invocationsRecorder.put(ConfigMappingMethods.METADATA_CLIENT_NAME, true); + return Optional.empty(); + } + + @Override + public Optional redirectUri() { + invocationsRecorder.put(ConfigMappingMethods.METADATA_REDIRECT_URI, true); + return Optional.empty(); + } + + @Override + public Optional postLogoutUri() { + invocationsRecorder.put(ConfigMappingMethods.METADATA_POST_LOGOUT_URI, true); + return Optional.empty(); + } + + @Override + public Map extraProps() { + invocationsRecorder.put(ConfigMappingMethods.METADATA_EXTRA_PROPS, true); + return Map.of(); + } + }; + } + + @Override + public Optional authServerUrl() { + invocationsRecorder.put(ConfigMappingMethods.AUTH_SERVER_URL, true); + return Optional.empty(); + } + + @Override + public Optional discoveryEnabled() { + invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_ENABLED, true); + return Optional.empty(); + } + + @Override + public Optional registrationPath() { + invocationsRecorder.put(ConfigMappingMethods.REGISTRATION_PATH, true); + return Optional.empty(); + } + + @Override + public Optional connectionDelay() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_DELAY, true); + return Optional.empty(); + } + + @Override + public int connectionRetryCount() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_RETRY_COUNT, true); + return 0; + } + + @Override + public Duration connectionTimeout() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_TIMEOUT, true); + return null; + } + + @Override + public boolean useBlockingDnsLookup() { + invocationsRecorder.put(ConfigMappingMethods.USE_BLOCKING_DNS_LOOKUP, true); + return false; + } + + @Override + public OptionalInt maxPoolSize() { + invocationsRecorder.put(ConfigMappingMethods.MAX_POOL_SIZE, true); + return OptionalInt.empty(); + } + + @Override + public boolean followRedirects() { + invocationsRecorder.put(ConfigMappingMethods.FOLLOW_REDIRECTS, true); + return false; + } + + @Override + public Proxy proxy() { + invocationsRecorder.put(ConfigMappingMethods.PROXY, true); + return new Proxy() { + @Override + public Optional host() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_HOST, true); + return Optional.empty(); + } + + @Override + public int port() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_PORT, true); + return 0; + } + + @Override + public Optional username() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_USERNAME, true); + return Optional.empty(); + } + + @Override + public Optional password() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_PASSWORD, true); + return Optional.empty(); + } + }; + } + + @Override + public Tls tls() { + invocationsRecorder.put(ConfigMappingMethods.TLS, true); + return new Tls() { + @Override + public Optional tlsConfigurationName() { + invocationsRecorder.put(ConfigMappingMethods.TLS_CONFIGURATION, true); + return Optional.empty(); + } + + @Override + public Optional verification() { + invocationsRecorder.put(ConfigMappingMethods.TLS_VERIFICATION, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_FILE_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreProvider() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_PROVIDER, true); + return Optional.empty(); + } + + @Override + public Optional keyStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreKeyAlias() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_KEY_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreKeyPassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_KEY_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional trustStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreCertAlias() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_CERT_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_FILE_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreProvider() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_PROVIDER, true); + return Optional.empty(); + } + }; + } +} diff --git a/extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigTest.java b/extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigTest.java new file mode 100644 index 0000000000000..d23dfcd678ae7 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/test/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfigTest.java @@ -0,0 +1,23 @@ +package io.quarkus.oidc.client.registration; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +public class OidcClientRegistrationConfigTest { + + @Test + public void testCopyBetweenConfigMappingAndClass() { + var configMappingGroup = new OidcClientRegistrationConfigImpl(); + new OidcClientRegistrationConfig(configMappingGroup); + for (var configMappingMethod : EnumSet.allOf(OidcClientRegistrationConfigImpl.ConfigMappingMethods.class)) { + Boolean invoked = configMappingGroup.invocationsRecorder.get(configMappingMethod); + assertTrue(invoked != null && invoked, + "OidcClientRegistrationConfig method '%s' return value is not copied from interface to class" + .formatted(configMappingMethod)); + } + } + +} diff --git a/extensions/oidc-client/deployment/pom.xml b/extensions/oidc-client/deployment/pom.xml index 3e96bb92c13c7..4a9351f66e255 100644 --- a/extensions/oidc-client/deployment/pom.xml +++ b/extensions/oidc-client/deployment/pom.xml @@ -119,9 +119,6 @@ ${project.version} - - -AlegacyConfigRoot=true - diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java index fe6e9fae6f74f..5e2bb4d70c012 100644 --- a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java @@ -242,7 +242,7 @@ public static class IsEnabled implements BooleanSupplier { OidcClientBuildTimeConfig config; public boolean getAsBoolean() { - return config.enabled; + return config.enabled(); } } } diff --git a/extensions/oidc-client/runtime/pom.xml b/extensions/oidc-client/runtime/pom.xml index cdace1db1d7a4..0091875c7551e 100644 --- a/extensions/oidc-client/runtime/pom.xml +++ b/extensions/oidc-client/runtime/pom.xml @@ -60,9 +60,6 @@ ${project.version} - - -AlegacyConfigRoot=true - diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index 03acfd1626ede..0d15cb4e9f0c1 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -7,30 +7,41 @@ import io.quarkus.oidc.common.runtime.OidcClientCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; -import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; -@ConfigGroup public class OidcClientConfig extends OidcClientCommonConfig { + public OidcClientConfig() { + + } + + public OidcClientConfig(io.quarkus.oidc.client.runtime.OidcClientConfig mapping) { + super(mapping); + id = mapping.id(); + clientEnabled = mapping.clientEnabled(); + scopes = mapping.scopes(); + refreshTokenTimeSkew = mapping.refreshTokenTimeSkew(); + accessTokenExpiresIn = mapping.accessTokenExpiresIn(); + absoluteExpiresIn = mapping.absoluteExpiresIn(); + grant.addConfigMappingValues(mapping.grant()); + grantOptions = mapping.grantOptions(); + earlyTokensAcquisition = mapping.earlyTokensAcquisition(); + headers = mapping.headers(); + } + /** * A unique OIDC client identifier. It must be set when OIDC clients are created dynamically * and is optional in all other cases. */ - @ConfigItem public Optional id = Optional.empty(); /** * If this client configuration is enabled. */ - @ConfigItem(defaultValue = "true") public boolean clientEnabled = true; /** * List of access token scopes */ - @ConfigItem public Optional> scopes = Optional.empty(); /** @@ -39,7 +50,6 @@ public class OidcClientConfig extends OidcClientCommonConfig { * when checking whether the access token should be refreshed. If the sum is greater than this access token's * expiration time then a refresh is going to happen. */ - @ConfigItem public Optional refreshTokenTimeSkew = Optional.empty(); /** @@ -47,20 +57,18 @@ public class OidcClientConfig extends OidcClientCommonConfig { * This property is only checked when an access token grant response * does not include an access token expiration property. */ - @ConfigItem public Optional accessTokenExpiresIn = Optional.empty(); /** * If the access token 'expires_in' property should be checked as an absolute time value * as opposed to a duration relative to the current time. */ - @ConfigItem(defaultValue = "false") public boolean absoluteExpiresIn; public Grant grant = new Grant(); - @ConfigGroup public static class Grant { + public static enum Type { /** * 'client_credentials' grant requiring an OIDC client authentication only @@ -121,31 +129,26 @@ public String getGrantType() { /** * Grant type */ - @ConfigItem(defaultValue = "client") public Type type = Type.CLIENT; /** * Access token property name in a token grant response */ - @ConfigItem(defaultValue = OidcConstants.ACCESS_TOKEN_VALUE) public String accessTokenProperty = OidcConstants.ACCESS_TOKEN_VALUE; /** * Refresh token property name in a token grant response */ - @ConfigItem(defaultValue = OidcConstants.REFRESH_TOKEN_VALUE) public String refreshTokenProperty = OidcConstants.REFRESH_TOKEN_VALUE; /** * Access token expiry property name in a token grant response */ - @ConfigItem(defaultValue = OidcConstants.EXPIRES_IN) public String expiresInProperty = OidcConstants.EXPIRES_IN; /** * Refresh token expiry property name in a token grant response */ - @ConfigItem(defaultValue = OidcConstants.REFRESH_EXPIRES_IN) public String refreshExpiresInProperty = OidcConstants.REFRESH_EXPIRES_IN; public Type getType() { @@ -187,13 +190,19 @@ public String getRefreshExpiresInProperty() { public void setRefreshExpiresInProperty(String refreshExpiresInProperty) { this.refreshExpiresInProperty = refreshExpiresInProperty; } + + private void addConfigMappingValues(io.quarkus.oidc.client.runtime.OidcClientConfig.Grant grant) { + this.type = Grant.Type.valueOf(grant.type().toString()); + this.accessTokenProperty = grant.accessTokenProperty(); + this.refreshTokenProperty = grant.refreshTokenProperty(); + this.expiresInProperty = grant.expiresInProperty(); + this.refreshExpiresInProperty = grant.refreshExpiresInProperty(); + } } /** * Grant options */ - @ConfigItem - @ConfigDocMapKey("grant-name") public Map> grantOptions; /** @@ -202,13 +211,11 @@ public void setRefreshExpiresInProperty(String refreshExpiresInProperty) { * This property should be disabled if the access token may expire before it is used for the first time and no refresh token * is available. */ - @ConfigItem(defaultValue = "true") public boolean earlyTokensAcquisition = true; /** * Custom HTTP headers which have to be sent to the token endpoint */ - @ConfigItem public Map headers; public Optional getId() { @@ -274,4 +281,5 @@ public void setGrant(Grant grant) { public Grant getGrant() { return grant; } + } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java index 0c7f8d948c57d..7cc57cc16ee17 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java @@ -43,10 +43,10 @@ public void init() { if (clientId.isPresent()) { // static named OidcClient oidcClient = Objects.requireNonNull(oidcClients.getClient(clientId.get()), "Unknown client"); - earlyTokenAcquisition = oidcClientsConfig.namedClients.get(clientId.get()).earlyTokensAcquisition; + earlyTokenAcquisition = oidcClientsConfig.namedClients().get(clientId.get()).earlyTokensAcquisition(); } else { // default OidcClient - earlyTokenAcquisition = oidcClientsConfig.defaultClient.earlyTokensAcquisition; + earlyTokenAcquisition = oidcClientsConfig.defaultClient().earlyTokensAcquisition(); oidcClient = oidcClients.getClient(); } } else { @@ -57,7 +57,7 @@ public void init() { } protected boolean isClientFeatureDisabled() { - return !oidcClientBuildTimeConfig.enabled; + return !oidcClientBuildTimeConfig.enabled(); } protected void initTokens() { diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientBuildTimeConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientBuildTimeConfig.java index a8dc58b35fdfb..0c667f0e6408b 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientBuildTimeConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientBuildTimeConfig.java @@ -1,17 +1,19 @@ package io.quarkus.oidc.client.runtime; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; /** * Build time configuration for OIDC client. */ -@ConfigRoot(name = "oidc-client", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) -public class OidcClientBuildTimeConfig { +@ConfigMapping(prefix = "quarkus.oidc-client") +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public interface OidcClientBuildTimeConfig { /** * If the OIDC client extension is enabled. */ - @ConfigItem(defaultValue = "true") - public boolean enabled; + @WithDefault("true") + boolean enabled(); } diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java new file mode 100644 index 0000000000000..06fd501e92e57 --- /dev/null +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientConfig.java @@ -0,0 +1,168 @@ +package io.quarkus.oidc.client.runtime; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.smallrye.config.WithDefault; + +public interface OidcClientConfig extends OidcClientCommonConfig { + + /** + * A unique OIDC client identifier. It must be set when OIDC clients are created dynamically + * and is optional in all other cases. + */ + Optional id(); + + /** + * If this client configuration is enabled. + */ + @WithDefault("true") + boolean clientEnabled(); + + /** + * List of access token scopes + */ + Optional> scopes(); + + /** + * Refresh token time skew. + * If this property is enabled then the configured duration is converted to seconds and is added to the current time + * when checking whether the access token should be refreshed. If the sum is greater than this access token's + * expiration time then a refresh is going to happen. + */ + Optional refreshTokenTimeSkew(); + + /** + * Access token expiration period relative to the current time. + * This property is only checked when an access token grant response + * does not include an access token expiration property. + */ + Optional accessTokenExpiresIn(); + + /** + * If the access token 'expires_in' property should be checked as an absolute time value + * as opposed to a duration relative to the current time. + */ + @WithDefault("false") + boolean absoluteExpiresIn(); + + /** + * OIDC Client grant config group. + */ + Grant grant(); + + interface Grant { + enum Type { + /** + * 'client_credentials' grant requiring an OIDC client authentication only + */ + CLIENT("client_credentials"), + /** + * 'password' grant requiring both OIDC client and user ('username' and 'password') authentications + */ + PASSWORD("password"), + /** + * 'authorization_code' grant requiring an OIDC client authentication as well as + * at least 'code' and 'redirect_uri' parameters which must be passed to OidcClient at the token request time. + */ + CODE("authorization_code"), + /** + * 'urn:ietf:params:oauth:grant-type:token-exchange' grant requiring an OIDC client authentication as well as + * at least 'subject_token' parameter which must be passed to OidcClient at the token request time. + */ + EXCHANGE("urn:ietf:params:oauth:grant-type:token-exchange"), + /** + * 'urn:ietf:params:oauth:grant-type:jwt-bearer' grant requiring an OIDC client authentication as well as + * at least an 'assertion' parameter which must be passed to OidcClient at the token request time. + */ + JWT("urn:ietf:params:oauth:grant-type:jwt-bearer"), + /** + * 'refresh_token' grant requiring an OIDC client authentication and a refresh token. + * Note, OidcClient supports this grant by default if an access token acquisition response contained a refresh + * token. + * However, in some cases, the refresh token is provided out of band, for example, it can be shared between + * several of the confidential client's services, etc. + * If 'quarkus.oidc-client.grant-type' is set to 'refresh' then `OidcClient` will only support refreshing the + * tokens. + */ + REFRESH("refresh_token"), + /** + * 'urn:openid:params:grant-type:ciba' grant requiring an OIDC client authentication as well as 'auth_req_id' + * parameter which must be passed to OidcClient at the token request time. + */ + CIBA("urn:openid:params:grant-type:ciba"), + /** + * 'urn:ietf:params:oauth:grant-type:device_code' grant requiring an OIDC client authentication as well as + * 'device_code' + * parameter which must be passed to OidcClient at the token request time. + */ + DEVICE("urn:ietf:params:oauth:grant-type:device_code"); + + private final String grantType; + + Type(String grantType) { + this.grantType = grantType; + } + + public String getGrantType() { + return grantType; + } + } + + /** + * Grant type + */ + @WithDefault("client") + Type type(); + + /** + * Access token property name in a token grant response + */ + @WithDefault(OidcConstants.ACCESS_TOKEN_VALUE) + String accessTokenProperty(); + + /** + * Refresh token property name in a token grant response + */ + @WithDefault(OidcConstants.REFRESH_TOKEN_VALUE) + String refreshTokenProperty(); + + /** + * Access token expiry property name in a token grant response + */ + @WithDefault(OidcConstants.EXPIRES_IN) + String expiresInProperty(); + + /** + * Refresh token expiry property name in a token grant response + */ + @WithDefault(OidcConstants.REFRESH_EXPIRES_IN) + String refreshExpiresInProperty(); + } + + /** + * Grant options + */ + @ConfigDocMapKey("grant-name") + Map> grantOptions(); + + /** + * Requires that all filters which use 'OidcClient' acquire the tokens at the post-construct initialization time, + * possibly long before these tokens are used. + * This property should be disabled if the access token may expire before it is used for the first time and no refresh token + * is available. + */ + @WithDefault("true") + boolean earlyTokensAcquisition(); + + /** + * Custom HTTP headers which have to be sent to the token endpoint + */ + Map headers(); + +} diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java index 7d3f85cd23065..604c7b159622b 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientRecorder.java @@ -48,15 +48,17 @@ private static OidcClients setup(OidcClientsConfig oidcClientsConfig, Supplier registrySupplier) { var tlsSupport = OidcTlsSupport.of(registrySupplier); - String defaultClientId = oidcClientsConfig.defaultClient.getId().orElse(DEFAULT_OIDC_CLIENT_ID); - OidcClient defaultClient = createOidcClient(oidcClientsConfig.defaultClient, defaultClientId, vertx, tlsSupport); + var defaultClientConfig = new OidcClientConfig(oidcClientsConfig.defaultClient()); + String defaultClientId = defaultClientConfig.getId().orElse(DEFAULT_OIDC_CLIENT_ID); + OidcClient defaultClient = createOidcClient(defaultClientConfig, defaultClientId, vertx, tlsSupport); Map staticOidcClients = new HashMap<>(); - for (Map.Entry config : oidcClientsConfig.namedClients.entrySet()) { - OidcCommonUtils.verifyConfigurationId(defaultClientId, config.getKey(), config.getValue().getId()); + for (var config : oidcClientsConfig.namedClients().entrySet()) { + var namedOidcClientConfig = new OidcClientConfig(config.getValue()); + OidcCommonUtils.verifyConfigurationId(defaultClientId, config.getKey(), namedOidcClientConfig.getId()); staticOidcClients.put(config.getKey(), - createOidcClient(config.getValue(), config.getKey(), vertx, tlsSupport)); + createOidcClient(namedOidcClientConfig, config.getKey(), vertx, tlsSupport)); } return new OidcClientsImpl(defaultClient, staticOidcClients, diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientsConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientsConfig.java index abf4db230b5ab..4c76cb46e88d0 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientsConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientsConfig.java @@ -2,27 +2,28 @@ import java.util.Map; -import io.quarkus.oidc.client.OidcClientConfig; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigDocSection; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithParentName; -@ConfigRoot(name = "oidc-client", phase = ConfigPhase.RUN_TIME) -public class OidcClientsConfig { +@ConfigMapping(prefix = "quarkus.oidc-client") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface OidcClientsConfig { /** * The default client. */ - @ConfigItem(name = ConfigItem.PARENT) - public OidcClientConfig defaultClient; + @WithParentName + OidcClientConfig defaultClient(); /** * Additional named clients. */ @ConfigDocSection @ConfigDocMapKey("id") - @ConfigItem(name = ConfigItem.PARENT) - public Map namedClients; + @WithParentName + Map namedClients(); } diff --git a/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java new file mode 100644 index 0000000000000..b786c83e4237a --- /dev/null +++ b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java @@ -0,0 +1,561 @@ +package io.quarkus.oidc.client; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.oidc.client.runtime.OidcClientConfig; + +final class OidcClientConfigImpl implements OidcClientConfig { + + enum ConfigMappingMethods { + ID, + AUTH_SERVER_URL, + DISCOVERY_ENABLED, + REGISTRATION_PATH, + CONNECTION_DELAY, + CONNECTION_RETRY_COUNT, + CONNECTION_TIMEOUT, + USE_BLOCKING_DNS_LOOKUP, + MAX_POOL_SIZE, + FOLLOW_REDIRECTS, + PROXY, + PROXY_HOST, + PROXY_PORT, + PROXY_USERNAME, + PROXY_PASSWORD, + TLS, + TLS_CONFIGURATION, + TLS_VERIFICATION, + TLS_KEYSTORE_FILE, + TLS_KEYSTORE_FILE_TYPE, + TLS_KEYSTORE_PROVIDER, + TLS_KEYSTORE_PASSWORD, + TLS_KEYSTORE_KEY_ALIAS, + TLS_KEYSTORE_KEY_PASSWORD, + TLS_TRUSTSTORE_PASSWORD, + TLS_TRUSTSTORE_FILE, + TLS_TRUSTSTORE_CERT_ALIAS, + TLS_TRUSTSTORE_FILE_TYPE, + TLS_TRUSTSTORE_PROVIDER, + HEADERS, + EARLY_TOKENS_ACQUISITION, + GRANT_OPTIONS, + CLIENT_ENABLED, + SCOPES, + REFRESH_TOKEN_TIME_SKEW, + ACCESS_TOKEN_EXPIRES_IN, + ABSOLUTE_EXPIRES_IN, + GRANT, + GRANT_TYPE, + GRANT_ACCESS_TOKEN_PROPERTY, + GRANT_REFRESH_TOKEN_PROPERTY, + GRANT_EXPIRES_IN_PROPERTY, + REFRESH_EXPIRES_IN_PROPERTY, + TOKEN_PATH, + REVOKE_PATH, + CLIENT_ID, + CLIENT_NAME, + CREDENTIALS, + CREDENTIALS_SECRET, + CREDENTIALS_CLIENT_SECRET, + CREDENTIALS_CLIENT_SECRET_VALUE, + CREDENTIALS_CLIENT_SECRET_PROVIDER, + CREDENTIALS_CLIENT_SECRET_METHOD, + CREDENTIALS_CLIENT_SECRET_PROVIDER_NAME, + CREDENTIALS_CLIENT_SECRET_PROVIDER_KEYRING_NAME, + CREDENTIALS_CLIENT_SECRET_PROVIDER_KEY, + CREDENTIALS_JWT, + CREDENTIALS_JWT_SOURCE, + CREDENTIALS_JWT_SECRET, + CREDENTIALS_JWT_SECRET_PROVIDER, + CREDENTIALS_JWT_SECRET_PROVIDER_NAME, + CREDENTIALS_JWT_SECRET_PROVIDER_KEYRING_NAME, + CREDENTIALS_JWT_SECRET_PROVIDER_KEY, + CREDENTIALS_JWT_KEY, + CREDENTIALS_JWT_KEY_FILE, + CREDENTIALS_JWT_KEY_STORE_FILE, + CREDENTIALS_JWT_KEY_STORE_PASSWORD, + CREDENTIALS_JWT_KEY_ID, + CREDENTIALS_JWT_KEY_PASSWORD, + CREDENTIALS_JWT_ISSUER, + CREDENTIALS_JWT_SUBJECT, + CREDENTIALS_JWT_CLAIMS, + CREDENTIALS_JWT_SIGNATURE_ALGORITHM, + CREDENTIALS_JWT_LIFESPAN, + CREDENTIALS_JWT_ASSERTION, + CREDENTIALS_JWT_AUDIENCE, + CREDENTIALS_JWT_TOKEN_ID + } + + final Map invocationsRecorder = new EnumMap<>(ConfigMappingMethods.class); + + @Override + public Optional tokenPath() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_PATH, true); + return Optional.empty(); + } + + @Override + public Optional revokePath() { + invocationsRecorder.put(ConfigMappingMethods.REVOKE_PATH, true); + return Optional.empty(); + } + + @Override + public Optional clientId() { + invocationsRecorder.put(ConfigMappingMethods.CLIENT_ID, true); + return Optional.empty(); + } + + @Override + public Optional clientName() { + invocationsRecorder.put(ConfigMappingMethods.CLIENT_NAME, true); + return Optional.empty(); + } + + @Override + public Credentials credentials() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS, true); + return new Credentials() { + @Override + public Optional secret() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_SECRET, true); + return Optional.empty(); + } + + @Override + public Secret clientSecret() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET, true); + return new Secret() { + + @Override + public Optional value() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_VALUE, true); + return Optional.empty(); + } + + @Override + public Provider provider() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER, true); + return new Provider() { + @Override + public Optional name() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER_NAME, true); + return Optional.empty(); + } + + @Override + public Optional keyringName() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER_KEYRING_NAME, + true); + return Optional.empty(); + } + + @Override + public Optional key() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER_KEY, true); + return Optional.empty(); + } + }; + } + + @Override + public Optional method() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_METHOD, true); + return Optional.empty(); + } + }; + } + + @Override + public Jwt jwt() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT, true); + return new Jwt() { + @Override + public Source source() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SOURCE, true); + return Source.BEARER; + } + + @Override + public Optional secret() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET, true); + return Optional.empty(); + } + + @Override + public Provider secretProvider() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER, true); + return new Provider() { + @Override + public Optional name() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER_NAME, true); + return Optional.empty(); + } + + @Override + public Optional keyringName() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER_KEYRING_NAME, + true); + return Optional.empty(); + } + + @Override + public Optional key() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER_KEY, true); + return Optional.empty(); + } + }; + } + + @Override + public Optional key() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY, true); + return Optional.empty(); + } + + @Override + public Optional keyFile() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_STORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_STORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional keyId() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_ID, true); + return Optional.empty(); + } + + @Override + public Optional keyPassword() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional audience() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_AUDIENCE, true); + return Optional.empty(); + } + + @Override + public Optional tokenKeyId() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_TOKEN_ID, true); + return Optional.empty(); + } + + @Override + public Optional issuer() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_ISSUER, true); + return Optional.empty(); + } + + @Override + public Optional subject() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SUBJECT, true); + return Optional.empty(); + } + + @Override + public Map claims() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_CLAIMS, true); + return Map.of(); + } + + @Override + public Optional signatureAlgorithm() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SIGNATURE_ALGORITHM, true); + return Optional.empty(); + } + + @Override + public int lifespan() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_LIFESPAN, true); + return 0; + } + + @Override + public boolean assertion() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_ASSERTION, true); + return false; + } + }; + } + }; + } + + @Override + public Optional id() { + invocationsRecorder.put(ConfigMappingMethods.ID, true); + return Optional.empty(); + } + + @Override + public boolean clientEnabled() { + invocationsRecorder.put(ConfigMappingMethods.CLIENT_ENABLED, true); + return false; + } + + @Override + public Optional> scopes() { + invocationsRecorder.put(ConfigMappingMethods.SCOPES, true); + return Optional.empty(); + } + + @Override + public Optional refreshTokenTimeSkew() { + invocationsRecorder.put(ConfigMappingMethods.REFRESH_TOKEN_TIME_SKEW, true); + return Optional.empty(); + } + + @Override + public Optional accessTokenExpiresIn() { + invocationsRecorder.put(ConfigMappingMethods.ACCESS_TOKEN_EXPIRES_IN, true); + return Optional.empty(); + } + + @Override + public boolean absoluteExpiresIn() { + invocationsRecorder.put(ConfigMappingMethods.ABSOLUTE_EXPIRES_IN, true); + return false; + } + + @Override + public Grant grant() { + invocationsRecorder.put(ConfigMappingMethods.GRANT, true); + return new Grant() { + @Override + public Type type() { + invocationsRecorder.put(ConfigMappingMethods.GRANT_TYPE, true); + return Type.CLIENT; + } + + @Override + public String accessTokenProperty() { + invocationsRecorder.put(ConfigMappingMethods.GRANT_ACCESS_TOKEN_PROPERTY, true); + return ""; + } + + @Override + public String refreshTokenProperty() { + invocationsRecorder.put(ConfigMappingMethods.GRANT_REFRESH_TOKEN_PROPERTY, true); + return ""; + } + + @Override + public String expiresInProperty() { + invocationsRecorder.put(ConfigMappingMethods.GRANT_EXPIRES_IN_PROPERTY, true); + return ""; + } + + @Override + public String refreshExpiresInProperty() { + invocationsRecorder.put(ConfigMappingMethods.REFRESH_EXPIRES_IN_PROPERTY, true); + return ""; + } + }; + } + + @Override + public Map> grantOptions() { + invocationsRecorder.put(ConfigMappingMethods.GRANT_OPTIONS, true); + return Map.of(); + } + + @Override + public boolean earlyTokensAcquisition() { + invocationsRecorder.put(ConfigMappingMethods.EARLY_TOKENS_ACQUISITION, true); + return false; + } + + @Override + public Map headers() { + invocationsRecorder.put(ConfigMappingMethods.HEADERS, true); + return Map.of(); + } + + @Override + public Optional authServerUrl() { + invocationsRecorder.put(ConfigMappingMethods.AUTH_SERVER_URL, true); + return Optional.empty(); + } + + @Override + public Optional discoveryEnabled() { + invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_ENABLED, true); + return Optional.empty(); + } + + @Override + public Optional registrationPath() { + invocationsRecorder.put(ConfigMappingMethods.REGISTRATION_PATH, true); + return Optional.empty(); + } + + @Override + public Optional connectionDelay() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_DELAY, true); + return Optional.empty(); + } + + @Override + public int connectionRetryCount() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_RETRY_COUNT, true); + return 0; + } + + @Override + public Duration connectionTimeout() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_TIMEOUT, true); + return null; + } + + @Override + public boolean useBlockingDnsLookup() { + invocationsRecorder.put(ConfigMappingMethods.USE_BLOCKING_DNS_LOOKUP, true); + return false; + } + + @Override + public OptionalInt maxPoolSize() { + invocationsRecorder.put(ConfigMappingMethods.MAX_POOL_SIZE, true); + return OptionalInt.empty(); + } + + @Override + public boolean followRedirects() { + invocationsRecorder.put(ConfigMappingMethods.FOLLOW_REDIRECTS, true); + return false; + } + + @Override + public Proxy proxy() { + invocationsRecorder.put(ConfigMappingMethods.PROXY, true); + return new Proxy() { + @Override + public Optional host() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_HOST, true); + return Optional.empty(); + } + + @Override + public int port() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_PORT, true); + return 0; + } + + @Override + public Optional username() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_USERNAME, true); + return Optional.empty(); + } + + @Override + public Optional password() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_PASSWORD, true); + return Optional.empty(); + } + }; + } + + @Override + public Tls tls() { + invocationsRecorder.put(ConfigMappingMethods.TLS, true); + return new Tls() { + @Override + public Optional tlsConfigurationName() { + invocationsRecorder.put(ConfigMappingMethods.TLS_CONFIGURATION, true); + return Optional.empty(); + } + + @Override + public Optional verification() { + invocationsRecorder.put(ConfigMappingMethods.TLS_VERIFICATION, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_FILE_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreProvider() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_PROVIDER, true); + return Optional.empty(); + } + + @Override + public Optional keyStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreKeyAlias() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_KEY_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreKeyPassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_KEY_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional trustStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreCertAlias() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_CERT_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_FILE_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreProvider() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_PROVIDER, true); + return Optional.empty(); + } + }; + } +} diff --git a/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigTest.java b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigTest.java new file mode 100644 index 0000000000000..9850825e21704 --- /dev/null +++ b/extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigTest.java @@ -0,0 +1,23 @@ +package io.quarkus.oidc.client; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +public class OidcClientConfigTest { + + @Test + public void testCopyBetweenConfigMappingAndClass() { + var configMappingGroup = new OidcClientConfigImpl(); + new OidcClientConfig(configMappingGroup); + for (var configMappingMethod : EnumSet.allOf(OidcClientConfigImpl.ConfigMappingMethods.class)) { + Boolean invoked = configMappingGroup.invocationsRecorder.get(configMappingMethod); + assertTrue(invoked != null && invoked, + "OidcClientConfig method '%s' return value is not copied from interface to class" + .formatted(configMappingMethod)); + } + } + +} diff --git a/extensions/oidc-common/runtime/pom.xml b/extensions/oidc-common/runtime/pom.xml index 4a8e750382909..ace03eb72f809 100644 --- a/extensions/oidc-common/runtime/pom.xml +++ b/extensions/oidc-common/runtime/pom.xml @@ -71,9 +71,6 @@ ${project.version} - - -AlegacyConfigRoot=true - diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java index 095719e40d8f3..619982e2bf395 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java @@ -4,31 +4,37 @@ import java.util.Map; import java.util.Optional; -import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup public class OidcClientCommonConfig extends OidcCommonConfig { + + public OidcClientCommonConfig() { + + } + + protected OidcClientCommonConfig(io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig mapping) { + super(mapping); + this.tokenPath = mapping.tokenPath(); + this.revokePath = mapping.revokePath(); + this.clientId = mapping.clientId(); + this.clientName = mapping.clientName(); + this.credentials.addConfigMappingValues(mapping.credentials()); + } + /** * The OIDC token endpoint that issues access and refresh tokens; * specified as a relative path or absolute URL. * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. */ - @ConfigItem public Optional tokenPath = Optional.empty(); /** * The relative path or absolute URL of the OIDC token revocation endpoint. */ - @ConfigItem public Optional revokePath = Optional.empty(); /** * The client id of the application. Each application has a client id that is used to identify the application. * Setting the client id is not required if {@link #applicationType} is `service` and no token introspection is required. */ - @ConfigItem public Optional clientId = Optional.empty(); /** @@ -37,16 +43,13 @@ public class OidcClientCommonConfig extends OidcCommonConfig { * For example, you can set this property to have more informative log messages which record an activity of the given * client. */ - @ConfigItem public Optional clientName = Optional.empty(); /** * Credentials the OIDC adapter uses to authenticate to the OIDC server. */ - @ConfigItem public Credentials credentials = new Credentials(); - @ConfigGroup public static class Credentials { /** @@ -54,7 +57,6 @@ public static class Credentials { * Must be set unless a secret is set in {@link #clientSecret} or {@link #jwt} client authentication is required. * You can use `client-secret.value` instead, but both properties are mutually exclusive. */ - @ConfigItem public Optional secret = Optional.empty(); /** @@ -63,13 +65,11 @@ public static class Credentials { * Note that a `secret.value` property can be used instead to support the `client_secret_basic` method * but both properties are mutually exclusive. */ - @ConfigItem public Secret clientSecret = new Secret(); /** * Client JSON Web Token (JWT) authentication methods */ - @ConfigItem public Jwt jwt = new Jwt(); public Optional getSecret() { @@ -96,13 +96,18 @@ public void setJwt(Jwt jwt) { this.jwt = jwt; } + private void addConfigMappingValues(io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig.Credentials mapping) { + secret = mapping.secret(); + clientSecret.addConfigMappingValues(mapping.clientSecret()); + jwt.addConfigMappingValues(mapping.jwt()); + } + /** * Supports the client authentication methods that involve sending a client secret. * * @see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication */ - @ConfigGroup public static class Secret { public static enum Method { @@ -136,20 +141,17 @@ public static enum Method { * The client secret value. This value is ignored if `credentials.secret` is set. * Must be set unless a secret is set in {@link #clientSecret} or {@link #jwt} client authentication is required. */ - @ConfigItem public Optional value = Optional.empty(); /** * The Secret CredentialsProvider. */ - @ConfigItem public Provider provider = new Provider(); /** * The authentication method. * If the `clientSecret.value` secret is set, this method is `basic` by default. */ - @ConfigItem public Optional method = Optional.empty(); public Optional getValue() { @@ -175,6 +177,13 @@ public Provider getSecretProvider() { public void setSecretProvider(Provider secretProvider) { this.provider = secretProvider; } + + private void addConfigMappingValues( + io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig.Credentials.Secret mapping) { + this.value = mapping.value(); + this.provider.addConfigMappingValues(mapping.provider()); + this.method = mapping.method().map(Enum::toString).map(Method::valueOf); + } } /** @@ -185,7 +194,6 @@ public void setSecretProvider(Provider secretProvider) { * @see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication */ - @ConfigGroup public static class Jwt { public static enum Source { @@ -200,20 +208,17 @@ public static enum Source { /** * JWT token source: OIDC provider client or an existing JWT bearer token. */ - @ConfigItem(defaultValue = "client") public Source source = Source.CLIENT; /** * If provided, indicates that JWT is signed using a secret key. * It is mutually exclusive with {@link #key}, {@link #keyFile} and {@link #keyStore} properties. */ - @ConfigItem public Optional secret = Optional.empty(); /** * If provided, indicates that JWT is signed using a secret key provided by Secret CredentialsProvider. */ - @ConfigItem public Provider secretProvider = new Provider(); /** @@ -222,7 +227,6 @@ public static enum Source { * It is mutually exclusive with {@link #secret}, {@link #keyFile} and {@link #keyStore} properties. * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. */ - @ConfigItem public Optional key = Optional.empty(); /** @@ -230,64 +234,53 @@ public static enum Source { * It is mutually exclusive with {@link #secret}, {@link #key} and {@link #keyStore} properties. * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. */ - @ConfigItem public Optional keyFile = Optional.empty(); /** * If provided, indicates that JWT is signed using a private key from a keystore. * It is mutually exclusive with {@link #secret}, {@link #key} and {@link #keyFile} properties. */ - @ConfigItem public Optional keyStoreFile = Optional.empty(); /** * A parameter to specify the password of the keystore file. */ - @ConfigItem public Optional keyStorePassword; /** * The private key id or alias. */ - @ConfigItem public Optional keyId = Optional.empty(); /** * The private key password. */ - @ConfigItem public Optional keyPassword; /** * The JWT audience (`aud`) claim value. * By default, the audience is set to the address of the OpenId Connect Provider's token endpoint. */ - @ConfigItem public Optional audience = Optional.empty(); /** * The key identifier of the signing key added as a JWT `kid` header. */ - @ConfigItem public Optional tokenKeyId = Optional.empty(); /** * The issuer of the signing key added as a JWT `iss` claim. The default value is the client id. */ - @ConfigItem public Optional issuer = Optional.empty(); /** * Subject of the signing key added as a JWT `sub` claim The default value is the client id. */ - @ConfigItem public Optional subject = Optional.empty(); /** * Additional claims. */ - @ConfigItem - @ConfigDocMapKey("claim-name") public Map claims = new HashMap<>(); /** @@ -295,14 +288,12 @@ public static enum Source { * Supported values: `RS256` (default), `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512`, * `HS256`, `HS384`, `HS512`. */ - @ConfigItem public Optional signatureAlgorithm = Optional.empty(); /** * The JWT lifespan in seconds. This value is added to the time at which the JWT was issued to calculate the * expiration time. */ - @ConfigItem(defaultValue = "10") public int lifespan = 10; /** @@ -311,7 +302,6 @@ public static enum Source { * and 'client_assertion_type' form properties, only 'assertion' is produced. * This option is only supported by the OIDC client extension. */ - @ConfigItem(defaultValue = "false") public boolean assertion = false; public Optional getSecret() { @@ -402,19 +392,37 @@ public void setAssertion(boolean assertion) { this.assertion = assertion; } + private void addConfigMappingValues( + io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig.Credentials.Jwt mapping) { + source = Source.valueOf(mapping.source().toString()); + secret = mapping.secret(); + secretProvider.addConfigMappingValues(mapping.secretProvider()); + key = mapping.key(); + keyFile = mapping.keyFile(); + keyStoreFile = mapping.keyStoreFile(); + keyStorePassword = mapping.keyStorePassword(); + keyId = mapping.keyId(); + keyPassword = mapping.keyPassword(); + audience = mapping.audience(); + tokenKeyId = mapping.tokenKeyId(); + issuer = mapping.issuer(); + subject = mapping.subject(); + claims = mapping.claims(); + signatureAlgorithm = mapping.signatureAlgorithm(); + lifespan = mapping.lifespan(); + assertion = mapping.assertion(); + } } /** * CredentialsProvider, which provides a client secret. */ - @ConfigGroup public static class Provider { /** * The CredentialsProvider bean name, which should only be set if more than one CredentialsProvider is * registered */ - @ConfigItem public Optional name = Optional.empty(); /** @@ -424,13 +432,11 @@ public static class Provider { * shared by multiple extensions to retrieve credentials from a more dynamic source like a vault instance or secret * manager */ - @ConfigItem public Optional keyringName = Optional.empty(); /** * The CredentialsProvider client secret key */ - @ConfigItem public Optional key = Optional.empty(); public Optional getName() { @@ -456,6 +462,13 @@ public Optional getKey() { public void setKey(String key) { this.key = Optional.of(key); } + + private void addConfigMappingValues( + io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig.Credentials.Provider mapping) { + name = mapping.name(); + keyringName = mapping.keyringName(); + key = mapping.key(); + } } } @@ -498,4 +511,5 @@ public Credentials getCredentials() { public void setCredentials(Credentials credentials) { this.credentials = credentials; } + } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index 9fb2108b6af4e..7662b906611df 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java @@ -5,11 +5,26 @@ import java.util.Optional; import java.util.OptionalInt; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; - -@ConfigGroup public class OidcCommonConfig { + + public OidcCommonConfig() { + + } + + protected OidcCommonConfig(io.quarkus.oidc.common.runtime.config.OidcCommonConfig mapping) { + this.authServerUrl = mapping.authServerUrl(); + this.discoveryEnabled = mapping.discoveryEnabled(); + this.registrationPath = mapping.registrationPath(); + this.connectionDelay = mapping.connectionDelay(); + this.connectionRetryCount = mapping.connectionRetryCount(); + this.connectionTimeout = mapping.connectionTimeout(); + this.useBlockingDnsLookup = mapping.useBlockingDnsLookup(); + this.maxPoolSize = mapping.maxPoolSize(); + this.followRedirects = mapping.followRedirects(); + this.proxy.addConfigMappingValues(mapping.proxy()); + this.tls.addConfigMappingValues(mapping.tls()); + } + /** * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. * Do not set this property if you use 'quarkus-oidc' and the public key verification ({@link #publicKey}) @@ -17,21 +32,18 @@ public class OidcCommonConfig { * The OIDC discovery endpoint is called by default by appending a `.well-known/openid-configuration` path to this URL. * For Keycloak, use `https://host:port/realms/{realm}`, replacing `{realm}` with the Keycloak realm name. */ - @ConfigItem public Optional authServerUrl = Optional.empty(); /** * Discovery of the OIDC endpoints. * If not enabled, you must configure the OIDC endpoint URLs individually. */ - @ConfigItem(defaultValueDocumentation = "true") public Optional discoveryEnabled = Optional.empty(); /** * The relative path or absolute URL of the OIDC dynamic client registration endpoint. * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. */ - @ConfigItem public Optional registrationPath = Optional.empty(); /** @@ -40,7 +52,6 @@ public class OidcCommonConfig { * This property is only effective when the initial OIDC connection is created. * For dropped connections, use the `connection-retry-count` property instead. */ - @ConfigItem public Optional connectionDelay = Optional.empty(); /** @@ -49,26 +60,22 @@ public class OidcCommonConfig { * For instance, if a request to the OIDC token endpoint fails due to a connection issue, it will be retried as per this * setting. */ - @ConfigItem(defaultValue = "3") public int connectionRetryCount = 3; /** * The number of seconds after which the current OIDC connection request times out. */ - @ConfigItem(defaultValue = "10s") public Duration connectionTimeout = Duration.ofSeconds(10); /** * Whether DNS lookup should be performed on the worker thread. * Use this option when you can see logged warnings about blocked Vert.x event loop by HTTP requests to OIDC server. */ - @ConfigItem(defaultValue = "false") public boolean useBlockingDnsLookup; /** * The maximum size of the connection pool used by the WebClient. */ - @ConfigItem public OptionalInt maxPoolSize = OptionalInt.empty(); /** @@ -76,22 +83,18 @@ public class OidcCommonConfig { * When this property is disabled only a single redirect to exactly the same original URI * is allowed but only if one or more cookies were set during the redirect request. */ - @ConfigItem(defaultValue = "true") public boolean followRedirects = true; /** * Options to configure the proxy the OIDC adapter uses to talk with the OIDC server. */ - @ConfigItem public Proxy proxy = new Proxy(); /** * TLS configurations */ - @ConfigItem public Tls tls = new Tls(); - @ConfigGroup public static class Tls { /** @@ -102,9 +105,24 @@ public static class Tls { *

* The default TLS configuration is not used by default. */ - @ConfigItem Optional tlsConfigurationName = Optional.empty(); + private void addConfigMappingValues(io.quarkus.oidc.common.runtime.config.OidcCommonConfig.Tls mapping) { + this.tlsConfigurationName = mapping.tlsConfigurationName(); + this.verification = mapping.verification().map(Enum::toString).map(Verification::valueOf); + this.keyStoreFile = mapping.keyStoreFile(); + this.keyStoreFileType = mapping.keyStoreFileType(); + this.keyStoreProvider = mapping.keyStoreProvider(); + this.keyStorePassword = mapping.keyStorePassword(); + this.keyStoreKeyAlias = mapping.keyStoreKeyAlias(); + this.keyStoreKeyPassword = mapping.keyStoreKeyPassword(); + this.trustStoreFile = mapping.trustStoreFile(); + this.trustStorePassword = mapping.trustStorePassword(); + this.trustStoreCertAlias = mapping.trustStoreCertAlias(); + this.trustStoreFileType = mapping.trustStoreFileType(); + this.trustStoreProvider = mapping.trustStoreProvider(); + } + public enum Verification { /** * Certificates are validated and hostname verification is enabled. This is the default value. @@ -129,8 +147,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional verification = Optional.empty(); /** @@ -138,8 +154,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional keyStoreFile = Optional.empty(); /** @@ -147,8 +161,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional keyStoreFileType = Optional.empty(); /** @@ -157,8 +169,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional keyStoreProvider; /** @@ -166,8 +176,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional keyStorePassword; /** @@ -177,8 +185,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional keyStoreKeyAlias = Optional.empty(); /** @@ -186,8 +192,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional keyStoreKeyPassword = Optional.empty(); /** @@ -195,8 +199,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional trustStoreFile = Optional.empty(); /** @@ -204,8 +206,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional trustStorePassword = Optional.empty(); /** @@ -213,8 +213,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional trustStoreCertAlias = Optional.empty(); /** @@ -224,8 +222,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional trustStoreFileType = Optional.empty(); /** @@ -235,8 +231,6 @@ public enum Verification { * * @deprecated Use the TLS registry instead. */ - @Deprecated - @ConfigItem public Optional trustStoreProvider; public Optional getVerification() { @@ -289,7 +283,6 @@ public void setTrustStoreProvider(String trustStoreProvider) { } - @ConfigGroup public static class Proxy { /** @@ -297,27 +290,29 @@ public static class Proxy { * Note: If the OIDC adapter requires a Proxy to talk with the OIDC server (Provider), * set this value to enable the usage of a Proxy. */ - @ConfigItem public Optional host = Optional.empty(); /** * The port number of the Proxy. The default value is `80`. */ - @ConfigItem(defaultValue = "80") public int port = 80; /** * The username, if the Proxy needs authentication. */ - @ConfigItem public Optional username = Optional.empty(); /** * The password, if the Proxy needs authentication. */ - @ConfigItem public Optional password = Optional.empty(); + private void addConfigMappingValues(io.quarkus.oidc.common.runtime.config.OidcCommonConfig.Proxy mapping) { + this.host = mapping.host(); + this.port = mapping.port(); + this.username = mapping.username(); + this.password = mapping.password(); + } } public Optional getConnectionDelay() { @@ -383,4 +378,5 @@ public Optional getDiscoveryEnabled() { public void setDiscoveryEnabled(Boolean discoveryEnabled) { this.discoveryEnabled = Optional.of(discoveryEnabled); } + } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java new file mode 100644 index 0000000000000..8ad80d388a927 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java @@ -0,0 +1,269 @@ +package io.quarkus.oidc.common.runtime.config; + +import java.util.Map; +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.smallrye.config.WithDefault; + +public interface OidcClientCommonConfig extends OidcCommonConfig { + /** + * The OIDC token endpoint that issues access and refresh tokens; + * specified as a relative path or absolute URL. + * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. + */ + Optional tokenPath(); + + /** + * The relative path or absolute URL of the OIDC token revocation endpoint. + */ + Optional revokePath(); + + /** + * The client id of the application. Each application has a client id that is used to identify the application. + * Setting the client id is not required if {@link #applicationType} is `service` and no token introspection is required. + */ + Optional clientId(); + + /** + * The client name of the application. It is meant to represent a human readable description of the application which you + * may provide when an application (client) is registered in an OpenId Connect provider's dashboard. + * For example, you can set this property to have more informative log messages which record an activity of the given + * client. + */ + Optional clientName(); + + /** + * Credentials the OIDC adapter uses to authenticate to the OIDC server. + */ + Credentials credentials(); + + interface Credentials { + + /** + * The client secret used by the `client_secret_basic` authentication method. + * Must be set unless a secret is set in {@link #clientSecret} or {@link #jwt} client authentication is required. + * You can use `client-secret.value` instead, but both properties are mutually exclusive. + */ + Optional secret(); + + /** + * The client secret used by the `client_secret_basic` (default), `client_secret_post`, or `client_secret_jwt` + * authentication methods. + * Note that a `secret.value` property can be used instead to support the `client_secret_basic` method + * but both properties are mutually exclusive. + */ + Secret clientSecret(); + + /** + * Client JSON Web Token (JWT) authentication methods + */ + Jwt jwt(); + + /** + * Supports the client authentication methods that involve sending a client secret. + * + * @see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + */ + interface Secret { + + enum Method { + /** + * `client_secret_basic` (default): The client id and secret are submitted with the HTTP Authorization Basic + * scheme. + */ + BASIC, + + /** + * `client_secret_post`: The client id and secret are submitted as the `client_id` and `client_secret` + * form parameters. + */ + POST, + + /** + * `client_secret_jwt`: The client id and generated JWT secret are submitted as the `client_id` and + * `client_secret` + * form parameters. + */ + POST_JWT, + + /** + * client id and secret are submitted as HTTP query parameters. This option is only supported by the OIDC + * extension. + */ + QUERY + } + + /** + * The client secret value. This value is ignored if `credentials.secret` is set. + * Must be set unless a secret is set in {@link #clientSecret} or {@link #jwt} client authentication is required. + */ + Optional value(); + + /** + * The Secret CredentialsProvider. + */ + Provider provider(); + + /** + * The authentication method. + * If the `clientSecret.value` secret is set, this method is `basic` by default. + */ + Optional method(); + + } + + /** + * Supports the client authentication `client_secret_jwt` and `private_key_jwt` methods, which involves sending a JWT + * token assertion signed with a client secret or private key. + * JWT Bearer client authentication is also supported. + * + * @see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + */ + interface Jwt { + + enum Source { + // JWT token is generated by the OIDC provider client to support + // `client_secret_jwt` and `private_key_jwt` authentication methods + CLIENT, + // JWT bearer token as used as a client assertion: https://www.rfc-editor.org/rfc/rfc7523#section-2.2 + // This option is only supported by the OIDC client extension. + BEARER + } + + /** + * JWT token source: OIDC provider client or an existing JWT bearer token. + */ + @WithDefault("client") + Source source(); + + /** + * If provided, indicates that JWT is signed using a secret key. + * It is mutually exclusive with {@link #key}, {@link #keyFile} and {@link #keyStore} properties. + */ + Optional secret(); + + /** + * If provided, indicates that JWT is signed using a secret key provided by Secret CredentialsProvider. + */ + Provider secretProvider(); + + /** + * String representation of a private key. If provided, indicates that JWT is signed using a private key in PEM or + * JWK format. + * It is mutually exclusive with {@link #secret}, {@link #keyFile} and {@link #keyStore} properties. + * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. + */ + Optional key(); + + /** + * If provided, indicates that JWT is signed using a private key in PEM or JWK format. + * It is mutually exclusive with {@link #secret}, {@link #key} and {@link #keyStore} properties. + * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`. + */ + Optional keyFile(); + + /** + * If provided, indicates that JWT is signed using a private key from a keystore. + * It is mutually exclusive with {@link #secret}, {@link #key} and {@link #keyFile} properties. + */ + Optional keyStoreFile(); + + /** + * A parameter to specify the password of the keystore file. + */ + Optional keyStorePassword(); + + /** + * The private key id or alias. + */ + Optional keyId(); + + /** + * The private key password. + */ + Optional keyPassword(); + + /** + * The JWT audience (`aud`) claim value. + * By default, the audience is set to the address of the OpenId Connect Provider's token endpoint. + */ + Optional audience(); + + /** + * The key identifier of the signing key added as a JWT `kid` header. + */ + Optional tokenKeyId(); + + /** + * The issuer of the signing key added as a JWT `iss` claim. The default value is the client id. + */ + Optional issuer(); + + /** + * Subject of the signing key added as a JWT `sub` claim The default value is the client id. + */ + Optional subject(); + + /** + * Additional claims. + */ + @ConfigDocMapKey("claim-name") + Map claims(); + + /** + * The signature algorithm used for the {@link #keyFile} property. + * Supported values: `RS256` (default), `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512`, + * `HS256`, `HS384`, `HS512`. + */ + Optional signatureAlgorithm(); + + /** + * The JWT lifespan in seconds. This value is added to the time at which the JWT was issued to calculate the + * expiration time. + */ + @WithDefault("10") + int lifespan(); + + /** + * If true then the client authentication token is a JWT bearer grant assertion. Instead of producing + * 'client_assertion' + * and 'client_assertion_type' form properties, only 'assertion' is produced. + * This option is only supported by the OIDC client extension. + */ + @WithDefault("false") + boolean assertion(); + + } + + /** + * CredentialsProvider, which provides a client secret. + */ + interface Provider { + + /** + * The CredentialsProvider bean name, which should only be set if more than one CredentialsProvider is + * registered + */ + Optional name(); + + /** + * The CredentialsProvider keyring name. + * The keyring name is only required when the CredentialsProvider being + * used requires the keyring name to look up the secret, which is often the case when a CredentialsProvider is + * shared by multiple extensions to retrieve credentials from a more dynamic source like a vault instance or secret + * manager + */ + Optional keyringName(); + + /** + * The CredentialsProvider client secret key + */ + Optional key(); + + } + } + +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java new file mode 100644 index 0000000000000..4edf4be8a9d46 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcCommonConfig.java @@ -0,0 +1,249 @@ +package io.quarkus.oidc.common.runtime.config; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.smallrye.config.WithDefault; + +public interface OidcCommonConfig { + /** + * The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`. + * Do not set this property if you use 'quarkus-oidc' and the public key verification ({@link #publicKey}) + * or certificate chain verification only ({@link #certificateChain}) is required. + * The OIDC discovery endpoint is called by default by appending a `.well-known/openid-configuration` path to this URL. + * For Keycloak, use `https://host:port/realms/{realm}`, replacing `{realm}` with the Keycloak realm name. + */ + Optional authServerUrl(); + + /** + * Discovery of the OIDC endpoints. + * If not enabled, you must configure the OIDC endpoint URLs individually. + */ + @ConfigDocDefault("true") + Optional discoveryEnabled(); + + /** + * The relative path or absolute URL of the OIDC dynamic client registration endpoint. + * Set if {@link #discoveryEnabled} is `false` or a discovered token endpoint path must be customized. + */ + Optional registrationPath(); + + /** + * The duration to attempt the initial connection to an OIDC server. + * For example, setting the duration to `20S` allows 10 retries, each 2 seconds apart. + * This property is only effective when the initial OIDC connection is created. + * For dropped connections, use the `connection-retry-count` property instead. + */ + Optional connectionDelay(); + + /** + * The number of times to retry re-establishing an existing OIDC connection if it is temporarily lost. + * Different from `connection-delay`, which applies only to initial connection attempts. + * For instance, if a request to the OIDC token endpoint fails due to a connection issue, it will be retried as per this + * setting. + */ + @WithDefault("3") + int connectionRetryCount(); + + /** + * The number of seconds after which the current OIDC connection request times out. + */ + @WithDefault("10s") + Duration connectionTimeout(); + + /** + * Whether DNS lookup should be performed on the worker thread. + * Use this option when you can see logged warnings about blocked Vert.x event loop by HTTP requests to OIDC server. + */ + @WithDefault("false") + boolean useBlockingDnsLookup(); + + /** + * The maximum size of the connection pool used by the WebClient. + */ + OptionalInt maxPoolSize(); + + /** + * Follow redirects automatically when WebClient gets HTTP 302. + * When this property is disabled only a single redirect to exactly the same original URI + * is allowed but only if one or more cookies were set during the redirect request. + */ + @WithDefault("true") + boolean followRedirects(); + + /** + * Options to configure the proxy the OIDC adapter uses to talk with the OIDC server. + */ + Proxy proxy(); + + /** + * TLS configurations + */ + Tls tls(); + + interface Tls { + + /** + * The name of the TLS configuration to use. + *

+ * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * The default TLS configuration is not used by default. + */ + Optional tlsConfigurationName(); + + enum Verification { + /** + * Certificates are validated and hostname verification is enabled. This is the default value. + */ + REQUIRED, + + /** + * Certificates are validated but hostname verification is disabled. + */ + CERTIFICATE_VALIDATION, + + /** + * All certificates are trusted and hostname verification is disabled. + */ + NONE + } + + /** + * Certificate validation and hostname verification, which can be one of the following {@link Verification} + * values. + * Default is `required`. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional verification(); + + /** + * An optional keystore that holds the certificate information instead of specifying separate files. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional keyStoreFile(); + + /** + * The type of the keystore file. If not given, the type is automatically detected based on the file name. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional keyStoreFileType(); + + /** + * The provider of the keystore file. If not given, the provider is automatically detected based on the + * keystore file type. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional keyStoreProvider(); + + /** + * The password of the keystore file. If not given, the default value, `password`, is used. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional keyStorePassword(); + + /** + * The alias of a specific key in the keystore. + * When SNI is disabled, if the keystore contains multiple + * keys and no alias is specified, the behavior is undefined. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional keyStoreKeyAlias(); + + /** + * The password of the key, if it is different from the {@link #keyStorePassword}. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional keyStoreKeyPassword(); + + /** + * The truststore that holds the certificate information of the certificates to trust. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional trustStoreFile(); + + /** + * The password of the truststore file. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional trustStorePassword(); + + /** + * The alias of the truststore certificate. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional trustStoreCertAlias(); + + /** + * The type of the truststore file. + * If not given, the type is automatically detected + * based on the file name. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional trustStoreFileType(); + + /** + * The provider of the truststore file. + * If not given, the provider is automatically detected + * based on the truststore file type. + * + * @deprecated Use the TLS registry instead. + */ + @Deprecated + Optional trustStoreProvider(); + + } + + interface Proxy { + + /** + * The host name or IP address of the Proxy.
+ * Note: If the OIDC adapter requires a Proxy to talk with the OIDC server (Provider), + * set this value to enable the usage of a Proxy. + */ + Optional host(); + + /** + * The port number of the Proxy. The default value is `80`. + */ + @WithDefault("80") + int port(); + + /** + * The username, if the Proxy needs authentication. + */ + Optional username(); + + /** + * The password, if the Proxy needs authentication. + */ + Optional password(); + + } +} diff --git a/extensions/oidc/deployment/pom.xml b/extensions/oidc/deployment/pom.xml index 8e45ae8a28f43..8691bf75f1911 100644 --- a/extensions/oidc/deployment/pom.xml +++ b/extensions/oidc/deployment/pom.xml @@ -116,9 +116,6 @@ ${project.version}
- - -AlegacyConfigRoot=true - diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java index aca96f6e25a7d..1a386b660dda2 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java @@ -5,11 +5,9 @@ import java.util.Optional; import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; +import io.smallrye.config.WithDefault; -@ConfigGroup -public class DevUiConfig { +public interface DevUiConfig { /** * Grant type which affects how OpenId Connect Dev UI will facilitate the token acquisition. @@ -19,11 +17,10 @@ public class DevUiConfig { * handler to acquire the tokens while a username and password will have to be entered to request a token using a * 'password' grant. */ - public Grant grant = new Grant(); + Grant grant(); - @ConfigGroup - public static class Grant { - public static enum Type { + interface Grant { + enum Type { /** * 'client_credentials' grant */ @@ -45,7 +42,7 @@ public static enum Type { private String grantType; - private Type(String grantType) { + Type(String grantType) { this.grantType = grantType; } @@ -57,22 +54,20 @@ public String getGrantType() { /** * Grant type which will be used to acquire a token to test the OIDC 'service' applications */ - @ConfigItem - public Optional type; + Optional type(); } /** * Grant options */ - @ConfigItem @ConfigDocMapKey("option-name") - public Map> grantOptions; + Map> grantOptions(); /** * The WebClient timeout. * Use this property to configure how long an HTTP client used by Dev UI handlers will wait for a response when requesting * tokens from OpenId Connect Provider and sending them to the service endpoint. */ - @ConfigItem(defaultValue = "4S") - public Duration webClientTimeout; + @WithDefault("4S") + Duration webClientTimeout(); } 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 c5b4ccea69d6b..ee5af7e5b9afc 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 @@ -418,7 +418,7 @@ public static class IsEnabled implements BooleanSupplier { OidcBuildTimeConfig config; public boolean getAsBoolean() { - return config.enabled; + return config.enabled(); } } @@ -426,7 +426,7 @@ public static class IsCacheEnabled implements BooleanSupplier { OidcBuildTimeConfig config; public boolean getAsBoolean() { - return config.enabled && config.defaultTokenCacheEnabled; + return config.enabled() && config.defaultTokenCacheEnabled(); } } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java index 2a25611394c30..58cc8d51e3722 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildTimeConfig.java @@ -1,30 +1,32 @@ package io.quarkus.oidc.deployment; import io.quarkus.oidc.runtime.OidcConfig; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; /** * Build time configuration for OIDC. */ +@ConfigMapping(prefix = "quarkus.oidc") @ConfigRoot -public class OidcBuildTimeConfig { +public interface OidcBuildTimeConfig { /** * If the OIDC extension is enabled. */ - @ConfigItem(defaultValue = "true") - public boolean enabled; + @WithDefault("true") + boolean enabled(); /** * Dev UI configuration. */ - @ConfigItem - public DevUiConfig devui; + DevUiConfig devui(); + /** * Enable the registration of the Default TokenIntrospection and UserInfo Cache implementation bean. * Note: This only enables the default implementation. It requires configuration to be activated. * See {@link OidcConfig#tokenCache}. */ - @ConfigItem(defaultValue = "true") - public boolean defaultTokenCacheEnabled; + @WithDefault("true") + boolean defaultTokenCacheEnabled(); } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevUIProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevUIProcessor.java index 7e094aeccf6c6..f76c97c96b782 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevUIProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevUIProcessor.java @@ -112,7 +112,8 @@ public void run() { capabilities, providerName, getApplicationType(providerConfig), - oidcConfig.devui.grant.type.isPresent() ? oidcConfig.devui.grant.type.get().getGrantType() : "code", + oidcConfig.devui().grant().type().isPresent() ? oidcConfig.devui().grant().type().get().getGrantType() + : "code", metadataNotNull ? metadata.getString("authorization_endpoint") : null, metadataNotNull ? metadata.getString("token_endpoint") : null, metadataNotNull ? metadata.getString("end_session_endpoint") : null, @@ -120,8 +121,8 @@ public void run() { ? (metadata.containsKey("introspection_endpoint") || metadata.containsKey("userinfo_endpoint")) : checkProviderUserInfoRequired(providerConfig), syntheticBeanBuildItemBuildProducer, - oidcConfig.devui.webClientTimeout, - oidcConfig.devui.grantOptions, + oidcConfig.devui().webClientTimeout(), + oidcConfig.devui().grantOptions(), nonApplicationRootPathBuildItem, configurationBuildItem, keycloakAdminUrl, diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java index 52876537d5b2e..6a73ec05d4e59 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java @@ -53,14 +53,14 @@ void produceProviderComponent(Optional confi capabilities, "Keycloak", configProps.get().getConfig().get("quarkus.oidc.application-type"), - oidcConfig.devui.grant.type.orElse(DevUiConfig.Grant.Type.CODE).getGrantType(), + oidcConfig.devui().grant().type().orElse(DevUiConfig.Grant.Type.CODE).getGrantType(), realmUrl + "/protocol/openid-connect/auth", realmUrl + "/protocol/openid-connect/token", realmUrl + "/protocol/openid-connect/logout", true, syntheticBeanBuildItemBuildProducer, - oidcConfig.devui.webClientTimeout, - oidcConfig.devui.grantOptions, + oidcConfig.devui().webClientTimeout(), + oidcConfig.devui().grantOptions(), nonApplicationRootPathBuildItem, configurationBuildItem, keycloakAdminUrl, diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java index 33ea63f1f53ab..b4dd761333862 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResource.java @@ -45,7 +45,7 @@ public void logout() { @Path("access-token-name") @GET public String accessTokenName() { - if (!config.defaultTenant.authentication.verifyAccessToken) { + if (!config.defaultTenant().authentication().verifyAccessToken()) { throw new IllegalStateException("Access token verification should be enabled"); } return accessToken.getName(); diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java index dc82b0c8ce510..59d17dd128ab7 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithJwtAccessToken.java @@ -26,6 +26,6 @@ public class ProtectedResourceWithJwtAccessToken { @GET public String getName() { - return idToken.getName() + ":" + config.defaultTenant.authentication.verifyAccessToken; + return idToken.getName() + ":" + config.defaultTenant().authentication().verifyAccessToken(); } } diff --git a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java index d11ae1b9fdb02..3305810d605be 100644 --- a/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java +++ b/extensions/oidc/deployment/src/test/java/io/quarkus/oidc/test/ProtectedResourceWithoutJwtAccessToken.java @@ -23,6 +23,6 @@ public class ProtectedResourceWithoutJwtAccessToken { @GET public String getName() { - return idToken.getName() + ":" + config.defaultTenant.authentication.verifyAccessToken; + return idToken.getName() + ":" + config.defaultTenant().authentication().verifyAccessToken(); } } 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 73633f3bbb4a1..b809ea73d3822 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 @@ -12,8 +12,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.oidc.UserInfo; -import io.quarkus.oidc.runtime.OidcConfig; +import io.quarkus.oidc.runtime.TenantConfigBean; import io.quarkus.security.PermissionsAllowed; import io.quarkus.test.QuarkusDevModeTest; import io.quarkus.test.common.QuarkusTestResource; @@ -92,16 +93,16 @@ void observe(@Observes Router router) { public static class UserInfoResource { @Inject - OidcConfig config; + UserInfo userInfo; @Inject - UserInfo userInfo; + TenantConfigBean tenantConfigBean; @PermissionsAllowed("openid") @Path("default-tenant") @GET public String getDefaultTenantName() { - if (!config.defaultTenant.authentication.userInfoRequired.orElse(false)) { + if (!tenantConfigBean.getDefaultTenant().oidcConfig().authentication.userInfoRequired.orElse(false)) { throw new IllegalStateException("Default tenant user info should be required"); } return userInfo.getPreferredUserName(); @@ -111,7 +112,7 @@ public String getDefaultTenantName() { @Path("named-tenant") @GET public String getNamedTenantName() { - if (!config.namedTenants.get("named").authentication.userInfoRequired.orElse(false)) { + if (!getNamedTenantConfig("named").authentication.userInfoRequired.orElse(false)) { throw new IllegalStateException("Named tenant user info should be required"); } return userInfo.getPreferredUserName(); @@ -121,14 +122,18 @@ public String getNamedTenantName() { @Path("named-tenant-2") @GET public boolean getNamed2TenantUserInfoRequired() { - return config.namedTenants.get("named-2").authentication.userInfoRequired.orElse(false); + return getNamedTenantConfig("named-2").authentication.userInfoRequired.orElse(false); } @PermissionsAllowed("openid") @Path("named-tenant-3") @GET public boolean getNamed3TenantUserInfoRequired() { - return config.namedTenants.get("named-3").authentication.userInfoRequired.orElse(false); + return getNamedTenantConfig("named-3").authentication.userInfoRequired.orElse(false); + } + + private OidcTenantConfig getNamedTenantConfig(String configName) { + return tenantConfigBean.getStaticTenant(configName).oidcConfig(); } } diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml index 25bb4a266db35..9e16adde7705b 100644 --- a/extensions/oidc/runtime/pom.xml +++ b/extensions/oidc/runtime/pom.xml @@ -81,9 +81,6 @@ ${project.version}
- - -AlegacyConfigRoot=true - diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index abc3b0a0627e9..252501412a6ff 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -13,21 +13,49 @@ import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.runtime.OidcConfig; -import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConvertWith; -import io.quarkus.runtime.configuration.TrimmedStringConverter; import io.quarkus.security.identity.SecurityIdentityAugmentor; -@ConfigGroup public class OidcTenantConfig extends OidcClientCommonConfig { + public OidcTenantConfig() { + + } + + public OidcTenantConfig(io.quarkus.oidc.runtime.OidcTenantConfig mapping, String fallbackTenantId) { + super(mapping); + if (mapping.tenantId().isPresent()) { + tenantId = mapping.tenantId(); + } else { + tenantId = Optional.ofNullable(fallbackTenantId); + } + tenantEnabled = mapping.tenantEnabled(); + applicationType = mapping.applicationType().map(Enum::toString).map(ApplicationType::valueOf); + authorizationPath = mapping.authorizationPath(); + userInfoPath = mapping.userInfoPath(); + introspectionPath = mapping.introspectionPath(); + jwksPath = mapping.jwksPath(); + endSessionPath = mapping.endSessionPath(); + tenantPaths = mapping.tenantPaths(); + publicKey = mapping.publicKey(); + introspectionCredentials.addConfigMappingValues(mapping.introspectionCredentials()); + roles.addConfigMappingValues(mapping.roles()); + token.addConfigMappingValues(mapping.token()); + logout.addConfigMappingValues(mapping.logout()); + certificateChain.addConfigMappingValues(mapping.certificateChain()); + authentication.addConfigMappingValues(mapping.authentication()); + codeGrant.addConfigMappingValues(mapping.codeGrant()); + tokenStateManager.addConfigMappingValues(mapping.tokenStateManager()); + allowTokenIntrospectionCache = mapping.allowTokenIntrospectionCache(); + allowUserInfoCache = mapping.allowUserInfoCache(); + cacheUserInfoInIdtoken = mapping.cacheUserInfoInIdtoken(); + jwks.addConfigMappingValues(mapping.jwks()); + provider = mapping.provider().map(Enum::toString).map(Provider::valueOf); + } + /** * A unique tenant identifier. It can be set by {@code TenantConfigResolver} providers, which * resolve the tenant configuration dynamically. */ - @ConfigItem public Optional tenantId = Optional.empty(); /** @@ -38,13 +66,11 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * or named tenants are configured. * In this case, you do not need to disable the default tenant. */ - @ConfigItem(defaultValue = "true") public boolean tenantEnabled = true; /** * The application type, which can be one of the following {@link ApplicationType} values. */ - @ConfigItem(defaultValueDocumentation = "service") public Optional applicationType = Optional.empty(); /** @@ -53,7 +79,6 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * You must set this property for `web-app` applications if OIDC discovery is disabled. * This property is ignored if OIDC discovery is enabled. */ - @ConfigItem public Optional authorizationPath = Optional.empty(); /** @@ -62,7 +87,6 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * and the `authentication.user-info-required` property is enabled. * This property is ignored if OIDC discovery is enabled. */ - @ConfigItem public Optional userInfoPath = Optional.empty(); /** @@ -72,7 +96,6 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * or 2) JWT tokens must be verified while the cached JWK verification set with no matching JWK is being refreshed. * This property is ignored if the discovery is enabled. */ - @ConfigItem public Optional introspectionPath = Optional.empty(); /** @@ -81,7 +104,6 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * This property should be set if OIDC discovery is disabled and the local JWT verification is required. * This property is ignored if the discovery is enabled. */ - @ConfigItem public Optional jwksPath = Optional.empty(); /** @@ -90,7 +112,6 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * required. * This property is ignored if the discovery is enabled. */ - @ConfigItem public Optional endSessionPath = Optional.empty(); /** @@ -100,14 +121,12 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * * @asciidoclet */ - @ConfigItem public Optional> tenantPaths = Optional.empty(); /** * The public key for the local JWT token verification. * OIDC server connection is not created when this property is set. */ - @ConfigItem public Optional publicKey = Optional.empty(); /** @@ -115,30 +134,25 @@ public class OidcTenantConfig extends OidcClientCommonConfig { * and OpenId Connect Provider does not support the OIDC client authentication configured with * {@link OidcCommonConfig#credentials} for its introspection endpoint. */ - @ConfigItem public IntrospectionCredentials introspectionCredentials = new IntrospectionCredentials(); /** * Introspection Basic Authentication configuration */ - @ConfigGroup public static class IntrospectionCredentials { /** * Name */ - @ConfigItem public Optional name = Optional.empty(); /** * Secret */ - @ConfigItem public Optional secret = Optional.empty(); /** * Include OpenId Connect Client ID configured with `quarkus.oidc.client-id`. */ - @ConfigItem(defaultValue = "true") public boolean includeClientId = true; public Optional getName() { @@ -165,24 +179,26 @@ public void setIncludeClientId(boolean includeClientId) { this.includeClientId = includeClientId; } + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.IntrospectionCredentials mapping) { + name = mapping.name(); + secret = mapping.secret(); + includeClientId = mapping.includeClientId(); + } } /** * Configuration to find and parse a custom claim containing the roles information. */ - @ConfigItem public Roles roles = new Roles(); /** * Configuration how to validate the token claims. */ - @ConfigItem public Token token = new Token(); /** * RP Initiated, BackChannel and FrontChannel Logout configuration */ - @ConfigItem public Logout logout = new Logout(); /** @@ -200,35 +216,29 @@ public void setIncludeClientId(boolean includeClientId) { * If the truststore does not have the leaf certificate imported, then the leaf certificate must be identified by its Common * Name. */ - @ConfigItem public CertificateChain certificateChain = new CertificateChain(); - @ConfigGroup public static class CertificateChain { /** * Common name of the leaf certificate. It must be set if the {@link #trustStoreFile} does not have * this certificate imported. * */ - @ConfigItem public Optional leafCertificateName = Optional.empty(); /** * Truststore file which keeps thumbprints of the trusted certificates. */ - @ConfigItem public Optional trustStoreFile = Optional.empty(); /** * A parameter to specify the password of the truststore file if it is configured with {@link #trustStoreFile}. */ - @ConfigItem public Optional trustStorePassword = Optional.empty(); /** * A parameter to specify the alias of the truststore certificate. */ - @ConfigItem public Optional trustStoreCertAlias = Optional.empty(); /** @@ -236,7 +246,6 @@ public static class CertificateChain { * detected * based on the file name. */ - @ConfigItem public Optional trustStoreFileType = Optional.empty(); public Optional getTrustStoreFile() { @@ -278,6 +287,14 @@ public Optional getTrustStorePassword() { public void setTrustStorePassword(String trustStorePassword) { this.trustStorePassword = Optional.ofNullable(trustStorePassword); } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.CertificateChain mapping) { + leafCertificateName = mapping.leafCertificateName(); + trustStoreFile = mapping.trustStoreFile(); + trustStorePassword = mapping.trustStorePassword(); + trustStoreCertAlias = mapping.trustStoreCertAlias(); + trustStoreFileType = mapping.trustStoreFileType(); + } } /** @@ -293,7 +310,6 @@ public void setTrustStorePassword(String trustStorePassword) { /** * Default token state manager configuration */ - @ConfigItem public TokenStateManager tokenStateManager = new TokenStateManager(); /** @@ -302,7 +318,6 @@ public void setTrustStorePassword(String trustStorePassword) { * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ - @ConfigItem(defaultValue = "true") public boolean allowTokenIntrospectionCache = true; /** @@ -311,7 +326,6 @@ public void setTrustStorePassword(String trustStorePassword) { * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable * it. */ - @ConfigItem(defaultValue = "true") public boolean allowUserInfoCache = true; /** @@ -324,10 +338,8 @@ public void setTrustStorePassword(String trustStorePassword) { * and the UserInfo cache is not enabled or caching UserInfo is disabled for the current tenant * with the {@link #allowUserInfoCache} property set to `false`. */ - @ConfigItem public Optional cacheUserInfoInIdtoken = Optional.empty(); - @ConfigGroup public static class Logout { /** @@ -335,7 +347,6 @@ public static class Logout { * initiate the * logout through this endpoint in conformance with the OpenID Connect RP-Initiated Logout specification. */ - @ConfigItem public Optional path = Optional.empty(); /** @@ -344,32 +355,26 @@ public static class Logout { * Connect Provider. * This endpoint URI must be properly registered at the OpenID Connect Provider as a valid redirect URI. */ - @ConfigItem public Optional postLogoutPath = Optional.empty(); /** * Name of the post logout URI parameter which is added as a query parameter to the logout redirect URI. */ - @ConfigItem(defaultValue = OidcConstants.POST_LOGOUT_REDIRECT_URI) public String postLogoutUriParam; /** * Additional properties which is added as the query parameters to the logout redirect URI. */ - @ConfigItem - @ConfigDocMapKey("query-parameter-name") public Map extraParams; /** * Back-Channel Logout configuration */ - @ConfigItem public Backchannel backchannel = new Backchannel(); /** * Front-Channel Logout configuration */ - @ConfigItem public Frontchannel frontchannel = new Frontchannel(); public void setPath(Optional path) { @@ -419,36 +424,40 @@ public Frontchannel getFrontchannel() { public void setFrontchannel(Frontchannel frontchannel) { this.frontchannel = frontchannel; } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Logout mapping) { + path = mapping.path(); + postLogoutPath = mapping.postLogoutPath(); + postLogoutUriParam = mapping.postLogoutUriParam(); + extraParams = mapping.extraParams(); + backchannel.addConfigMappingValues(mapping.backchannel()); + frontchannel.addConfigMappingValues(mapping.frontchannel()); + } } - @ConfigGroup public static class Backchannel { /** * The relative path of the Back-Channel Logout endpoint at the application. * It must start with the forward slash '/', for example, '/back-channel-logout'. * This value is always resolved relative to 'quarkus.http.root-path'. */ - @ConfigItem public Optional path = Optional.empty(); /** * Maximum number of logout tokens that can be cached before they are matched against ID tokens stored in session * cookies. */ - @ConfigItem(defaultValue = "10") public int tokenCacheSize = 10; /** * Number of minutes a logout token can be cached for. */ - @ConfigItem(defaultValue = "10M") public Duration tokenCacheTimeToLive = Duration.ofMinutes(10); /** * Token cache timer interval. * If this property is set, a timer checks and removes the stale entries periodically. */ - @ConfigItem public Optional cleanUpTimerInterval = Optional.empty(); /** @@ -456,7 +465,6 @@ public static class Backchannel { * Only `sub` (subject) and `sid` (session id) claims can be used as keys. * Set it to `sid` only if ID tokens issued by the OIDC provider have no `sub` but have `sid` claim. */ - @ConfigItem(defaultValue = "sub") public String logoutTokenKey = "sub"; public void setPath(Optional path) { @@ -498,15 +506,21 @@ public Optional getCleanUpTimerInterval() { public void setCleanUpTimerInterval(Duration cleanUpTimerInterval) { this.cleanUpTimerInterval = Optional.of(cleanUpTimerInterval); } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Backchannel mapping) { + path = mapping.path(); + tokenCacheSize = mapping.tokenCacheSize(); + tokenCacheTimeToLive = mapping.tokenCacheTimeToLive(); + cleanUpTimerInterval = mapping.cleanUpTimerInterval(); + logoutTokenKey = mapping.logoutTokenKey(); + } } /** * Configuration for controlling how JsonWebKeySet containing verification keys should be acquired and managed. */ - @ConfigItem public Jwks jwks = new Jwks(); - @ConfigGroup public static class Jwks { /** * If JWK verification keys should be fetched at the moment a connection to the OIDC provider @@ -516,21 +530,18 @@ public static class Jwks { * has to be verified. Typically it can only be necessary if the token or other telated request properties * provide an additional context which is required to resolve the keys correctly. */ - @ConfigItem(defaultValue = "true") public boolean resolveEarly = true; /** * Maximum number of JWK keys that can be cached. * This property is ignored if the {@link #resolveEarly} property is set to true. */ - @ConfigItem(defaultValue = "10") public int cacheSize = 10; /** * Number of minutes a JWK key can be cached for. * This property is ignored if the {@link #resolveEarly} property is set to true. */ - @ConfigItem(defaultValue = "10M") public Duration cacheTimeToLive = Duration.ofMinutes(10); /** @@ -538,14 +549,12 @@ public static class Jwks { * If this property is set, a timer checks and removes the stale entries periodically. * This property is ignored if the {@link #resolveEarly} property is set to true. */ - @ConfigItem public Optional cleanUpTimerInterval = Optional.empty(); /** * In case there is no key identifier ('kid') or certificate thumbprints ('x5t', 'x5t#S256') specified in the JOSE * header and no key could be determined, check all available keys matching the token algorithm ('alg') header value. */ - @ConfigItem(defaultValue = "false") public boolean tryAll = false; public int getCacheSize() { @@ -587,14 +596,20 @@ public boolean isTryAll() { public void setTryAll(boolean fallbackToTryAll) { this.tryAll = fallbackToTryAll; } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Jwks mapping) { + resolveEarly = mapping.resolveEarly(); + cacheSize = mapping.cacheSize(); + cacheTimeToLive = mapping.cacheTimeToLive(); + cleanUpTimerInterval = mapping.cleanUpTimerInterval(); + tryAll = mapping.tryAll(); + } } - @ConfigGroup public static class Frontchannel { /** * The relative path of the Front-Channel Logout endpoint at the application. */ - @ConfigItem public Optional path = Optional.empty(); public void setPath(Optional path) { @@ -604,12 +619,15 @@ public void setPath(Optional path) { public Optional getPath() { return path; } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Frontchannel mapping) { + path = mapping.path(); + } } /** * Default Authorization Code token state manager configuration */ - @ConfigGroup public static class TokenStateManager { public enum Strategy { @@ -632,7 +650,6 @@ public enum Strategy { /** * Default TokenStateManager strategy. */ - @ConfigItem(defaultValue = "keep_all_tokens") public Strategy strategy = Strategy.KEEP_ALL_TOKENS; /** @@ -641,13 +658,11 @@ public enum Strategy { * * Enable this property to minimize a session cookie size */ - @ConfigItem(defaultValue = "false") public boolean splitTokens; /** * Mandates that the Default TokenStateManager encrypt the session cookie that stores the tokens. */ - @ConfigItem(defaultValue = "true") public boolean encryptionRequired = true; /** @@ -666,7 +681,6 @@ public enum Strategy { * The length of the secret used to encrypt the tokens should be at least 32 characters long. * A warning is logged if the secret length is less than 16 characters. */ - @ConfigItem public Optional encryptionSecret = Optional.empty(); /** @@ -693,7 +707,6 @@ public static enum EncryptionAlgorithm { /** * Session cookie key encryption algorithm */ - @ConfigItem(defaultValue = "A256GCMKW") public EncryptionAlgorithm encryptionAlgorithm = EncryptionAlgorithm.A256GCMKW; public boolean isEncryptionRequired() { @@ -735,6 +748,14 @@ public EncryptionAlgorithm getEncryptionAlgorithm() { public void setEncryptionAlgorithm(EncryptionAlgorithm encryptionAlgorithm) { this.encryptionAlgorithm = encryptionAlgorithm; } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.TokenStateManager mapping) { + strategy = Strategy.valueOf(mapping.strategy().toString()); + splitTokens = mapping.splitTokens(); + encryptionRequired = mapping.encryptionRequired(); + encryptionSecret = mapping.encryptionSecret(); + encryptionAlgorithm = EncryptionAlgorithm.valueOf(mapping.encryptionAlgorithm().toString()); + } } public Optional getAuthorizationPath() { @@ -833,7 +854,6 @@ public Logout getLogout() { return logout; } - @ConfigGroup public static class Roles { public static Roles fromClaimPath(List path) { @@ -855,20 +875,17 @@ public static Roles fromClaimPathAndSeparator(List path, String sep) { * Use double quotes with the namespace-qualified claim names. * This property can be used if a token has no `groups` claim but has the groups set in one or more different claims. */ - @ConfigItem public Optional> roleClaimPath = Optional.empty(); /** * The separator for splitting strings that contain multiple group values. * It is only used if the "role-claim-path" property points to one or more custom claims whose values are strings. * A single space is used by default because the standard `scope` claim can contain a space-separated sequence. */ - @ConfigItem public Optional roleClaimSeparator = Optional.empty(); /** * Source of the principal roles. */ - @ConfigItem public Optional source = Optional.empty(); public Optional> getRoleClaimPath() { @@ -895,6 +912,12 @@ public void setSource(Source source) { this.source = Optional.of(source); } + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Roles mapping) { + roleClaimPath = mapping.roleClaimPath(); + roleClaimSeparator = mapping.roleClaimSeparator(); + source = mapping.source().map(Enum::toString).map(Source::valueOf); + } + // Source of the principal roles public static enum Source { /** @@ -919,7 +942,6 @@ public static enum Source { * Defines the authorization request properties when authenticating * users using the Authorization Code Grant Type. */ - @ConfigGroup public static class Authentication { /** @@ -950,7 +972,6 @@ public enum ResponseMode { /** * Authorization code flow response mode. */ - @ConfigItem(defaultValueDocumentation = "query") public Optional responseMode = Optional.empty(); /** @@ -962,7 +983,6 @@ public enum ResponseMode { * Note the original request URI is restored after the user has authenticated if `restorePathAfterRedirect` is set * to `true`. */ - @ConfigItem public Optional redirectPath = Optional.empty(); /** @@ -972,14 +992,12 @@ public enum ResponseMode { * Note if `redirectPath` property is not set, the original request URI is restored even if this property is * disabled. */ - @ConfigItem(defaultValue = "false") public boolean restorePathAfterRedirect; /** * Remove the query parameters such as `code` and `state` set by the OIDC server on the redirect URI * after the user has authenticated by redirecting a user to the same URI but without the query parameters. */ - @ConfigItem(defaultValue = "true") public boolean removeRedirectParameters = true; /** @@ -997,7 +1015,6 @@ public enum ResponseMode { * * If this property is not set, HTTP 401 status is returned in case of the user authentication failure. */ - @ConfigItem public Optional errorPath = Optional.empty(); /** @@ -1012,7 +1029,6 @@ public enum ResponseMode { * instead, which can inform that the session has expired and advise the user to re-authenticated by following * a link to the secured initial entry page. */ - @ConfigItem public Optional sessionExpiredPath = Optional.empty(); /** @@ -1030,7 +1046,6 @@ public enum ResponseMode { *

* Bearer access token is always verified. */ - @ConfigItem(defaultValueDocumentation = "true when access token is injected as the JsonWebToken bean, false otherwise") public boolean verifyAccessToken; /** @@ -1038,20 +1053,17 @@ public enum ResponseMode { * proxy. * This property, if enabled, also affects the logout `post_logout_redirect_uri` and the local redirect requests. */ - @ConfigItem(defaultValueDocumentation = "false") public Optional forceRedirectHttpsScheme = Optional.empty(); /** * List of scopes */ - @ConfigItem public Optional> scopes = Optional.empty(); /** * The separator which is used when more than one scope is configured. * A single space is used by default. */ - @ConfigItem public Optional scopeSeparator = Optional.empty(); /** @@ -1060,28 +1072,22 @@ public enum ResponseMode { * Do not enable this property if your OpenId Connect provider does not support setting `nonce` in ID token * or if you work with OAuth2 provider such as `GitHub` which does not issue ID tokens. */ - @ConfigItem(defaultValue = "false") public boolean nonceRequired = false; /** * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers, * but does not work for OAuth2 providers such as Twitter OAuth2, which do not accept this scope and throw errors. */ - @ConfigItem(defaultValueDocumentation = "true") public Optional addOpenidScope = Optional.empty(); /** * Additional properties added as query parameters to the authentication redirect URI. */ - @ConfigItem - @ConfigDocMapKey("parameter-name") public Map extraParams = new HashMap<>(); /** * Request URL query parameters which, if present, are added to the authentication redirect URI. */ - @ConfigItem - @ConvertWith(TrimmedStringConverter.class) public Optional> forwardParams = Optional.empty(); /** @@ -1089,7 +1095,6 @@ public enum ResponseMode { * when HTTP is used. It might be necessary when running behind an SSL/TLS terminating reverse proxy. * The cookies are always secure if HTTPS is used, even if this property is set to false. */ - @ConfigItem(defaultValue = "false") public boolean cookieForceSecure; /** @@ -1097,7 +1102,6 @@ public enum ResponseMode { * For example, a session cookie name for the default OIDC tenant is `q_session` but can be changed to `q_session_test` * if this property is set to `test`. */ - @ConfigItem public Optional cookieSuffix = Optional.empty(); /** @@ -1105,7 +1109,6 @@ public enum ResponseMode { * logout cookies. * The `cookie-path-header` property, if set, is checked first. */ - @ConfigItem(defaultValue = "/") public String cookiePath = "/"; /** @@ -1113,19 +1116,16 @@ public enum ResponseMode { * whose value is used to set a path parameter for the session, state and post logout cookies. * If the header is missing, the `cookie-path` property is checked. */ - @ConfigItem public Optional cookiePathHeader = Optional.empty(); /** * Cookie domain parameter value which, if set, is used for the session, state and post logout cookies. */ - @ConfigItem public Optional cookieDomain = Optional.empty(); /** * SameSite attribute for the session cookie. */ - @ConfigItem(defaultValue = "lax") public CookieSameSite cookieSameSite = CookieSameSite.LAX; /** @@ -1138,7 +1138,6 @@ public enum ResponseMode { * Disable this property to permit only a single authorization code flow in the same browser. * */ - @ConfigItem(defaultValue = "true") public boolean allowMultipleCodeFlows = true; /** @@ -1162,7 +1161,6 @@ public enum ResponseMode { * It causes a new authentication redirect to OpenId Connect provider. Doing so might increase the * risk of browser redirect loops. */ - @ConfigItem(defaultValue = "false") public boolean failOnMissingStateParam = false; /** @@ -1176,7 +1174,6 @@ public enum ResponseMode { * It is also enabled automatically if `io.quarkus.oidc.UserInfo` injection point is detected but only * if the current OIDC tenant supports a UserInfo endpoint. */ - @ConfigItem(defaultValueDocumentation = "true when UserInfo bean is injected, false otherwise") public Optional userInfoRequired = Optional.empty(); /** @@ -1187,7 +1184,6 @@ public enum ResponseMode { * the session has expired. * This property is ignored if the `token.refresh-expired` property has not been enabled. */ - @ConfigItem(defaultValue = "5M") public Duration sessionAgeExtension = Duration.ofMinutes(5); /** @@ -1197,7 +1193,6 @@ public enum ResponseMode { * State cookie name is unique by default, see {@link #allowMultipleCodeFlows}. * Keep its age to the reasonable minimum value such as 5 minutes or less. */ - @ConfigItem(defaultValue = "5M") public Duration stateCookieAge = Duration.ofMinutes(5); /** @@ -1215,7 +1210,6 @@ public enum ResponseMode { * this property is enabled. You can register a custom {@linkplain JavaScriptRequestChecker} to do a custom JavaScript * request check instead. */ - @ConfigItem(defaultValue = "true") public boolean javaScriptAutoRedirect = true; /** @@ -1223,20 +1217,17 @@ public enum ResponseMode { * Disable this property only when you need to use the authorization code flow with OAuth2 providers which do not return * ID token - an internal IdToken is generated in such cases. */ - @ConfigItem(defaultValueDocumentation = "true") public Optional idTokenRequired = Optional.empty(); /** * Internal ID token lifespan. * This property is only checked when an internal IdToken is generated when Oauth2 providers do not return IdToken. */ - @ConfigItem(defaultValueDocumentation = "5M") public Optional internalIdTokenLifespan = Optional.empty(); /** * Requires that a Proof Key for Code Exchange (PKCE) is used. */ - @ConfigItem(defaultValueDocumentation = "false") public Optional pkceRequired = Optional.empty(); /** @@ -1246,8 +1237,6 @@ public enum ResponseMode { * @deprecated This field is deprecated. Use {@link #stateSecret} instead. * */ - @ConfigItem - @Deprecated(forRemoval = true) public Optional pkceSecret = Optional.empty(); /** @@ -1265,7 +1254,6 @@ public enum ResponseMode { *

* Error is reported if the secret length is less than 16 characters. */ - @ConfigItem public Optional stateSecret = Optional.empty(); public Optional getInternalIdTokenLifespan() { @@ -1509,27 +1497,56 @@ public Optional getSessionExpiredPath() { public void setSessionExpiredPath(String sessionExpiredPath) { this.sessionExpiredPath = Optional.of(sessionExpiredPath); } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Authentication mapping) { + responseMode = mapping.responseMode().map(Enum::toString).map(ResponseMode::valueOf); + redirectPath = mapping.redirectPath(); + restorePathAfterRedirect = mapping.restorePathAfterRedirect(); + removeRedirectParameters = mapping.removeRedirectParameters(); + errorPath = mapping.errorPath(); + sessionExpiredPath = mapping.sessionExpiredPath(); + verifyAccessToken = mapping.verifyAccessToken(); + forceRedirectHttpsScheme = mapping.forceRedirectHttpsScheme(); + scopes = mapping.scopes(); + scopeSeparator = mapping.scopeSeparator(); + nonceRequired = mapping.nonceRequired(); + addOpenidScope = mapping.addOpenidScope(); + extraParams = mapping.extraParams(); + forwardParams = mapping.forwardParams(); + cookieForceSecure = mapping.cookieForceSecure(); + cookieSuffix = mapping.cookieSuffix(); + cookiePath = mapping.cookiePath(); + cookiePathHeader = mapping.cookiePathHeader(); + cookieDomain = mapping.cookieDomain(); + cookieSameSite = CookieSameSite.valueOf(mapping.cookieSameSite().toString()); + allowMultipleCodeFlows = mapping.allowMultipleCodeFlows(); + failOnMissingStateParam = mapping.failOnMissingStateParam(); + userInfoRequired = mapping.userInfoRequired(); + sessionAgeExtension = mapping.sessionAgeExtension(); + stateCookieAge = mapping.stateCookieAge(); + javaScriptAutoRedirect = mapping.javaScriptAutoRedirect(); + idTokenRequired = mapping.idTokenRequired(); + internalIdTokenLifespan = mapping.internalIdTokenLifespan(); + pkceRequired = mapping.pkceRequired(); + pkceSecret = mapping.pkceSecret(); + stateSecret = mapping.stateSecret(); + } } /** * Authorization Code grant configuration */ - @ConfigGroup public static class CodeGrant { /** * Additional parameters, in addition to the required `code` and `redirect-uri` parameters, * which must be included to complete the authorization code grant request. */ - @ConfigItem - @ConfigDocMapKey("parameter-name") public Map extraParams = new HashMap<>(); /** * Custom HTTP headers which must be sent to complete the authorization code grant request. */ - @ConfigItem - @ConfigDocMapKey("header-name") public Map headers = new HashMap<>(); public Map getExtraParams() { @@ -1547,6 +1564,11 @@ public Map getHeaders() { public void setHeaders(Map headers) { this.headers = headers; } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.CodeGrant mapping) { + extraParams = mapping.extraParams(); + headers = mapping.headers(); + } } /** @@ -1573,7 +1595,6 @@ public String getAlgorithm() { } } - @ConfigGroup public static class Token { public static Token fromIssuer(String issuer) { @@ -1599,7 +1620,6 @@ public static Token fromAudience(String... audience) { * configuring * the provider to use the fixed `iss` claim value) are not possible. */ - @ConfigItem public Optional issuer = Optional.empty(); /** @@ -1613,7 +1633,6 @@ public static Token fromAudience(String... audience) { * * Audience verification for access tokens is only done if this property is configured. */ - @ConfigItem public Optional> audience = Optional.empty(); /** @@ -1622,7 +1641,6 @@ public static Token fromAudience(String... audience) { * Note that if you enable this property and if UserInfo is also required, * both the token and UserInfo `sub` claims must be present and match each other. */ - @ConfigItem(defaultValue = "false") public boolean subjectRequired = false; /** @@ -1632,14 +1650,11 @@ public static Token fromAudience(String... audience) { * Strings are the only supported types. Use {@linkplain SecurityIdentityAugmentor} to verify claims of other types or * complex claims. */ - @ConfigItem - @ConfigDocMapKey("claim-name") public Map requiredClaims = new HashMap<>(); /** * Expected token type */ - @ConfigItem public Optional tokenType = Optional.empty(); /** @@ -1649,7 +1664,6 @@ public static Token fromAudience(String... audience) { * When checking token issuance, current time is allowed to be sooner than token issue time by at most the configured * number of seconds. */ - @ConfigItem public OptionalInt lifespanGrace = OptionalInt.empty(); /** @@ -1667,7 +1681,6 @@ public static Token fromAudience(String... audience) { * However, even if the current logout token is allowed to have no `exp` claim, the `exp` claim is still verified * if the logout token contains it. */ - @ConfigItem public Optional age = Optional.empty(); /** @@ -1677,7 +1690,6 @@ public static Token fromAudience(String... audience) { * Note that ID token is always required to have an `iat` claim and therefore this property has no impact on the ID * token verification process. */ - @ConfigItem(defaultValue = "true") public boolean issuedAtRequired = true; /** @@ -1685,7 +1697,6 @@ public static Token fromAudience(String... audience) { * claims are * checked. */ - @ConfigItem public Optional principalClaim = Optional.empty(); /** @@ -1703,7 +1714,6 @@ public static Token fromAudience(String... audience) { * This property is enabled if `quarkus.oidc.token.refresh-token-time-skew` is configured, * you do not need to enable this property manually in this case. */ - @ConfigItem public boolean refreshExpired; /** @@ -1713,26 +1723,22 @@ public static Token fromAudience(String... audience) { * If the sum is greater than the authorization code ID or access token's expiration time, a refresh is going to * happen. */ - @ConfigItem public Optional refreshTokenTimeSkew = Optional.empty(); /** * The forced JWK set refresh interval in minutes. */ - @ConfigItem(defaultValue = "10M") public Duration forcedJwkRefreshInterval = Duration.ofMinutes(10); /** * Custom HTTP header that contains a bearer token. * This option is valid only when the application is of type {@link ApplicationType#SERVICE}}. */ - @ConfigItem public Optional header = Optional.empty(); /** * HTTP Authorization header scheme. */ - @ConfigItem(defaultValue = OidcConstants.BEARER_SCHEME) public String authorizationScheme = OidcConstants.BEARER_SCHEME; /** @@ -1740,7 +1746,6 @@ public static Token fromAudience(String... audience) { * OIDC providers support many signature algorithms but if necessary you can restrict * Quarkus application to accept tokens signed only using an algorithm configured with this property. */ - @ConfigItem public Optional signatureAlgorithm = Optional.empty(); /** @@ -1753,7 +1758,6 @@ public static Token fromAudience(String... audience) { * If this property is not set and the `private_key_jwt` client authentication method is used, the private key * used to sign the client authentication JWT tokens are also used to decrypt the encrypted ID tokens. */ - @ConfigItem public Optional decryptionKeyLocation = Optional.empty(); /** @@ -1765,14 +1769,12 @@ public static Token fromAudience(String... audience) { * Also note this property is ignored if JWK endpoint URI is not available and introspecting the tokens is * the only verification option. */ - @ConfigItem(defaultValue = "true") public boolean allowJwtIntrospection = true; /** * Require that JWT tokens are only introspected remotely. * */ - @ConfigItem(defaultValue = "false") public boolean requireJwtIntrospectionOnly = false; /** @@ -1780,7 +1782,6 @@ public static Token fromAudience(String... audience) { * * Set this property to `false` if only JWT tokens are expected. */ - @ConfigItem(defaultValue = "true") public boolean allowOpaqueTokenIntrospection = true; /** @@ -1790,7 +1791,6 @@ public static Token fromAudience(String... audience) { * Prefer using {@link TenantFeature} qualifier when registering custom {@link TokenCustomizer}. * Use this property only to refer to `TokenCustomizer` implementations provided by this extension. */ - @ConfigItem public Optional customizerName = Optional.empty(); /** @@ -1800,7 +1800,6 @@ public static Token fromAudience(String... audience) { * provider does not have a token introspection endpoint. * This property has no effect when JWT tokens must be verified. */ - @ConfigItem(defaultValueDocumentation = "false") public Optional verifyAccessTokenWithUserInfo = Optional.empty(); public Optional isVerifyAccessTokenWithUserInfo() { @@ -1970,6 +1969,30 @@ public String getAuthorizationScheme() { public void setAuthorizationScheme(String authorizationScheme) { this.authorizationScheme = authorizationScheme; } + + private void addConfigMappingValues(io.quarkus.oidc.runtime.OidcTenantConfig.Token mapping) { + issuer = mapping.issuer(); + audience = mapping.audience(); + subjectRequired = mapping.subjectRequired(); + requiredClaims = mapping.requiredClaims(); + tokenType = mapping.tokenType(); + lifespanGrace = mapping.lifespanGrace(); + age = mapping.age(); + issuedAtRequired = mapping.issuedAtRequired(); + principalClaim = mapping.principalClaim(); + refreshExpired = mapping.refreshExpired(); + refreshTokenTimeSkew = mapping.refreshTokenTimeSkew(); + forcedJwkRefreshInterval = mapping.forcedJwkRefreshInterval(); + header = mapping.header(); + authorizationScheme = mapping.authorizationScheme(); + signatureAlgorithm = mapping.signatureAlgorithm().map(Enum::toString).map(SignatureAlgorithm::valueOf); + decryptionKeyLocation = mapping.decryptionKeyLocation(); + allowJwtIntrospection = mapping.allowJwtIntrospection(); + requireJwtIntrospectionOnly = mapping.requireJwtIntrospectionOnly(); + allowOpaqueTokenIntrospection = mapping.allowOpaqueTokenIntrospection(); + customizerName = mapping.customizerName(); + verifyAccessTokenWithUserInfo = mapping.verifyAccessTokenWithUserInfo(); + } } public static enum ApplicationType { @@ -1997,7 +2020,6 @@ public static enum ApplicationType { /** * Well known OpenId Connect provider identifier */ - @ConfigItem public Optional provider = Optional.empty(); public static enum Provider { @@ -2081,4 +2103,5 @@ public CertificateChain getCertificateChain() { public void setCertificateChain(CertificateChain certificateChain) { this.certificateChain = certificateChain; } + } 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 7e1a7ebf91006..020a3ba2dc80a 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 @@ -37,10 +37,10 @@ public BackChannelLogoutHandler(OidcConfig oidcConfig) { } public void setup(@Observes Router router) { - addRoute(router, oidcConfig.defaultTenant); + addRoute(router, new OidcTenantConfig(oidcConfig.defaultTenant(), OidcUtils.DEFAULT_TENANT_ID)); - for (OidcTenantConfig oidcTenantConfig : oidcConfig.namedTenants.values()) { - addRoute(router, oidcTenantConfig); + for (var nameToOidcTenantConfig : oidcConfig.namedTenants().entrySet()) { + addRoute(router, new OidcTenantConfig(nameToOidcTenantConfig.getValue(), nameToOidcTenantConfig.getKey())); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java index 647bb0ab695ef..9121ba6a73811 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/DefaultTokenIntrospectionUserInfoCache.java @@ -33,8 +33,8 @@ public class DefaultTokenIntrospectionUserInfoCache implements TokenIntrospectio final MemoryCache cache; public DefaultTokenIntrospectionUserInfoCache(OidcConfig oidcConfig, Vertx vertx) { - cache = new MemoryCache(vertx, oidcConfig.tokenCache.cleanUpTimerInterval, - oidcConfig.tokenCache.timeToLive, oidcConfig.tokenCache.maxSize); + cache = new MemoryCache(vertx, oidcConfig.tokenCache().cleanUpTimerInterval(), + oidcConfig.tokenCache().timeToLive(), oidcConfig.tokenCache().maxSize()); } @Override diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java index 7980b899d887e..a92a8ff0f67bf 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcConfig.java @@ -4,68 +4,66 @@ import java.util.Map; import java.util.Optional; -import io.quarkus.oidc.OidcTenantConfig; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigDocSection; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithParentName; -@ConfigRoot(name = "oidc", phase = ConfigPhase.RUN_TIME) -public class OidcConfig { +@ConfigMapping(prefix = "quarkus.oidc") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface OidcConfig { /** * The default tenant. */ - @ConfigItem(name = ConfigItem.PARENT) - public OidcTenantConfig defaultTenant; + @WithParentName + OidcTenantConfig defaultTenant(); /** * Additional named tenants. */ @ConfigDocSection @ConfigDocMapKey("tenant") - @ConfigItem(name = ConfigItem.PARENT) - public Map namedTenants; + @WithParentName + Map namedTenants(); /** * Default TokenIntrospection and UserInfo Cache configuration which is used for all the tenants if it is enabled * with the build-time 'quarkus.oidc.default-token-cache-enabled' property ('true' by default) and also activated, * see its `max-size` property. */ - @ConfigItem - public TokenCache tokenCache = new TokenCache(); + TokenCache tokenCache(); /** * If OIDC tenants should be resolved using the bearer access token's issuer (`iss`) claim value. */ - @ConfigItem(defaultValue = "false") - public boolean resolveTenantsWithIssuer; + @WithDefault("false") + boolean resolveTenantsWithIssuer(); /** * Default TokenIntrospection and UserInfo cache configuration. */ - @ConfigGroup - public static class TokenCache { + interface TokenCache { /** * Maximum number of cache entries. * Set it to a positive value if the cache has to be enabled. */ - @ConfigItem(defaultValue = "0") - public int maxSize = 0; + @WithDefault("0") + int maxSize(); /** * Maximum amount of time a given cache entry is valid for. */ - @ConfigItem(defaultValue = "3M") - public Duration timeToLive = Duration.ofMinutes(3); + @WithDefault("3M") + Duration timeToLive(); /** * Clean up timer interval. * If this property is set then a timer will check and remove the stale entries periodically. */ - @ConfigItem - public Optional cleanUpTimerInterval = Optional.empty(); + Optional cleanUpTimerInterval(); } } 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 ff8b8b326c71c..9dd8de738e6ee 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 @@ -111,19 +111,21 @@ public TenantConfigBean setup(OidcConfig config, Vertx vertxValue, OidcTlsSuppor boolean userInfoInjectionPointDetected) { OidcRecorder.userInfoInjectionPointDetected = userInfoInjectionPointDetected; - String defaultTenantId = config.defaultTenant.getTenantId().orElse(DEFAULT_TENANT_ID); - var defaultTenantInitializer = createStaticTenantContextCreator(vertxValue, config.defaultTenant, - !config.namedTenants.isEmpty(), defaultTenantId, tlsSupport); - TenantConfigContext defaultTenantContext = createStaticTenantContext(vertxValue, config.defaultTenant, - !config.namedTenants.isEmpty(), defaultTenantId, tlsSupport, defaultTenantInitializer); + var defaultTenant = new OidcTenantConfig(config.defaultTenant(), DEFAULT_TENANT_ID); + String defaultTenantId = defaultTenant.getTenantId().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 (Map.Entry tenant : config.namedTenants.entrySet()) { - OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), tenant.getValue().getTenantId()); - var staticTenantInitializer = createStaticTenantContextCreator(vertxValue, tenant.getValue(), false, + for (var tenant : config.namedTenants().entrySet()) { + var namedTenantConfig = new OidcTenantConfig(tenant.getValue(), tenant.getKey()); + OidcCommonUtils.verifyConfigurationId(defaultTenantId, tenant.getKey(), namedTenantConfig.getTenantId()); + var staticTenantInitializer = createStaticTenantContextCreator(vertxValue, namedTenantConfig, false, tenant.getKey(), tlsSupport); staticTenantsConfig.put(tenant.getKey(), - createStaticTenantContext(vertxValue, tenant.getValue(), false, tenant.getKey(), tlsSupport, + createStaticTenantContext(vertxValue, namedTenantConfig, false, tenant.getKey(), tlsSupport, staticTenantInitializer)); } @@ -707,7 +709,7 @@ private TenantSpecificOidcIdentityProvider(String tenantId) { this.blockingExecutor = Arc.container().instance(BlockingSecurityExecutor.class).get(); if (tenantId.equals(DEFAULT_TENANT_ID)) { OidcConfig config = Arc.container().instance(OidcConfig.class).get(); - this.tenantId = config.defaultTenant.getTenantId().orElse(OidcUtils.DEFAULT_TENANT_ID); + this.tenantId = config.defaultTenant().tenantId().orElse(OidcUtils.DEFAULT_TENANT_ID); } else { this.tenantId = tenantId; } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java new file mode 100644 index 0000000000000..bd34006639614 --- /dev/null +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcTenantConfig.java @@ -0,0 +1,1142 @@ +package io.quarkus.oidc.runtime; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.oidc.JavaScriptRequestChecker; +import io.quarkus.oidc.TenantConfigResolver; +import io.quarkus.oidc.TenantFeature; +import io.quarkus.oidc.TokenCustomizer; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.quarkus.oidc.common.runtime.config.OidcClientCommonConfig; +import io.quarkus.oidc.common.runtime.config.OidcCommonConfig; +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.configuration.TrimmedStringConverter; +import io.quarkus.security.identity.SecurityIdentityAugmentor; +import io.smallrye.config.WithConverter; +import io.smallrye.config.WithDefault; + +public interface OidcTenantConfig extends OidcClientCommonConfig { + + /** + * A unique tenant identifier. It can be set by {@code TenantConfigResolver} providers, which + * resolve the tenant configuration dynamically. + */ + Optional tenantId(); + + /** + * If this tenant configuration is enabled. + * + * The default tenant is disabled if it is not configured but + * a {@link TenantConfigResolver} that resolves tenant configurations is registered, + * or named tenants are configured. + * In this case, you do not need to disable the default tenant. + */ + @WithDefault("true") + boolean tenantEnabled(); + + /** + * The application type, which can be one of the following {@link ApplicationType} values. + */ + @ConfigDocDefault("service") + Optional applicationType(); + + /** + * The relative path or absolute URL of the OpenID Connect (OIDC) authorization endpoint, which authenticates + * users. + * You must set this property for `web-app` applications if OIDC discovery is disabled. + * This property is ignored if OIDC discovery is enabled. + */ + Optional authorizationPath(); + + /** + * The relative path or absolute URL of the OIDC UserInfo endpoint. + * You must set this property for `web-app` applications if OIDC discovery is disabled + * and the `authentication.user-info-required` property is enabled. + * This property is ignored if OIDC discovery is enabled. + */ + Optional userInfoPath(); + + /** + * Relative path or absolute URL of the OIDC RFC7662 introspection endpoint which can introspect both opaque and + * JSON Web Token (JWT) tokens. + * This property must be set if OIDC discovery is disabled and 1) the opaque bearer access tokens must be verified + * or 2) JWT tokens must be verified while the cached JWK verification set with no matching JWK is being refreshed. + * This property is ignored if the discovery is enabled. + */ + Optional introspectionPath(); + + /** + * Relative path or absolute URL of the OIDC JSON Web Key Set (JWKS) endpoint which returns a JSON Web Key + * Verification Set. + * This property should be set if OIDC discovery is disabled and the local JWT verification is required. + * This property is ignored if the discovery is enabled. + */ + Optional jwksPath(); + + /** + * Relative path or absolute URL of the OIDC end_session_endpoint. + * This property must be set if OIDC discovery is disabled and RP Initiated Logout support for the `web-app` applications is + * required. + * This property is ignored if the discovery is enabled. + */ + Optional endSessionPath(); + + /** + * The paths which must be secured by this tenant. Tenant with the most specific path wins. + * Please see the xref:security-openid-connect-multitenancy.adoc#configure-tenant-paths[Configure tenant paths] + * section of the OIDC multitenancy guide for explanation of allowed path patterns. + * + * @asciidoclet + */ + Optional> tenantPaths(); + + /** + * The public key for the local JWT token verification. + * OIDC server connection is not created when this property is set. + */ + Optional publicKey(); + + /** + * Introspection Basic Authentication which must be configured only if the introspection is required + * and OpenId Connect Provider does not support the OIDC client authentication configured with + * {@link OidcCommonConfig#credentials} for its introspection endpoint. + */ + IntrospectionCredentials introspectionCredentials(); + + /** + * Introspection Basic Authentication configuration + */ + interface IntrospectionCredentials { + /** + * Name + */ + Optional name(); + + /** + * Secret + */ + Optional secret(); + + /** + * Include OpenId Connect Client ID configured with `quarkus.oidc.client-id`. + */ + @WithDefault("true") + boolean includeClientId(); + + } + + /** + * Configuration to find and parse a custom claim containing the roles information. + */ + Roles roles(); + + /** + * Configuration how to validate the token claims. + */ + Token token(); + + /** + * RP Initiated, BackChannel and FrontChannel Logout configuration + */ + Logout logout(); + + /** + * Configuration of the certificate chain which can be used to verify tokens. + * If the certificate chain truststore is configured, the tokens can be verified using the certificate + * chain inlined in the Base64-encoded format as an `x5c` header in the token itself. + *

+ * The certificate chain inlined in the token is verified. + * Signature of every certificate in the chain but the root certificate is verified by the next certificate in the chain. + * Thumbprint of the root certificate in the chain must match a thumbprint of one of the certificates in the truststore. + *

+ * Additionally, a direct trust in the leaf chain certificate which will be used to verify the token signature must + * be established. + * By default, the leaf certificate's thumbprint must match a thumbprint of one of the certificates in the truststore. + * If the truststore does not have the leaf certificate imported, then the leaf certificate must be identified by its Common + * Name. + */ + CertificateChain certificateChain(); + + interface CertificateChain { + /** + * Common name of the leaf certificate. It must be set if the {@link #trustStoreFile} does not have + * this certificate imported. + * + */ + Optional leafCertificateName(); + + /** + * Truststore file which keeps thumbprints of the trusted certificates. + */ + Optional trustStoreFile(); + + /** + * A parameter to specify the password of the truststore file if it is configured with {@link #trustStoreFile}. + */ + Optional trustStorePassword(); + + /** + * A parameter to specify the alias of the truststore certificate. + */ + Optional trustStoreCertAlias(); + + /** + * An optional parameter to specify type of the truststore file. If not given, the type is automatically + * detected + * based on the file name. + */ + Optional trustStoreFileType(); + + } + + /** + * Different options to configure authorization requests + */ + Authentication authentication(); + + /** + * Authorization code grant configuration + */ + CodeGrant codeGrant(); + + /** + * Default token state manager configuration + */ + TokenStateManager tokenStateManager(); + + /** + * Allow caching the token introspection data. + * Note enabling this property does not enable the cache itself but only permits to cache the token introspection + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable + * it. + */ + @WithDefault("true") + boolean allowTokenIntrospectionCache(); + + /** + * Allow caching the user info data. + * Note enabling this property does not enable the cache itself but only permits to cache the user info data + * for a given tenant. If the default token cache can be used, see {@link OidcConfig.TokenCache} to enable + * it. + */ + @WithDefault("true") + boolean allowUserInfoCache(); + + /** + * Allow inlining UserInfo in IdToken instead of caching it in the token cache. + * This property is only checked when an internal IdToken is generated when OAuth2 providers do not return IdToken. + * Inlining UserInfo in the generated IdToken allows to store it in the session cookie and avoids introducing a cached + * state. + *

+ * Inlining UserInfo in the generated IdToken is enabled if the session cookie is encrypted + * and the UserInfo cache is not enabled or caching UserInfo is disabled for the current tenant + * with the {@link #allowUserInfoCache} property set to `false`. + */ + Optional cacheUserInfoInIdtoken(); + + interface Logout { + + /** + * The relative path of the logout endpoint at the application. If provided, the application is able to + * initiate the + * logout through this endpoint in conformance with the OpenID Connect RP-Initiated Logout specification. + */ + Optional path(); + + /** + * Relative path of the application endpoint where the user should be redirected to after logging out from the + * OpenID + * Connect Provider. + * This endpoint URI must be properly registered at the OpenID Connect Provider as a valid redirect URI. + */ + Optional postLogoutPath(); + + /** + * Name of the post logout URI parameter which is added as a query parameter to the logout redirect URI. + */ + @WithDefault(OidcConstants.POST_LOGOUT_REDIRECT_URI) + String postLogoutUriParam(); + + /** + * Additional properties which is added as the query parameters to the logout redirect URI. + */ + @ConfigDocMapKey("query-parameter-name") + Map extraParams(); + + /** + * Back-Channel Logout configuration + */ + Backchannel backchannel(); + + /** + * Front-Channel Logout configuration + */ + Frontchannel frontchannel(); + } + + interface Backchannel { + /** + * The relative path of the Back-Channel Logout endpoint at the application. + * It must start with the forward slash '/', for example, '/back-channel-logout'. + * This value is always resolved relative to 'quarkus.http.root-path'. + */ + Optional path(); + + /** + * Maximum number of logout tokens that can be cached before they are matched against ID tokens stored in session + * cookies. + */ + @WithDefault("10") + int tokenCacheSize(); + + /** + * Number of minutes a logout token can be cached for. + */ + @WithDefault("10M") + Duration tokenCacheTimeToLive(); + + /** + * Token cache timer interval. + * If this property is set, a timer checks and removes the stale entries periodically. + */ + Optional cleanUpTimerInterval(); + + /** + * Logout token claim whose value is used as a key for caching the tokens. + * Only `sub` (subject) and `sid` (session id) claims can be used as keys. + * Set it to `sid` only if ID tokens issued by the OIDC provider have no `sub` but have `sid` claim. + */ + @WithDefault("sub") + String logoutTokenKey(); + } + + /** + * Configuration for controlling how JsonWebKeySet containing verification keys should be acquired and managed. + */ + Jwks jwks(); + + interface Jwks { + /** + * If JWK verification keys should be fetched at the moment a connection to the OIDC provider + * is initialized. + *

+ * Disabling this property delays the key acquisition until the moment the current token + * has to be verified. Typically it can only be necessary if the token or other telated request properties + * provide an additional context which is required to resolve the keys correctly. + */ + @WithDefault("true") + boolean resolveEarly(); + + /** + * Maximum number of JWK keys that can be cached. + * This property is ignored if the {@link #resolveEarly} property is set to true. + */ + @WithDefault("10") + int cacheSize(); + + /** + * Number of minutes a JWK key can be cached for. + * This property is ignored if the {@link #resolveEarly} property is set to true. + */ + @WithDefault("10M") + Duration cacheTimeToLive(); + + /** + * Cache timer interval. + * If this property is set, a timer checks and removes the stale entries periodically. + * This property is ignored if the {@link #resolveEarly} property is set to true. + */ + Optional cleanUpTimerInterval(); + + /** + * In case there is no key identifier ('kid') or certificate thumbprints ('x5t', 'x5t#S256') specified in the JOSE + * header and no key could be determined, check all available keys matching the token algorithm ('alg') header value. + */ + @WithDefault("false") + boolean tryAll(); + + } + + interface Frontchannel { + /** + * The relative path of the Front-Channel Logout endpoint at the application. + */ + Optional path(); + } + + /** + * Default Authorization Code token state manager configuration + */ + interface TokenStateManager { + + enum Strategy { + /** + * Keep ID, access and refresh tokens. + */ + KEEP_ALL_TOKENS, + + /** + * Keep ID token only + */ + ID_TOKEN, + + /** + * Keep ID and refresh tokens only + */ + ID_REFRESH_TOKENS + } + + /** + * Default TokenStateManager strategy. + */ + @WithDefault("keep_all_tokens") + Strategy strategy(); + + /** + * Default TokenStateManager keeps all tokens (ID, access and refresh) + * returned in the authorization code grant response in a single session cookie by default. + * + * Enable this property to minimize a session cookie size + */ + @WithDefault("false") + boolean splitTokens(); + + /** + * Mandates that the Default TokenStateManager encrypt the session cookie that stores the tokens. + */ + @WithDefault("true") + boolean encryptionRequired(); + + /** + * The secret used by the Default TokenStateManager to encrypt the session cookie + * storing the tokens when {@link #encryptionRequired} property is enabled. + *

+ * If this secret is not set, the client secret configured with + * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` is checked. + * Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication is + * checked. + * The secret is auto-generated every time an application starts if it remains uninitialized after checking all of these + * properties. + * Generated secret can not decrypt the session cookie encrypted before the restart, therefore a user re-authentication + * will be required. + *

+ * The length of the secret used to encrypt the tokens should be at least 32 characters long. + * A warning is logged if the secret length is less than 16 characters. + */ + Optional encryptionSecret(); + + /** + * Supported session cookie key encryption algorithms + */ + enum EncryptionAlgorithm { + /** + * Content encryption key will be generated and encrypted using the A256GCMKW algorithm and the configured + * encryption secret. + * The generated content encryption key will be used to encrypt the session cookie content. + */ + A256GCMKW, + /** + * The configured key encryption secret will be used as the content encryption key to encrypt the session cookie + * content. + * Using the direct encryption avoids a content encryption key generation step and + * will make the encrypted session cookie sequence slightly shorter. + *

+ * Avoid using the direct encryption if the encryption secret is less than 32 characters long. + */ + DIR; + } + + /** + * Session cookie key encryption algorithm + */ + @WithDefault("A256GCMKW") + EncryptionAlgorithm encryptionAlgorithm(); + } + + interface Roles { + + /** + * A list of paths to claims containing an array of groups. + * Each path starts from the top level JWT JSON object + * and can contain multiple segments. + * Each segment represents a JSON object name only; for example: "realm/groups". + * Use double quotes with the namespace-qualified claim names. + * This property can be used if a token has no `groups` claim but has the groups set in one or more different claims. + */ + Optional> roleClaimPath(); + + /** + * The separator for splitting strings that contain multiple group values. + * It is only used if the "role-claim-path" property points to one or more custom claims whose values are strings. + * A single space is used by default because the standard `scope` claim can contain a space-separated sequence. + */ + Optional roleClaimSeparator(); + + /** + * Source of the principal roles. + */ + Optional source(); + + // Source of the principal roles + enum Source { + /** + * ID Token - the default value for the `web-app` applications. + */ + idtoken, + + /** + * Access Token - the default value for the `service` applications; + * can also be used as the source of roles for the `web-app` applications. + */ + accesstoken, + + /** + * User Info + */ + userinfo + } + } + + /** + * Defines the authorization request properties when authenticating + * users using the Authorization Code Grant Type. + */ + interface Authentication { + + /** + * SameSite attribute values for the session cookie. + */ + enum CookieSameSite { + STRICT, + LAX, + NONE + } + + /** + * Authorization code flow response mode + */ + enum ResponseMode { + /** + * Authorization response parameters are encoded in the query string added to the `redirect_uri` + */ + QUERY, + + /** + * Authorization response parameters are encoded as HTML form values that are auto-submitted in the browser + * and transmitted by the HTTP POST method using the application/x-www-form-urlencoded content type + */ + FORM_POST + } + + /** + * Authorization code flow response mode. + */ + @ConfigDocDefault("query") + Optional responseMode(); + + /** + * The relative path for calculating a `redirect_uri` query parameter. + * It has to start from a forward slash and is appended to the request URI's host and port. + * For example, if the current request URI is `https://localhost:8080/service`, a `redirect_uri` parameter + * is set to `https://localhost:8080/` if this property is set to `/` and be the same as the request URI + * if this property has not been configured. + * Note the original request URI is restored after the user has authenticated if `restorePathAfterRedirect` is set + * to `true`. + */ + Optional redirectPath(); + + /** + * If this property is set to `true`, the original request URI which was used before + * the authentication is restored after the user has been redirected back to the application. + * + * Note if `redirectPath` property is not set, the original request URI is restored even if this property is + * disabled. + */ + @WithDefault("false") + boolean restorePathAfterRedirect(); + + /** + * Remove the query parameters such as `code` and `state` set by the OIDC server on the redirect URI + * after the user has authenticated by redirecting a user to the same URI but without the query parameters. + */ + @WithDefault("true") + boolean removeRedirectParameters(); + + /** + * Relative path to the public endpoint which processes the error response from the OIDC authorization + * endpoint. + * If the user authentication has failed, the OIDC provider returns an `error` and an optional + * `error_description` + * parameters, instead of the expected authorization `code`. + * + * If this property is set, the user is redirected to the endpoint which can return a user-friendly + * error description page. It has to start from a forward slash and is appended to the request URI's host and port. + * For example, if it is set as `/error` and the current request URI is + * `https://localhost:8080/callback?error=invalid_scope`, + * a redirect is made to `https://localhost:8080/error?error=invalid_scope`. + * + * If this property is not set, HTTP 401 status is returned in case of the user authentication failure. + */ + Optional errorPath(); + + /** + * Relative path to the public endpoint which an authenticated user is redirected to when the session has expired. + *

+ * When the OIDC session has expired and the session can not be refreshed, a user is redirected + * to the OIDC provider to re-authenticate. The user experience may not be ideal in this case + * as it may not be obvious to the authenticated user why an authentication challenge is returned. + *

+ * Set this property if you would like the user whose session has expired be redirected to a public application specific + * page + * instead, which can inform that the session has expired and advise the user to re-authenticated by following + * a link to the secured initial entry page. + */ + Optional sessionExpiredPath(); + + /** + * Both ID and access tokens are fetched from the OIDC provider as part of the authorization code flow. + *

+ * ID token is always verified on every user request as the primary token which is used + * to represent the principal and extract the roles. + *

+ * Authorization code flow access token is meant to be propagated to downstream services + * and is not verified by default unless `quarkus.oidc.roles.source` property is set to `accesstoken` + * which means the authorization decision is based on the roles extracted from the access token. + *

+ * Authorization code flow access token verification is also enabled if this token is injected as JsonWebToken. + * Set this property to `false` if it is not required. + *

+ * Bearer access token is always verified. + */ + @ConfigDocDefault("true when access token is injected as the JsonWebToken bean, false otherwise") + @WithDefault("false") + boolean verifyAccessToken(); + + /** + * Force `https` as the `redirect_uri` parameter scheme when running behind an SSL/TLS terminating reverse + * proxy. + * This property, if enabled, also affects the logout `post_logout_redirect_uri` and the local redirect requests. + */ + @ConfigDocDefault("false") + Optional forceRedirectHttpsScheme(); + + /** + * List of scopes + */ + Optional> scopes(); + + /** + * The separator which is used when more than one scope is configured. + * A single space is used by default. + */ + Optional scopeSeparator(); + + /** + * Require that ID token includes a `nonce` claim which must match `nonce` authentication request query parameter. + * Enabling this property can help mitigate replay attacks. + * Do not enable this property if your OpenId Connect provider does not support setting `nonce` in ID token + * or if you work with OAuth2 provider such as `GitHub` which does not issue ID tokens. + */ + @WithDefault("false") + boolean nonceRequired(); + + /** + * Add the `openid` scope automatically to the list of scopes. This is required for OpenId Connect providers, + * but does not work for OAuth2 providers such as Twitter OAuth2, which do not accept this scope and throw errors. + */ + @ConfigDocDefault("true") + Optional addOpenidScope(); + + /** + * Additional properties added as query parameters to the authentication redirect URI. + */ + @ConfigDocMapKey("parameter-name") + Map extraParams(); + + /** + * Request URL query parameters which, if present, are added to the authentication redirect URI. + */ + Optional<@WithConverter(TrimmedStringConverter.class) List> forwardParams(); + + /** + * If enabled the state, session, and post logout cookies have their `secure` parameter set to `true` + * when HTTP is used. It might be necessary when running behind an SSL/TLS terminating reverse proxy. + * The cookies are always secure if HTTPS is used, even if this property is set to false. + */ + @WithDefault("false") + boolean cookieForceSecure(); + + /** + * Cookie name suffix. + * For example, a session cookie name for the default OIDC tenant is `q_session` but can be changed to `q_session_test` + * if this property is set to `test`. + */ + Optional cookieSuffix(); + + /** + * Cookie path parameter value which, if set, is used to set a path parameter for the session, state and post + * logout cookies. + * The `cookie-path-header` property, if set, is checked first. + */ + @WithDefault("/") + String cookiePath(); + + /** + * Cookie path header parameter value which, if set, identifies the incoming HTTP header + * whose value is used to set a path parameter for the session, state and post logout cookies. + * If the header is missing, the `cookie-path` property is checked. + */ + Optional cookiePathHeader(); + + /** + * Cookie domain parameter value which, if set, is used for the session, state and post logout cookies. + */ + Optional cookieDomain(); + + /** + * SameSite attribute for the session cookie. + */ + @WithDefault("lax") + CookieSameSite cookieSameSite(); + + /** + * If a state cookie is present, a `state` query parameter must also be present and both the state + * cookie name suffix and state cookie value must match the value of the `state` query parameter when + * the redirect path matches the current path. + * However, if multiple authentications are attempted from the same browser, for example, from the different + * browser tabs, then the currently available state cookie might represent the authentication flow + * initiated from another tab and not related to the current request. + * Disable this property to permit only a single authorization code flow in the same browser. + * + */ + @WithDefault("true") + boolean allowMultipleCodeFlows(); + + /** + * Fail with the HTTP 401 error if the state cookie is present but no state query parameter is present. + *

+ * When either multiple authentications are disabled or the redirect URL + * matches the original request URL, the stale state cookie might remain in the browser cache from + * the earlier failed redirect to an OpenId Connect provider and be visible during the current request. + * For example, if Single-page application (SPA) uses XHR to handle redirects to the provider + * which does not support CORS for its authorization endpoint, the browser blocks it + * and the state cookie created by Quarkus remains in the browser cache. + * Quarkus reports an authentication failure when it detects such an old state cookie but find no matching state + * query parameter. + *

+ * Reporting HTTP 401 error is usually the right thing to do in such cases, it minimizes a risk of the + * browser redirect loop but also can identify problems in the way SPA or Quarkus application manage redirects. + * For example, enabling {@link #javaScriptAutoRedirect} or having the provider redirect to URL configured + * with {@link #redirectPath} might be needed to avoid such errors. + *

+ * However, setting this property to `false` might help if the above options are not suitable. + * It causes a new authentication redirect to OpenId Connect provider. Doing so might increase the + * risk of browser redirect loops. + */ + @WithDefault("false") + boolean failOnMissingStateParam(); + + /** + * If this property is set to `true`, an OIDC UserInfo endpoint is called. + *

+ * This property is enabled automatically if `quarkus.oidc.roles.source` is set to `userinfo` + * or `quarkus.oidc.token.verify-access-token-with-user-info` is set to `true` + * or `quarkus.oidc.authentication.id-token-required` is set to `false`, + * the current OIDC tenant must support a UserInfo endpoint in these cases. + *

+ * It is also enabled automatically if `io.quarkus.oidc.UserInfo` injection point is detected but only + * if the current OIDC tenant supports a UserInfo endpoint. + */ + @ConfigDocDefault("true when UserInfo bean is injected, false otherwise") + Optional userInfoRequired(); + + /** + * Session age extension in minutes. + * The user session age property is set to the value of the ID token life-span by default and + * the user is redirected to the OIDC provider to re-authenticate once the session has expired. + * If this property is set to a nonzero value, then the expired ID token can be refreshed before + * the session has expired. + * This property is ignored if the `token.refresh-expired` property has not been enabled. + */ + @WithDefault("5M") + Duration sessionAgeExtension(); + + /** + * State cookie age in minutes. + * State cookie is created every time a new authorization code flow redirect starts + * and removed when this flow is completed. + * State cookie name is unique by default, see {@link #allowMultipleCodeFlows}. + * Keep its age to the reasonable minimum value such as 5 minutes or less. + */ + @WithDefault("5M") + Duration stateCookieAge(); + + /** + * If this property is set to `true`, a normal 302 redirect response is returned + * if the request was initiated by a JavaScript API such as XMLHttpRequest or Fetch and the current user needs to be + * (re)authenticated, which might not be desirable for Single-page applications (SPA) since + * it automatically following the redirect might not work given that OIDC authorization endpoints typically do not + * support + * CORS. + *

+ * If this property is set to `false`, a status code of `499` is returned to allow + * SPA to handle the redirect manually if a request header identifying current request as a JavaScript request is found. + * `X-Requested-With` request header with its value set to either `JavaScript` or `XMLHttpRequest` is expected by + * default if + * this property is enabled. You can register a custom {@linkplain JavaScriptRequestChecker} to do a custom JavaScript + * request check instead. + */ + @WithDefault("true") + boolean javaScriptAutoRedirect(); + + /** + * Requires that ID token is available when the authorization code flow completes. + * Disable this property only when you need to use the authorization code flow with OAuth2 providers which do not return + * ID token - an internal IdToken is generated in such cases. + */ + @ConfigDocDefault("true") + Optional idTokenRequired(); + + /** + * Internal ID token lifespan. + * This property is only checked when an internal IdToken is generated when Oauth2 providers do not return IdToken. + */ + @ConfigDocDefault("5M") + Optional internalIdTokenLifespan(); + + /** + * Requires that a Proof Key for Code Exchange (PKCE) is used. + */ + @ConfigDocDefault("false") + Optional pkceRequired(); + + /** + * Secret used to encrypt a Proof Key for Code Exchange (PKCE) code verifier in the code flow state. + * This secret should be at least 32 characters long. + * + * @deprecated This field is deprecated. Use {@link #stateSecret} instead. + * + */ + @Deprecated(forRemoval = true) + Optional pkceSecret(); + + /** + * Secret used to encrypt Proof Key for Code Exchange (PKCE) code verifier and/or nonce in the code flow + * state. + * This secret should be at least 32 characters long. + *

+ * If this secret is not set, the client secret configured with + * either `quarkus.oidc.credentials.secret` or `quarkus.oidc.credentials.client-secret.value` is checked. + * Finally, `quarkus.oidc.credentials.jwt.secret` which can be used for `client_jwt_secret` authentication is + * checked. A client secret is not be used as a state encryption secret if it is less than 32 characters + * long. + *

+ * The secret is auto-generated if it remains uninitialized after checking all of these properties. + *

+ * Error is reported if the secret length is less than 16 characters. + */ + Optional stateSecret(); + + } + + /** + * Authorization Code grant configuration + */ + interface CodeGrant { + + /** + * Additional parameters, in addition to the required `code` and `redirect-uri` parameters, + * which must be included to complete the authorization code grant request. + */ + @ConfigDocMapKey("parameter-name") + Map extraParams(); + + /** + * Custom HTTP headers which must be sent to complete the authorization code grant request. + */ + @ConfigDocMapKey("header-name") + Map headers(); + + } + + /** + * Supported asymmetric signature algorithms + */ + enum SignatureAlgorithm { + RS256, + RS384, + RS512, + PS256, + PS384, + PS512, + ES256, + ES384, + ES512, + EDDSA; + + private static String EDDSA_ALG = "EDDSA"; + private static String REQUIRED_EDDSA_ALG = "EdDSA"; + + public String getAlgorithm() { + String name = name(); + return EDDSA_ALG.equals(name) ? REQUIRED_EDDSA_ALG : name; + } + } + + interface Token { + + /** + * The expected issuer `iss` claim value. + * This property overrides the `issuer` property, which might be set in OpenId Connect provider's well-known + * configuration. + * If the `iss` claim value varies depending on the host, IP address, or tenant id of the provider, you can skip the + * issuer verification by setting this property to `any`, but it should be done only when other options (such as + * configuring + * the provider to use the fixed `iss` claim value) are not possible. + */ + Optional issuer(); + + /** + * The expected audience `aud` claim value, which can be a string or an array of strings. + * + * Note the audience claim is verified for ID tokens by default. + * ID token audience must be equal to the value of `quarkus.oidc.client-id` property. + * Use this property to override the expected value if your OpenID Connect provider + * sets a different audience claim value in ID tokens. Set it to `any` if your provider + * does not set ID token audience` claim. + * + * Audience verification for access tokens is only done if this property is configured. + */ + Optional> audience(); + + /** + * Require that the token includes a `sub` (subject) claim which is a unique + * and never reassigned identifier for the current user. + * Note that if you enable this property and if UserInfo is also required, + * both the token and UserInfo `sub` claims must be present and match each other. + */ + @WithDefault("false") + boolean subjectRequired(); + + /** + * A map of required claims and their expected values. + * For example, `quarkus.oidc.token.required-claims.org_id = org_xyz` would require tokens to have the `org_id` claim to + * be present and set to `org_xyz`. + * Strings are the only supported types. Use {@linkplain SecurityIdentityAugmentor} to verify claims of other types or + * complex claims. + */ + @ConfigDocMapKey("claim-name") + Map requiredClaims(); + + /** + * Expected token type + */ + Optional tokenType(); + + /** + * Life span grace period in seconds. + * When checking token expiry, current time is allowed to be later than token expiration time by at most the configured + * number of seconds. + * When checking token issuance, current time is allowed to be sooner than token issue time by at most the configured + * number of seconds. + */ + OptionalInt lifespanGrace(); + + /** + * Token age. + * + * It allows for the number of seconds to be specified that must not elapse since the `iat` (issued at) time. + * A small leeway to account for clock skew which can be configured with `quarkus.oidc.token.lifespan-grace` to verify + * the token expiry time + * can also be used to verify the token age property. + * + * Note that setting this property does not relax the requirement that Bearer and Code Flow JWT tokens + * must have a valid (`exp`) expiry claim value. The only exception where setting this property relaxes the requirement + * is when a logout token is sent with a back-channel logout request since the current + * OpenId Connect Back-Channel specification does not explicitly require the logout tokens to contain an `exp` claim. + * However, even if the current logout token is allowed to have no `exp` claim, the `exp` claim is still verified + * if the logout token contains it. + */ + Optional age(); + + /** + * Require that the token includes a `iat` (issued at) claim + * + * Set this property to `false` if your JWT token does not contain an `iat` (issued at) claim. + * Note that ID token is always required to have an `iat` claim and therefore this property has no impact on the ID + * token verification process. + */ + @WithDefault("true") + boolean issuedAtRequired(); + + /** + * Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub` + * claims are + * checked. + */ + Optional principalClaim(); + + /** + * Refresh expired authorization code flow ID or access tokens. + * If this property is enabled, a refresh token request is performed if the authorization code + * ID or access token has expired and, if successful, the local session is updated with the new set of tokens. + * Otherwise, the local session is invalidated and the user redirected to the OpenID Provider to re-authenticate. + * In this case, the user might not be challenged again if the OIDC provider session is still active. + * + * For this option be effective the `authentication.session-age-extension` property should also be set to a nonzero + * value since the refresh token is currently kept in the user session. + * + * This option is valid only when the application is of type {@link ApplicationType#WEB_APP}}. + * + * This property is enabled if `quarkus.oidc.token.refresh-token-time-skew` is configured, + * you do not need to enable this property manually in this case. + */ + @WithDefault("false") + boolean refreshExpired(); + + /** + * The refresh token time skew, in seconds. + * If this property is enabled, the configured number of seconds is added to the current time + * when checking if the authorization code ID or access token should be refreshed. + * If the sum is greater than the authorization code ID or access token's expiration time, a refresh is going to + * happen. + */ + Optional refreshTokenTimeSkew(); + + /** + * The forced JWK set refresh interval in minutes. + */ + @WithDefault("10M") + Duration forcedJwkRefreshInterval(); + + /** + * Custom HTTP header that contains a bearer token. + * This option is valid only when the application is of type {@link ApplicationType#SERVICE}}. + */ + Optional header(); + + /** + * HTTP Authorization header scheme. + */ + @WithDefault(OidcConstants.BEARER_SCHEME) + String authorizationScheme(); + + /** + * Required signature algorithm. + * OIDC providers support many signature algorithms but if necessary you can restrict + * Quarkus application to accept tokens signed only using an algorithm configured with this property. + */ + Optional signatureAlgorithm(); + + /** + * Decryption key location. + * JWT tokens can be inner-signed and encrypted by OpenId Connect providers. + * However, it is not always possible to remotely introspect such tokens because + * the providers might not control the private decryption keys. + * In such cases set this property to point to the file containing the decryption private key in + * PEM or JSON Web Key (JWK) format. + * If this property is not set and the `private_key_jwt` client authentication method is used, the private key + * used to sign the client authentication JWT tokens are also used to decrypt the encrypted ID tokens. + */ + Optional decryptionKeyLocation(); + + /** + * Allow the remote introspection of JWT tokens when no matching JWK key is available. + * + * This property is set to `true` by default for backward-compatibility reasons. It is planned that this default value + * will be changed to `false` in an upcoming release. + * + * Also note this property is ignored if JWK endpoint URI is not available and introspecting the tokens is + * the only verification option. + */ + @WithDefault("true") + boolean allowJwtIntrospection(); + + /** + * Require that JWT tokens are only introspected remotely. + * + */ + @WithDefault("false") + boolean requireJwtIntrospectionOnly(); + + /** + * Allow the remote introspection of the opaque tokens. + * + * Set this property to `false` if only JWT tokens are expected. + */ + @WithDefault("true") + boolean allowOpaqueTokenIntrospection(); + + /** + * Token customizer name. + * + * Allows to select a tenant specific token customizer as a named bean. + * Prefer using {@link TenantFeature} qualifier when registering custom {@link TokenCustomizer}. + * Use this property only to refer to `TokenCustomizer` implementations provided by this extension. + */ + Optional customizerName(); + + /** + * Indirectly verify that the opaque (binary) access token is valid by using it to request UserInfo. + * Opaque access token is considered valid if the provider accepted this token and returned a valid UserInfo. + * You should only enable this option if the opaque access tokens must be accepted but OpenId Connect + * provider does not have a token introspection endpoint. + * This property has no effect when JWT tokens must be verified. + */ + @ConfigDocDefault("false") + Optional verifyAccessTokenWithUserInfo(); + + } + + enum ApplicationType { + /** + * A {@code WEB_APP} is a client that serves pages, usually a front-end application. For this type of client the + * Authorization Code Flow is defined as the preferred method for authenticating users. + */ + WEB_APP, + + /** + * A {@code SERVICE} is a client that has a set of protected HTTP resources, usually a backend application following the + * RESTful Architectural Design. For this type of client, the Bearer Authorization method is defined as the preferred + * method for authenticating and authorizing users. + */ + SERVICE, + + /** + * A combined {@code SERVICE} and {@code WEB_APP} client. + * For this type of client, the Bearer Authorization method is used if the Authorization header is set + * and Authorization Code Flow - if not. + */ + HYBRID + } + + /** + * Well known OpenId Connect provider identifier + */ + Optional provider(); + + enum Provider { + APPLE, + DISCORD, + FACEBOOK, + GITHUB, + GOOGLE, + LINKEDIN, + MASTODON, + MICROSOFT, + SLACK, + SPOTIFY, + STRAVA, + TWITCH, + TWITTER, + // New name for Twitter + X + } + +} diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java new file mode 100644 index 0000000000000..b71efbcb8f78c --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java @@ -0,0 +1,1238 @@ +package io.quarkus.oidc.runtime; + +import java.nio.file.Path; +import java.time.Duration; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.runtime.configuration.TrimmedStringConverter; +import io.smallrye.config.WithConverter; + +final class OidcTenantConfigImpl implements OidcTenantConfig { + + private final String tenantId; + + OidcTenantConfigImpl() { + this.tenantId = null; + } + + OidcTenantConfigImpl(String tenantId) { + this.tenantId = tenantId; + } + + enum ConfigMappingMethods { + AUTH_SERVER_URL, + DISCOVERY_ENABLED, + REGISTRATION_PATH, + CONNECTION_DELAY, + CONNECTION_RETRY_COUNT, + CONNECTION_TIMEOUT, + USE_BLOCKING_DNS_LOOKUP, + MAX_POOL_SIZE, + FOLLOW_REDIRECTS, + PROXY, + PROXY_HOST, + PROXY_PORT, + PROXY_USERNAME, + PROXY_PASSWORD, + TLS, + TLS_CONFIGURATION, + TLS_VERIFICATION, + TLS_KEYSTORE_FILE, + TLS_KEYSTORE_FILE_TYPE, + TLS_KEYSTORE_PROVIDER, + TLS_KEYSTORE_PASSWORD, + TLS_KEYSTORE_KEY_ALIAS, + TLS_KEYSTORE_KEY_PASSWORD, + TLS_TRUSTSTORE_PASSWORD, + TLS_TRUSTSTORE_FILE, + TLS_TRUSTSTORE_CERT_ALIAS, + TLS_TRUSTSTORE_FILE_TYPE, + TLS_TRUSTSTORE_PROVIDER, + TOKEN_PATH, + REVOKE_PATH, + CLIENT_ID, + CLIENT_NAME, + CREDENTIALS, + CREDENTIALS_SECRET, + CREDENTIALS_CLIENT_SECRET, + CREDENTIALS_CLIENT_SECRET_VALUE, + CREDENTIALS_CLIENT_SECRET_PROVIDER, + CREDENTIALS_CLIENT_SECRET_METHOD, + CREDENTIALS_CLIENT_SECRET_PROVIDER_NAME, + CREDENTIALS_CLIENT_SECRET_PROVIDER_KEYRING_NAME, + CREDENTIALS_CLIENT_SECRET_PROVIDER_KEY, + CREDENTIALS_JWT, + CREDENTIALS_JWT_SOURCE, + CREDENTIALS_JWT_SECRET, + CREDENTIALS_JWT_SECRET_PROVIDER, + CREDENTIALS_JWT_SECRET_PROVIDER_NAME, + CREDENTIALS_JWT_SECRET_PROVIDER_KEYRING_NAME, + CREDENTIALS_JWT_SECRET_PROVIDER_KEY, + CREDENTIALS_JWT_KEY, + CREDENTIALS_JWT_KEY_FILE, + CREDENTIALS_JWT_KEY_STORE_FILE, + CREDENTIALS_JWT_KEY_STORE_PASSWORD, + CREDENTIALS_JWT_KEY_ID, + CREDENTIALS_JWT_KEY_PASSWORD, + CREDENTIALS_JWT_ISSUER, + CREDENTIALS_JWT_SUBJECT, + CREDENTIALS_JWT_CLAIMS, + CREDENTIALS_JWT_SIGNATURE_ALGORITHM, + CREDENTIALS_JWT_LIFESPAN, + CREDENTIALS_JWT_ASSERTION, + CREDENTIALS_JWT_AUDIENCE, + CREDENTIALS_JWT_TOKEN_ID, + PROVIDER, + JWKS, + CACHE_USER_INFO_ID_TOKEN, + ALLOW_USER_INFO_CACHE, + ALLOW_TOKEN_INTROSPECTION_CACHE, + TOKEN_STATE_MANAGER, + CODE_GRANT, + AUTHENTICATION, + CERTIFICATION_CHAIN, + LOGOUT, + TOKEN, + ROLES, + INTROSPECTION_CREDENTIALS, + PUBLIC_KEY, + TENANT_PATHS, + END_SESSION_PATH, + JWKS_PATH, + INTROSPECTION_PATH, + USER_INFO_PATH, + AUTHORIZATION_PATH, + APPLICATION_TYPE, + TENANT_ENABLED, + TOKEN_STATE_MANAGER_ENCRYPTION_ALGORITHM, + TOKEN_STATE_MANAGER_ENCRYPTION_SECRET, + TOKEN_STATE_MANAGER_ENCRYPTION_REQUIRED, + TOKEN_STATE_MANAGER_SPLIT_TOKENS, + TOKEN_STATE_MANAGER_STRATEGY, + JWKS_RESOLVE_EARLY, + JWKS_CACHE_SIZE, + JWKS_CACHE_TIME_TO_LIVE, + JWKS_CLEAN_UP_TIMER_INTERVAL, + JWKS_TRY_ALL, + CODE_GRANT_EXTRA_PARAMS, + CODE_GRANT_HEADERS, + AUTHENTICATION_RESPONSE_MODE, + AUTHENTICATION_REDIRECT_PATH, + AUTHENTICATION_RESTORE_PATH_AFTER_REDIRECT, + AUTHENTICATION_REMOVE_REDIRECT_PARAMETERS, + AUTHENTICATION_ERROR_PATH, + AUTHENTICATION_SESSION_EXPIRED_PATH, + AUTHENTICATION_VERIFY_ACCESS_TOKEN, + AUTHENTICATION_FORCED_REDIRECT_HTTPS_SCHEME, + AUTHENTICATION_SCOPES, + AUTHENTICATION_SCOPE_SEPARATOR, + AUTHENTICATION_NONCE_REQUIRED, + AUTHENTICATION_ADD_OPENID_SCOPE, + AUTHENTICATION_EXTRA_PARAMS, + AUTHENTICATION_FORWARD_PARAMS, + AUTHENTICATION_COOKIE_FORCE_SECURE, + AUTHENTICATION_COOKIE_SUFFIX, + AUTHENTICATION_COOKIE_PATH, + AUTHENTICATION_COOKIE_PATH_HEADER, + AUTHENTICATION_COOKIE_DOMAIN, + AUTHENTICATION_COOKIE_SAME_SITE, + AUTHENTICATION_ALLOW_MULTIPLE_CODE_FLOWS, + AUTHENTICATION_FAIL_ON_MISSING_STATE_PARAM, + AUTHENTICATION_USER_INFO_REQUIRED, + AUTHENTICATION_SESSION_AGE_EXTENSION, + AUTHENTICATION_STATE_COOKIE_AGE, + AUTHENTICATION_JAVASCRIPT_AUTO_REDIRECT, + AUTHENTICATION_ID_TOKEN_REQUIRED, + AUTHENTICATION_INTERNAL_ID_TOKEN_LIFESPAN, + AUTHENTICATION_PKCE_REQUIRED, + AUTHENTICATION_PKCE_SECRET, + AUTHENTICATION_STATE_SECRET, + CERTIFICATION_CHAIN_LEAF_CERTIFICATE_NAME, + CERTIFICATION_CHAIN_TRUST_STORE_FILE, + CERTIFICATION_CHAIN_TRUST_STORE_PASSWORD, + CERTIFICATION_CHAIN_TRUST_STORE_CERT_ALIAS, + CERTIFICATION_CHAIN_TRUST_STORE_FILE_TYPE, + LOGOUT_PATH, + LOGOUT_POST_LOGOUT_PATH, + LOGOUT_POST_LOGOUT_URI_PARAM, + LOGOUT_POST_LOGOUT_EXTRA_PARAMS, + LOGOUT_POST_LOGOUT_BACK_CHANNEL, + LOGOUT_POST_LOGOUT_FRONT_CHANNEL, + LOGOUT_POST_LOGOUT_FRONT_CHANNEL_PATH, + LOGOUT_POST_LOGOUT_BACK_CHANNEL_PATH, + LOGOUT_POST_LOGOUT_BACK_CHANNEL_TOKEN_CACHE_SIZE, + LOGOUT_POST_LOGOUT_BACK_CHANNEL_TOKEN_CACHE_TTL, + LOGOUT_POST_LOGOUT_BACK_CHANNEL_CLEAN_UP_TIMER_INTERVAL, + LOGOUT_POST_LOGOUT_BACK_CHANNEL_LOGOUT_TOKEN_KEY, + TOKEN_ISSUER, + TOKEN_AUDIENCE, + TOKEN_SUBJECT_REQUIRED, + TOKEN_REQUIRED_CLAIMS, + TOKEN_TOKEN_TYPE, + TOKEN_LIFESPAN_GRACE, + TOKEN_AGE, + TOKEN_ISSUED_AT_REQUIRED, + TOKEN_PRINCIPAL_CLAIM, + TOKEN_REFRESH_EXPIRED, + TOKEN_REFRESH_TOKEN_TIME_SKEW, + TOKEN_FORCED_JWK_REFRESH_INTERNAL, + TOKEN_HEADER, + TOKEN_AUTHORIZATION_SCHEME, + TOKEN_SIGNATURE_ALGORITHM, + TOKEN_DECRYPTION_KEY_LOCATION, + TOKEN_ALLOW_JWT_INTROSPECTION, + TOKEN_REQUIRE_JWT_INTROSPECTION_ONLY, + TOKEN_ALLOW_OPAQUE_TOKEN_INTROSPECTION, + TOKEN_CUSTOMIZER_NAME, + TOKEN_VERIFY_ACCESS_TOKEN_WITH_USER_INFO, + ROLES_ROLE_CLAIM_PATH, + ROLES_ROLE_CLAIM_SEPARATOR, + ROLES_SOURCE, + INTROSPECTION_CREDENTIALS_NAME, + INTROSPECTION_CREDENTIALS_SECRET, + INTROSPECTION_CREDENTIALS_INCLUDE_CLIENT_ID, + TENANT_ID + } + + final Map invocationsRecorder = new EnumMap<>(ConfigMappingMethods.class); + + @Override + public Optional tenantId() { + invocationsRecorder.put(ConfigMappingMethods.TENANT_ID, true); + return Optional.ofNullable(tenantId); + } + + @Override + public boolean tenantEnabled() { + invocationsRecorder.put(ConfigMappingMethods.TENANT_ENABLED, true); + return false; + } + + @Override + public Optional applicationType() { + invocationsRecorder.put(ConfigMappingMethods.APPLICATION_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional authorizationPath() { + invocationsRecorder.put(ConfigMappingMethods.AUTHORIZATION_PATH, true); + return Optional.empty(); + } + + @Override + public Optional userInfoPath() { + invocationsRecorder.put(ConfigMappingMethods.USER_INFO_PATH, true); + return Optional.empty(); + } + + @Override + public Optional introspectionPath() { + invocationsRecorder.put(ConfigMappingMethods.INTROSPECTION_PATH, true); + return Optional.empty(); + } + + @Override + public Optional jwksPath() { + invocationsRecorder.put(ConfigMappingMethods.JWKS_PATH, true); + return Optional.empty(); + } + + @Override + public Optional endSessionPath() { + invocationsRecorder.put(ConfigMappingMethods.END_SESSION_PATH, true); + return Optional.empty(); + } + + @Override + public Optional> tenantPaths() { + invocationsRecorder.put(ConfigMappingMethods.TENANT_PATHS, true); + return Optional.empty(); + } + + @Override + public Optional publicKey() { + invocationsRecorder.put(ConfigMappingMethods.PUBLIC_KEY, true); + return Optional.empty(); + } + + @Override + public IntrospectionCredentials introspectionCredentials() { + invocationsRecorder.put(ConfigMappingMethods.INTROSPECTION_CREDENTIALS, true); + return new IntrospectionCredentials() { + @Override + public Optional name() { + invocationsRecorder.put(ConfigMappingMethods.INTROSPECTION_CREDENTIALS_NAME, true); + return Optional.empty(); + } + + @Override + public Optional secret() { + invocationsRecorder.put(ConfigMappingMethods.INTROSPECTION_CREDENTIALS_SECRET, true); + return Optional.empty(); + } + + @Override + public boolean includeClientId() { + invocationsRecorder.put(ConfigMappingMethods.INTROSPECTION_CREDENTIALS_INCLUDE_CLIENT_ID, true); + return false; + } + }; + } + + @Override + public Roles roles() { + invocationsRecorder.put(ConfigMappingMethods.ROLES, true); + return new Roles() { + @Override + public Optional> roleClaimPath() { + invocationsRecorder.put(ConfigMappingMethods.ROLES_ROLE_CLAIM_PATH, true); + return Optional.empty(); + } + + @Override + public Optional roleClaimSeparator() { + invocationsRecorder.put(ConfigMappingMethods.ROLES_ROLE_CLAIM_SEPARATOR, true); + return Optional.empty(); + } + + @Override + public Optional source() { + invocationsRecorder.put(ConfigMappingMethods.ROLES_SOURCE, true); + return Optional.empty(); + } + }; + } + + @Override + public Token token() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN, true); + return new Token() { + @Override + public Optional issuer() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_ISSUER, true); + return Optional.empty(); + } + + @Override + public Optional> audience() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_AUDIENCE, true); + return Optional.empty(); + } + + @Override + public boolean subjectRequired() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_SUBJECT_REQUIRED, true); + return false; + } + + @Override + public Map requiredClaims() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_REQUIRED_CLAIMS, true); + return Map.of(); + } + + @Override + public Optional tokenType() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_TOKEN_TYPE, true); + return Optional.empty(); + } + + @Override + public OptionalInt lifespanGrace() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_LIFESPAN_GRACE, true); + return OptionalInt.empty(); + } + + @Override + public Optional age() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_AGE, true); + return Optional.empty(); + } + + @Override + public boolean issuedAtRequired() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_ISSUED_AT_REQUIRED, true); + return false; + } + + @Override + public Optional principalClaim() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_PRINCIPAL_CLAIM, true); + return Optional.empty(); + } + + @Override + public boolean refreshExpired() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_REFRESH_EXPIRED, true); + return false; + } + + @Override + public Optional refreshTokenTimeSkew() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_REFRESH_TOKEN_TIME_SKEW, true); + return Optional.empty(); + } + + @Override + public Duration forcedJwkRefreshInterval() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_FORCED_JWK_REFRESH_INTERNAL, true); + return null; + } + + @Override + public Optional header() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_HEADER, true); + return Optional.empty(); + } + + @Override + public String authorizationScheme() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_AUTHORIZATION_SCHEME, true); + return ""; + } + + @Override + public Optional signatureAlgorithm() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_SIGNATURE_ALGORITHM, true); + return Optional.empty(); + } + + @Override + public Optional decryptionKeyLocation() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_DECRYPTION_KEY_LOCATION, true); + return Optional.empty(); + } + + @Override + public boolean allowJwtIntrospection() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_ALLOW_JWT_INTROSPECTION, true); + return false; + } + + @Override + public boolean requireJwtIntrospectionOnly() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_REQUIRE_JWT_INTROSPECTION_ONLY, true); + return false; + } + + @Override + public boolean allowOpaqueTokenIntrospection() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_ALLOW_OPAQUE_TOKEN_INTROSPECTION, true); + return false; + } + + @Override + public Optional customizerName() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_CUSTOMIZER_NAME, true); + return Optional.empty(); + } + + @Override + public Optional verifyAccessTokenWithUserInfo() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_VERIFY_ACCESS_TOKEN_WITH_USER_INFO, true); + return Optional.empty(); + } + }; + } + + @Override + public Logout logout() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT, true); + return new Logout() { + @Override + public Optional path() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_PATH, true); + return Optional.empty(); + } + + @Override + public Optional postLogoutPath() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_PATH, true); + return Optional.empty(); + } + + @Override + public String postLogoutUriParam() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_URI_PARAM, true); + return ""; + } + + @Override + public Map extraParams() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_EXTRA_PARAMS, true); + return Map.of(); + } + + @Override + public Backchannel backchannel() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_BACK_CHANNEL, true); + return new Backchannel() { + @Override + public Optional path() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_BACK_CHANNEL_PATH, true); + return Optional.empty(); + } + + @Override + public int tokenCacheSize() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_BACK_CHANNEL_TOKEN_CACHE_SIZE, true); + return 0; + } + + @Override + public Duration tokenCacheTimeToLive() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_BACK_CHANNEL_TOKEN_CACHE_TTL, true); + return null; + } + + @Override + public Optional cleanUpTimerInterval() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_BACK_CHANNEL_CLEAN_UP_TIMER_INTERVAL, + true); + return Optional.empty(); + } + + @Override + public String logoutTokenKey() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_BACK_CHANNEL_LOGOUT_TOKEN_KEY, true); + return ""; + } + }; + } + + @Override + public Frontchannel frontchannel() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_FRONT_CHANNEL, true); + return new Frontchannel() { + @Override + public Optional path() { + invocationsRecorder.put(ConfigMappingMethods.LOGOUT_POST_LOGOUT_FRONT_CHANNEL_PATH, true); + return Optional.empty(); + } + }; + } + }; + } + + @Override + public CertificateChain certificateChain() { + invocationsRecorder.put(ConfigMappingMethods.CERTIFICATION_CHAIN, true); + return new CertificateChain() { + @Override + public Optional leafCertificateName() { + invocationsRecorder.put(ConfigMappingMethods.CERTIFICATION_CHAIN_LEAF_CERTIFICATE_NAME, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.CERTIFICATION_CHAIN_TRUST_STORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional trustStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.CERTIFICATION_CHAIN_TRUST_STORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreCertAlias() { + invocationsRecorder.put(ConfigMappingMethods.CERTIFICATION_CHAIN_TRUST_STORE_CERT_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.CERTIFICATION_CHAIN_TRUST_STORE_FILE_TYPE, true); + return Optional.empty(); + } + }; + } + + @Override + public Authentication authentication() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION, true); + return new Authentication() { + @Override + public Optional responseMode() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_RESPONSE_MODE, true); + return Optional.empty(); + } + + @Override + public Optional redirectPath() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_REDIRECT_PATH, true); + return Optional.empty(); + } + + @Override + public boolean restorePathAfterRedirect() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_RESTORE_PATH_AFTER_REDIRECT, true); + return false; + } + + @Override + public boolean removeRedirectParameters() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_REMOVE_REDIRECT_PARAMETERS, true); + return false; + } + + @Override + public Optional errorPath() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_ERROR_PATH, true); + return Optional.empty(); + } + + @Override + public Optional sessionExpiredPath() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_SESSION_EXPIRED_PATH, true); + return Optional.empty(); + } + + @Override + public boolean verifyAccessToken() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_VERIFY_ACCESS_TOKEN, true); + return false; + } + + @Override + public Optional forceRedirectHttpsScheme() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_FORCED_REDIRECT_HTTPS_SCHEME, true); + return Optional.empty(); + } + + @Override + public Optional> scopes() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_SCOPES, true); + return Optional.empty(); + } + + @Override + public Optional scopeSeparator() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_SCOPE_SEPARATOR, true); + return Optional.empty(); + } + + @Override + public boolean nonceRequired() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_NONCE_REQUIRED, true); + return false; + } + + @Override + public Optional addOpenidScope() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_ADD_OPENID_SCOPE, true); + return Optional.empty(); + } + + @Override + public Map extraParams() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_EXTRA_PARAMS, true); + return Map.of(); + } + + @Override + public Optional<@WithConverter(TrimmedStringConverter.class) List> forwardParams() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_FORWARD_PARAMS, true); + return Optional.empty(); + } + + @Override + public boolean cookieForceSecure() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_COOKIE_FORCE_SECURE, true); + return false; + } + + @Override + public Optional cookieSuffix() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_COOKIE_SUFFIX, true); + return Optional.empty(); + } + + @Override + public String cookiePath() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_COOKIE_PATH, true); + return ""; + } + + @Override + public Optional cookiePathHeader() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_COOKIE_PATH_HEADER, true); + return Optional.empty(); + } + + @Override + public Optional cookieDomain() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_COOKIE_DOMAIN, true); + return Optional.empty(); + } + + @Override + public CookieSameSite cookieSameSite() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_COOKIE_SAME_SITE, true); + return CookieSameSite.LAX; + } + + @Override + public boolean allowMultipleCodeFlows() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_ALLOW_MULTIPLE_CODE_FLOWS, true); + return false; + } + + @Override + public boolean failOnMissingStateParam() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_FAIL_ON_MISSING_STATE_PARAM, true); + return false; + } + + @Override + public Optional userInfoRequired() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_USER_INFO_REQUIRED, true); + return Optional.empty(); + } + + @Override + public Duration sessionAgeExtension() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_SESSION_AGE_EXTENSION, true); + return null; + } + + @Override + public Duration stateCookieAge() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_STATE_COOKIE_AGE, true); + return null; + } + + @Override + public boolean javaScriptAutoRedirect() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_JAVASCRIPT_AUTO_REDIRECT, true); + return false; + } + + @Override + public Optional idTokenRequired() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_ID_TOKEN_REQUIRED, true); + return Optional.empty(); + } + + @Override + public Optional internalIdTokenLifespan() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_INTERNAL_ID_TOKEN_LIFESPAN, true); + return Optional.empty(); + } + + @Override + public Optional pkceRequired() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_PKCE_REQUIRED, true); + return Optional.empty(); + } + + @Override + public Optional pkceSecret() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_PKCE_SECRET, true); + return Optional.empty(); + } + + @Override + public Optional stateSecret() { + invocationsRecorder.put(ConfigMappingMethods.AUTHENTICATION_STATE_SECRET, true); + return Optional.empty(); + } + }; + } + + @Override + public CodeGrant codeGrant() { + invocationsRecorder.put(ConfigMappingMethods.CODE_GRANT, true); + return new CodeGrant() { + @Override + public Map extraParams() { + invocationsRecorder.put(ConfigMappingMethods.CODE_GRANT_EXTRA_PARAMS, true); + return Map.of(); + } + + @Override + public Map headers() { + invocationsRecorder.put(ConfigMappingMethods.CODE_GRANT_HEADERS, true); + return Map.of(); + } + }; + } + + @Override + public TokenStateManager tokenStateManager() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_STATE_MANAGER, true); + return new TokenStateManager() { + @Override + public Strategy strategy() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_STATE_MANAGER_STRATEGY, true); + return Strategy.ID_TOKEN; + } + + @Override + public boolean splitTokens() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_STATE_MANAGER_SPLIT_TOKENS, true); + return false; + } + + @Override + public boolean encryptionRequired() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_STATE_MANAGER_ENCRYPTION_REQUIRED, true); + return false; + } + + @Override + public Optional encryptionSecret() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_STATE_MANAGER_ENCRYPTION_SECRET, true); + return Optional.empty(); + } + + @Override + public EncryptionAlgorithm encryptionAlgorithm() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_STATE_MANAGER_ENCRYPTION_ALGORITHM, true); + return EncryptionAlgorithm.A256GCMKW; + } + }; + } + + @Override + public boolean allowTokenIntrospectionCache() { + invocationsRecorder.put(ConfigMappingMethods.ALLOW_TOKEN_INTROSPECTION_CACHE, true); + return false; + } + + @Override + public boolean allowUserInfoCache() { + invocationsRecorder.put(ConfigMappingMethods.ALLOW_USER_INFO_CACHE, true); + return false; + } + + @Override + public Optional cacheUserInfoInIdtoken() { + invocationsRecorder.put(ConfigMappingMethods.CACHE_USER_INFO_ID_TOKEN, true); + return Optional.empty(); + } + + @Override + public Jwks jwks() { + invocationsRecorder.put(ConfigMappingMethods.JWKS, true); + return new Jwks() { + @Override + public boolean resolveEarly() { + invocationsRecorder.put(ConfigMappingMethods.JWKS_RESOLVE_EARLY, true); + return false; + } + + @Override + public int cacheSize() { + invocationsRecorder.put(ConfigMappingMethods.JWKS_CACHE_SIZE, true); + return 0; + } + + @Override + public Duration cacheTimeToLive() { + invocationsRecorder.put(ConfigMappingMethods.JWKS_CACHE_TIME_TO_LIVE, true); + return null; + } + + @Override + public Optional cleanUpTimerInterval() { + invocationsRecorder.put(ConfigMappingMethods.JWKS_CLEAN_UP_TIMER_INTERVAL, true); + return Optional.empty(); + } + + @Override + public boolean tryAll() { + invocationsRecorder.put(ConfigMappingMethods.JWKS_TRY_ALL, true); + return false; + } + }; + } + + @Override + public Optional provider() { + invocationsRecorder.put(ConfigMappingMethods.PROVIDER, true); + return Optional.empty(); + } + + @Override + public Optional tokenPath() { + invocationsRecorder.put(ConfigMappingMethods.TOKEN_PATH, true); + return Optional.empty(); + } + + @Override + public Optional revokePath() { + invocationsRecorder.put(ConfigMappingMethods.REVOKE_PATH, true); + return Optional.empty(); + } + + @Override + public Optional clientId() { + invocationsRecorder.put(ConfigMappingMethods.CLIENT_ID, true); + return Optional.empty(); + } + + @Override + public Optional clientName() { + invocationsRecorder.put(ConfigMappingMethods.CLIENT_NAME, true); + return Optional.empty(); + } + + @Override + public Credentials credentials() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS, true); + return new Credentials() { + @Override + public Optional secret() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_SECRET, true); + return Optional.empty(); + } + + @Override + public Secret clientSecret() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET, true); + return new Secret() { + + @Override + public Optional value() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_VALUE, true); + return Optional.empty(); + } + + @Override + public Provider provider() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER, true); + return new Provider() { + @Override + public Optional name() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER_NAME, true); + return Optional.empty(); + } + + @Override + public Optional keyringName() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER_KEYRING_NAME, + true); + return Optional.empty(); + } + + @Override + public Optional key() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_PROVIDER_KEY, true); + return Optional.empty(); + } + }; + } + + @Override + public Optional method() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_CLIENT_SECRET_METHOD, true); + return Optional.empty(); + } + }; + } + + @Override + public Jwt jwt() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT, true); + return new Jwt() { + @Override + public Source source() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SOURCE, true); + return Source.BEARER; + } + + @Override + public Optional secret() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET, true); + return Optional.empty(); + } + + @Override + public Provider secretProvider() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER, true); + return new Provider() { + @Override + public Optional name() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER_NAME, true); + return Optional.empty(); + } + + @Override + public Optional keyringName() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER_KEYRING_NAME, + true); + return Optional.empty(); + } + + @Override + public Optional key() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SECRET_PROVIDER_KEY, true); + return Optional.empty(); + } + }; + } + + @Override + public Optional key() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY, true); + return Optional.empty(); + } + + @Override + public Optional keyFile() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_STORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_STORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional keyId() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_ID, true); + return Optional.empty(); + } + + @Override + public Optional keyPassword() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEY_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional audience() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_AUDIENCE, true); + return Optional.empty(); + } + + @Override + public Optional tokenKeyId() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_TOKEN_ID, true); + return Optional.empty(); + } + + @Override + public Optional issuer() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_ISSUER, true); + return Optional.empty(); + } + + @Override + public Optional subject() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SUBJECT, true); + return Optional.empty(); + } + + @Override + public Map claims() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_CLAIMS, true); + return Map.of(); + } + + @Override + public Optional signatureAlgorithm() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_SIGNATURE_ALGORITHM, true); + return Optional.empty(); + } + + @Override + public int lifespan() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_LIFESPAN, true); + return 0; + } + + @Override + public boolean assertion() { + invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_ASSERTION, true); + return false; + } + }; + } + }; + } + + @Override + public Optional authServerUrl() { + invocationsRecorder.put(ConfigMappingMethods.AUTH_SERVER_URL, true); + return Optional.empty(); + } + + @Override + public Optional discoveryEnabled() { + invocationsRecorder.put(ConfigMappingMethods.DISCOVERY_ENABLED, true); + return Optional.empty(); + } + + @Override + public Optional registrationPath() { + invocationsRecorder.put(ConfigMappingMethods.REGISTRATION_PATH, true); + return Optional.empty(); + } + + @Override + public Optional connectionDelay() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_DELAY, true); + return Optional.empty(); + } + + @Override + public int connectionRetryCount() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_RETRY_COUNT, true); + return 0; + } + + @Override + public Duration connectionTimeout() { + invocationsRecorder.put(ConfigMappingMethods.CONNECTION_TIMEOUT, true); + return null; + } + + @Override + public boolean useBlockingDnsLookup() { + invocationsRecorder.put(ConfigMappingMethods.USE_BLOCKING_DNS_LOOKUP, true); + return false; + } + + @Override + public OptionalInt maxPoolSize() { + invocationsRecorder.put(ConfigMappingMethods.MAX_POOL_SIZE, true); + return OptionalInt.empty(); + } + + @Override + public boolean followRedirects() { + invocationsRecorder.put(ConfigMappingMethods.FOLLOW_REDIRECTS, true); + return false; + } + + @Override + public Proxy proxy() { + invocationsRecorder.put(ConfigMappingMethods.PROXY, true); + return new Proxy() { + @Override + public Optional host() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_HOST, true); + return Optional.empty(); + } + + @Override + public int port() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_PORT, true); + return 0; + } + + @Override + public Optional username() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_USERNAME, true); + return Optional.empty(); + } + + @Override + public Optional password() { + invocationsRecorder.put(ConfigMappingMethods.PROXY_PASSWORD, true); + return Optional.empty(); + } + }; + } + + @Override + public Tls tls() { + invocationsRecorder.put(ConfigMappingMethods.TLS, true); + return new Tls() { + @Override + public Optional tlsConfigurationName() { + invocationsRecorder.put(ConfigMappingMethods.TLS_CONFIGURATION, true); + return Optional.empty(); + } + + @Override + public Optional verification() { + invocationsRecorder.put(ConfigMappingMethods.TLS_VERIFICATION, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_FILE_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreProvider() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_PROVIDER, true); + return Optional.empty(); + } + + @Override + public Optional keyStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreKeyAlias() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_KEY_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional keyStoreKeyPassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_KEYSTORE_KEY_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFile() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_FILE, true); + return Optional.empty(); + } + + @Override + public Optional trustStorePassword() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_PASSWORD, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreCertAlias() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_CERT_ALIAS, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreFileType() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_FILE_TYPE, true); + return Optional.empty(); + } + + @Override + public Optional trustStoreProvider() { + invocationsRecorder.put(ConfigMappingMethods.TLS_TRUSTSTORE_PROVIDER, true); + return Optional.empty(); + } + }; + } +} diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigTest.java new file mode 100644 index 0000000000000..98e8167489b0e --- /dev/null +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigTest.java @@ -0,0 +1,39 @@ +package io.quarkus.oidc.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +import io.quarkus.oidc.OidcTenantConfig; + +public class OidcTenantConfigTest { + + @Test + public void testCopyBetweenConfigMappingAndClass() { + var configMappingGroup = new OidcTenantConfigImpl(); + new OidcTenantConfig(configMappingGroup, "tested-later"); + for (var configMappingMethod : EnumSet.allOf(OidcTenantConfigImpl.ConfigMappingMethods.class)) { + Boolean invoked = configMappingGroup.invocationsRecorder.get(configMappingMethod); + assertTrue(invoked != null && invoked, + "OidcTenantConfig method '%s' return value is not copied from interface to class" + .formatted(configMappingMethod)); + } + } + + @Test + public void testTenantIdConfiguration() { + // prefer tenant id configured by user + var configMappingGroup = new OidcTenantConfigImpl("user-defined-value"); + var tenantConfig = new OidcTenantConfig(configMappingGroup, "fallback"); + assertEquals("user-defined-value", tenantConfig.getTenantId().get()); + + // fallback to tenant key + configMappingGroup = new OidcTenantConfigImpl(); + tenantConfig = new OidcTenantConfig(configMappingGroup, "tenant-key"); + assertEquals("tenant-key", tenantConfig.getTenantId().get()); + } + +} diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/TokenIntrospectionCacheTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/TokenIntrospectionCacheTest.java index 4f14689b62f71..8817359258867 100644 --- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/TokenIntrospectionCacheTest.java +++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/TokenIntrospectionCacheTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import java.time.Duration; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -46,8 +48,13 @@ public Boolean call() throws Exception { } private static OidcConfig createOidcConfig() { - OidcConfig cfg = new OidcConfig(); - cfg.tokenCache.maxSize = 2; - return cfg; + record OidcConfigImpl(OidcTenantConfig defaultTenant, Map namedTenants, TokenCache tokenCache, + boolean resolveTenantsWithIssuer) implements OidcConfig { + } + record TokenCacheImpl(int maxSize, Duration timeToLive, + Optional cleanUpTimerInterval) implements OidcConfig.TokenCache { + } + var tokenCache = new TokenCacheImpl(2, Duration.ofMinutes(3), Optional.empty()); + return new OidcConfigImpl(null, Map.of(), tokenCache, false); } } diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java index c258e1299b7d3..715ddfa489883 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/TenantSpecificJwtPreferredNameValidator.java @@ -22,7 +22,8 @@ public class TenantSpecificJwtPreferredNameValidator implements Validator { @Override public String validate(JwtContext jwtContext) throws MalformedClaimException { // verify that normal scoped validator is created when the runtime config is ready - if (!"quarkus-app-b".equals(oidcConfig.namedTenants.get("tenant-requiredclaim").token.requiredClaims.get("azp"))) { + if (!"quarkus-app-b" + .equals(oidcConfig.namedTenants().get("tenant-requiredclaim").token().requiredClaims().get("azp"))) { throw new IllegalStateException("The 'tenant-requiredclaim' tenant required claim 'azp' is not 'quarkus-app-b'"); } diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java index 44c48d9909304..7a888ee8217e0 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/OidcEventResource.java @@ -16,7 +16,7 @@ public class OidcEventResource { private final String expectedAuthServerUrl; public OidcEventResource(OidcEventObserver oidcEventObserver, OidcConfig oidcConfig) { - this.expectedAuthServerUrl = dropTrailingSlash(oidcConfig.defaultTenant.authServerUrl.get()); + this.expectedAuthServerUrl = dropTrailingSlash(oidcConfig.defaultTenant().authServerUrl().get()); this.oidcEventObserver = oidcEventObserver; }