diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index ccf4b972f2a861..130f0edf649836 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -938,6 +938,16 @@
quarkus-rest-client-oidc-filter-deployment
${project.version}
+
+ io.quarkus
+ quarkus-oidc-client-registration
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-oidc-client-registration-deployment
+ ${project.version}
+
io.quarkus
quarkus-oidc-client-graphql
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java
index 58e502a49d66eb..497136eab4c929 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java
@@ -73,6 +73,7 @@ public enum Feature {
OBSERVABILITY,
OIDC,
OIDC_CLIENT,
+ OIDC_CLIENT_REGISTRATION,
RESTEASY_CLIENT_OIDC_FILTER,
REST_CLIENT_OIDC_FILTER,
OIDC_CLIENT_GRAPHQL_CLIENT_INTEGRATION,
diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml
index 32cf89bc60284c..59f92096d05414 100644
--- a/devtools/bom-descriptor-json/pom.xml
+++ b/devtools/bom-descriptor-json/pom.xml
@@ -1799,6 +1799,19 @@
+
+ io.quarkus
+ quarkus-oidc-client-registration
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
io.quarkus
quarkus-oidc-common
diff --git a/docs/pom.xml b/docs/pom.xml
index 9c26e51cd450dc..ee82231a6665aa 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -1810,6 +1810,19 @@
+
+ io.quarkus
+ quarkus-oidc-client-registration-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
io.quarkus
quarkus-oidc-common-deployment
diff --git a/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc
new file mode 100644
index 00000000000000..0bb4a77c617428
--- /dev/null
+++ b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc
@@ -0,0 +1,513 @@
+////
+This guide is maintained in the main Quarkus repository
+and pull requests should be submitted there:
+https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
+////
+= OpenID Connect (OIDC) and OAuth2 dynamic client registration
+include::_attributes.adoc[]
+:diataxis-type: reference
+:categories: security
+:topics: security,oidc,client
+:extensions: io.quarkus:quarkus-oidc-client-registration
+
+Typically, you have to register an OIDC client (application) manually in your OIDC provider's dashboard.
+During this process, you specify the human readable application name, allowed redirect and post logout URLs, and other properties.
+After the registration has been completed, you copy the generated client id and secret to your Quarkus OIDC application properties.
+
+OpenID Connect and OAuth2 dynamic client registration allows you to register OIDC clients dynamically, and manage individual client registrations.
+You can read more about it in the https://openid.net/specs/openid-connect-registration-1_0.html[OIDC client registration] and https://datatracker.ietf.org/doc/html/rfc7592[OAuth2 Dynamic Client Registration Management Protocol] specification documents.
+
+You can use Quarkus `quarkus-oidc-client-registration` extension to register one or more clients using OIDC client registration configurations and read, update and delete metadata of the registered clients.
+
+xref:security-openid-connect-multitenancy#tenant-config-resolver[OIDC TenantConfigResolver] can be used to create OIDC tenant configurations using the metadata of the registered clients.
+
+[IMPORTANT]
+====
+Currently, Quarkus `quarkus-oidc-client-registration` extension has an `experimental` status.
+Dynamic client registration API provided by this extension may change while this extension has an experiemental status.
+====
+
+== OIDC Client Registration
+
+Add the following dependency:
+
+[source,xml]
+----
+
+ io.quarkus
+ quarkus-oidc-client-registration
+
+----
+
+The `quarkus-oidc-client-registration` extension allows register one or more clients using OIDC client registration configurations, either on start-up or on demand, and read, update and delete metadata of the registered clients.
+
+You can register and manage client registrations from the custom xref:security-openid-connect-multitenancy#tenant-config-resolver[OIDC TenantConfigResolver].
+
+Alternatively, you can register clients without even using OIDC. For example, it can be a command line tool which registers clients and passes metadata of the registered clients to Quarkus services which require them.
+
+Each OIDC client registration configuration represents an OIDC client registration endpoint which can accept many individual client registrations.
+
+[[register-clients-on-startup]]
+=== Register clients on start-up
+
+You start with declaring one or more OIDC client registration configurations, for example:
+
+[source,properties]
+----
+# Default OIDC client registration which auto-discovers a standard client registration endpoint.
+# It does not require an initial registration token.
+
+quarkus.oidc-client-registration.auth-server-url=${quarkus.oidc.auth-server-url}
+quarkus.oidc-client-registration.metadata.client-name=Default Client
+quarkus.oidc-client-registration.metadata.redirect-uri=http://localhost:8081/protected
+
+# Named OIDC client registration which configures a registration endpoint path:
+# It require an initial registration token for a client registration to succeed.
+
+quarkus.oidc-client-registration.tenant-client.registration-path=${quarkus.oidc.auth-server-url}/clients-registrations/openid-connect
+quarkus.oidc-client-registration.tenant-client.metadata.client-name=Tenant Client
+quarkus.oidc-client-registration.tenant-client.metadata.redirect-uri=http://localhost:8081/protected/tenant
+quarkus.oidc-client-registration.initial-token=${initial-registration-token}
+----
+
+The above configuration will lead to two new client registrations created in your OIDC provider.
+
+You or may not need to acquire an initial registration access token. If you don't, then you will have to enable one or more client registration policies in your OIDC provider's dashboard. For example, see https://www.keycloak.org/docs/latest/securing_apps/#_client_registration_policies[Keycloak client registration policies].
+
+The next step is to inject either `quarkus.oidc.client.registration.OidcClientRegistration` if only a single default client registration is done, or `quarkus.oidc.client.registration.OidcClientRegistrations` if more than one registration is configured, and use metadata of the clients registered with these configurations.
+
+For example:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
+import io.quarkus.oidc.TenantConfigResolver;
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.quarkus.oidc.client.registration.OidcClientRegistrations;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class CustomTenantConfigResolver implements TenantConfigResolver {
+
+ @Inject
+ OidcClientRegistration clientReg;
+
+ @Inject
+ OidcClientRegistrations clientRegs;
+
+ @Override
+ public Uni resolve(RoutingContext routingContext,
+ OidcRequestContext requestContext) {
+
+ if (routingContext.request().path().endsWith("/protected")) {
+ // Use the registered client created from the default OIDC client registration
+ return clientReg.registeredClient().onItem().transform(client -> createTenantConfig("registered-client", client));
+ } else if (routingContext.request().path().endsWith("/protected/tenant")) {
+ // Use the registered client created from the named 'tenant-client' OIDC client registration
+ OidcClientRegistration tenantClientReg = clientRegs.getClientRegistration("tenant-client");
+ return tenantClientReg.registeredClient().onItem().transform(client -> createTenantConfig("registered-client-tenant", client));
+ }
+ return null;
+ }
+
+ // Convert metadata of registered clients to OidcTenantConfig
+ private OidcTenantConfig createTenantConfig(String tenantId, RegisteredClient client) {
+ ClientMetadata metadata = client.getMetadata();
+
+ OidcTenantConfig oidcConfig = new OidcTenantConfig();
+ oidcConfig.setTenantId(tenantId);
+ oidcConfig.setAuthServerUrl(authServerUrl);
+ oidcConfig.setApplicationType(ApplicationType.WEB_APP);
+ oidcConfig.setClientName(metadata.getClientName());
+ oidcConfig.setClientId(metadata.getClientId());
+ oidcConfig.getCredentials().setSecret(metadata.getClientSecret());
+ String redirectUri = metadata.getRedirectUris().get(0);
+ oidcConfig.getAuthentication().setRedirectPath(URI.create(redirectUri).getPath());
+ return oidcConfig;
+ }
+}
+----
+
+[[register-clients-on-demand]]
+=== Register clients on demand
+
+You can register new clients on demand.
+You can add new clients to the existing, already configured `OidcClientConfiguration` or to a newly created `OidcClientConfiguration`.
+
+Start from configuring one or more OIDC client registrations:
+
+[source,properties]
+----
+quarkus.oidc-client-registration.auth-server-url=${quarkus.oidc.auth-server-url}
+----
+
+The above configuration is sufficient for registering new clients using this configuration. For example:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
+import io.quarkus.oidc.TenantConfigResolver;
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class CustomTenantConfigResolver implements TenantConfigResolver {
+
+ @Inject
+ OidcClientRegistration clientReg;
+
+ @Inject
+ @ConfigProperty(name = "quarkus.oidc.auth-server-url")
+ String authServerUrl;
+
+
+ @Override
+ public Uni resolve(RoutingContext routingContext,
+ OidcRequestContext requestContext) {
+ if (routingContext.request().path().endsWith("/protected/oidc-client-reg-existing-config")) {
+ // New client registration done dynamically at the request time using the configured client registration
+ ClientMetadata metadata = createMetadata("http://localhost:8081/protected/dynamic-tenant",
+ "Dynamic Tenant Client");
+
+ return clientReg.registerClient(metadata).onItem().transform(r ->
+ createTenantConfig("registered-client-dynamically", r));
+ }
+ return null;
+ }
+
+ // Create metadata of registered clients to OidcTenantConfig
+ private OidcTenantConfig createTenantConfig(String tenantId, ClientMetadata metadata) {
+ OidcTenantConfig oidcConfig = new OidcTenantConfig();
+ oidcConfig.setTenantId(tenantId);
+ oidcConfig.setAuthServerUrl(authServerUrl);
+ oidcConfig.setApplicationType(ApplicationType.WEB_APP);
+ oidcConfig.setClientName(metadata.getClientName());
+ oidcConfig.setClientId(metadata.getClientId());
+ oidcConfig.getCredentials().setSecret(metadata.getClientSecret());
+ String redirectUri = metadata.getRedirectUris().get(0);
+ oidcConfig.getAuthentication().setRedirectPath(URI.create(redirectUri).getPath());
+ return oidcConfig;
+ }
+
+ protected static ClientMetadata createMetadata(String redirectUri, String clientName) {
+ return ClientMetadata.builder()
+ .setRedirectUri(redirectUri)
+ .setClientName(clientName)
+ .build();
+ }
+}
+----
+
+Alternatively, you can use `OidcClientRegistrations` to prepare a new `OidcClientRegistration` and use `OidcClientRegistration` to register a client. For example:
+
+The above configuration is sufficient for registering new clients using this configuration. For example:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
+import io.quarkus.oidc.TenantConfigResolver;
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrations;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class CustomTenantConfigResolver implements TenantConfigResolver {
+
+ @Inject
+ OidcClientRegistrations clientRegs;
+
+ @Inject
+ @ConfigProperty(name = "quarkus.oidc.auth-server-url")
+ String authServerUrl;
+
+ @Override
+ public Uni resolve(RoutingContext routingContext,
+ OidcRequestContext requestContext) {
+ if (routingContext.request().path().endsWith("/protected/new-oidc-client-reg")) {
+ // New client registration done dynamically at the request time
+
+ OidcClientRegistrationConfig clientRegConfig = new OidcClientRegistrationConfig();
+ clientRegConfig.auth-server-url = Optional.of(authServerUrl);
+ clientRegConfig.metadata.redirectUri = Optional.of("http://localhost:8081/protected/new-oidc-client-reg");
+ clientRegConfig.metadata.clientName = Optional.of("Dynamic Client");
+
+ return clientRegs.newClientRegistration(clientRegConfig)
+ .onItem().transform(reg ->
+ createTenantConfig("registered-client-dynamically", reg.registeredClient());
+ }
+
+ return null;
+ }
+
+ // Create metadata of registered clients to OidcTenantConfig
+ private OidcTenantConfig createTenantConfig(String tenantId, ClientMetadata metadata) {
+ OidcTenantConfig oidcConfig = new OidcTenantConfig();
+ oidcConfig.setTenantId(tenantId);
+ oidcConfig.setAuthServerUrl(authServerUrl);
+ oidcConfig.setApplicationType(ApplicationType.WEB_APP);
+ oidcConfig.setClientName(metadata.getClientName());
+ oidcConfig.setClientId(metadata.getClientId());
+ oidcConfig.getCredentials().setSecret(metadata.getClientSecret());
+ String redirectUri = metadata.getRedirectUris().get(0);
+ oidcConfig.getAuthentication().setRedirectPath(URI.create(redirectUri).getPath());
+ return oidcConfig;
+ }
+
+ protected static ClientMetadata createMetadata(String redirectUri, String clientName) {
+ return ClientMetadata.builder()
+ .setRedirectUri(redirectUri)
+ .setClientName(clientName)
+ .build();
+ }
+}
+----
+
+[[managing-registered-clients]]
+=== Managing registered clients
+
+`io.quarkus.oidc.client.registration.RegisteredClient` represents a registered client and can be used to read and update its metadata.
+It can also be used to delete this client.
+
+For example:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.TenantConfigResolver;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.quarkus.runtime.StartupEvent;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class CustomTenantConfigResolver implements TenantConfigResolver {
+
+ @Inject
+ OidcClientRegistration clientReg;
+
+ RegisteredClient registeredClient;
+
+ void onStartup(@Observes StartupEvent event) {
+
+ // Default OIDC client registration, client has already been registered at startup, `await()` will return immediately.
+ registeredClient = clientReg.registeredClient().await().indefinitely();
+
+ // Read the latest client metadata
+ registeredClient = registeredClient.read().await().indefinitely();
+ }
+
+ @Override
+ public Uni resolve(RoutingContext routingContext,
+ OidcRequestContext requestContext) {
+
+ if (routingContext.request().path().endsWith("/protected")) {
+ // Use the registered client created from the default OIDC client registration
+
+ return createTenantConfig("registered-client", registeredClient));
+ }
+ return null;
+ }
+
+ // Convert metadata of registered clients to OidcTenantConfig
+ private OidcTenantConfig createTenantConfig(String tenantId, RegisteredClient client) {
+ ClientMetadata metadata = client.getMetadata();
+
+ OidcTenantConfig oidcConfig = new OidcTenantConfig();
+ oidcConfig.setTenantId(tenantId);
+ oidcConfig.setAuthServerUrl(authServerUrl);
+ oidcConfig.setApplicationType(ApplicationType.WEB_APP);
+ oidcConfig.setClientName(metadata.getClientName());
+ oidcConfig.setClientId(metadata.getClientId());
+ oidcConfig.getCredentials().setSecret(metadata.getClientSecret());
+ String redirectUri = metadata.getRedirectUris().get(0);
+ oidcConfig.getAuthentication().setRedirectPath(URI.create(redirectUri).getPath());
+ return oidcConfig;
+ }
+}
+----
+
+[[avoiding-duplicate-registrations]]
+=== Avoiding duplicate registrations
+
+When you register clients in startup, as described in the <> section, you will most likely want to avoid creating duplicate registrations after a restart.
+
+In this case, you should configure OIDC client registration to perform the registration at the request time, as opposed to at the startup time:
+
+[source,properties]
+----
+quarkus.oidc-client-registration.register-early=false
+----
+
+The next thing you should do is to persist the already registered client's registration URI and registration token at the shutdown time, you can get them from the `io.quarkus.oidc.client.registration.RegisteredClient` instance.
+
+Finally, at the startup time, you should restore the already registered client instead of registering it again.
+
+For example:
+
+[source,java]
+----
+package io.quarkus.it.keycloak;
+
+
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.TenantConfigResolver;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.quarkus.runtime.ShutdownEvent;
+import io.quarkus.runtime.StartupEvent;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class CustomTenantConfigResolver implements TenantConfigResolver {
+
+ @Inject
+ OidcClientRegistration clientReg;
+
+ RegisteredClient registeredClient;
+
+ void onStartup(@Observes StartupEvent event) {
+
+ String registrationUri = readRegistrationUriFromDatabase("Registered Client");
+ String registrationToken = readRegistrationTokenFromDatabase("Registered Client");
+
+ if (registrationUri != null && registrationToken != null) {
+ // Read an already registered client
+ registeredClient = clientReg.readClient(registrationUri, registrationToken).await().indefinitely();
+ } else {
+ // Register a new client
+ registeredClient = clientReg.registeredClient().await().indefinitely();
+ }
+
+ }
+
+ void onShutdown(@Observes ShutdownEvent event) {
+
+ saveRegistrationUriToDatabase("Registered Client", registeredClient.registrationUri());
+ saveRegistrationTokenToDatabase("Registered Client", registeredClient.registrationToken());
+
+ }
+
+ String readRegistrationUriFromDatabase(String clientName) {
+ // implementation is not shown for brevity
+ }
+ String readRegistrationTokenFromDatabase(String clientName) {
+ // implementation is not shown for brevity
+ }
+ void saveRegistrationUriToDatabase(String clientName, String registrationUri) {
+ // implementation is not shown for brevity
+ }
+ void saveRegistrationTokenToDatabase(String clientName, String registrationToken) {
+ // implementation is not shown for brevity
+ }
+
+ @Override
+ public Uni resolve(RoutingContext routingContext,
+ OidcRequestContext requestContext) {
+
+ if (routingContext.request().path().endsWith("/protected")) {
+ // Use the registered client created from the default OIDC client registration
+
+ return createTenantConfig("registered-client", registeredClient));
+ }
+ return null;
+ }
+
+ // Convert metadata of registered clients to OidcTenantConfig
+ private OidcTenantConfig createTenantConfig(String tenantId, RegisteredClient client) {
+ ClientMetadata metadata = client.getMetadata();
+
+ OidcTenantConfig oidcConfig = new OidcTenantConfig();
+ oidcConfig.setTenantId(tenantId);
+ oidcConfig.setAuthServerUrl(authServerUrl);
+ oidcConfig.setApplicationType(ApplicationType.WEB_APP);
+ oidcConfig.setClientName(metadata.getClientName());
+ oidcConfig.setClientId(metadata.getClientId());
+ oidcConfig.getCredentials().setSecret(metadata.getClientSecret());
+ String redirectUri = metadata.getRedirectUris().get(0);
+ oidcConfig.getAuthentication().setRedirectPath(URI.create(redirectUri).getPath());
+ return oidcConfig;
+ }
+}
+----
+
+If you register clients dynamically, on demand, as described in the <> section, the problem of the duplicate client registration should not arise.
+You can persist the already registered client's registration URI and registration token if necessary though and check them too to avoid any duplicate reservation risk.
+
+[[configuration-reference]]
+== Configuration reference
+
+include::{generated-dir}/config/quarkus-oidc-client-registration.adoc[opts=optional, leveloffset=+1]
+
+== References
+
+* https://openid.net/specs/openid-connect-registration-1_0.html[OIDC client registration]
+* https://datatracker.ietf.org/doc/html/rfc7592[OAuth2 Dynamic Client Registration Management Protocol]
+* https://www.keycloak.org/docs/latest/securing_apps/#_client_registration[Keycloak Dynamic Client Registration Service]
+* xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication]
+* xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications]
+* xref:security-overview.adoc[Quarkus Security overview]
diff --git a/extensions/oidc-client-registration/deployment/pom.xml b/extensions/oidc-client-registration/deployment/pom.xml
new file mode 100644
index 00000000000000..e4720ae98627a8
--- /dev/null
+++ b/extensions/oidc-client-registration/deployment/pom.xml
@@ -0,0 +1,96 @@
+
+
+
+ quarkus-oidc-client-registration-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-oidc-client-registration-deployment
+ Quarkus - OpenID Connect Dynamic Client Registration - Deployment
+
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-vertx-deployment
+
+
+ io.quarkus
+ quarkus-oidc-client-registration
+
+
+ io.quarkus
+ quarkus-oidc-common-deployment
+
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+ default-compile
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+ -AlegacyConfigRoot=true
+
+
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+
+
+
+ test-keycloak
+
+
+ test-containers
+
+
+
+
+
+ maven-surefire-plugin
+
+ false
+
+ ${keycloak.docker.legacy.image}
+ false
+
+
+
+
+
+
+
+
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
new file mode 100644
index 00000000000000..f0d71493b33d20
--- /dev/null
+++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java
@@ -0,0 +1,67 @@
+package io.quarkus.oidc.client.registration.deployment;
+
+import java.util.function.BooleanSupplier;
+
+import jakarta.inject.Singleton;
+
+import io.quarkus.arc.BeanDestroyer;
+import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
+import io.quarkus.deployment.Feature;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.annotations.ExecutionTime;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrations;
+import io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationRecorder;
+import io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationsConfig;
+import io.quarkus.tls.TlsRegistryBuildItem;
+import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
+
+@BuildSteps(onlyIf = OidcClientRegistrationBuildStep.IsEnabled.class)
+public class OidcClientRegistrationBuildStep {
+
+ @BuildStep
+ ExtensionSslNativeSupportBuildItem enableSslInNative() {
+ return new ExtensionSslNativeSupportBuildItem(Feature.OIDC_CLIENT_REGISTRATION);
+ }
+
+ @Record(ExecutionTime.RUNTIME_INIT)
+ @BuildStep
+ public void setup(
+ OidcClientRegistrationsConfig oidcConfig,
+ OidcClientRegistrationRecorder recorder,
+ CoreVertxBuildItem vertxBuildItem,
+ TlsRegistryBuildItem tlsRegistry,
+ BuildProducer syntheticBean) {
+
+ OidcClientRegistrations oidcClientRegistrations = recorder.setup(oidcConfig, vertxBuildItem.getVertx(),
+ tlsRegistry.registry());
+
+ syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClientRegistration.class).unremovable()
+ .types(OidcClientRegistration.class)
+ .supplier(recorder.createOidcClientRegistrationBean(oidcClientRegistrations))
+ .scope(Singleton.class)
+ .setRuntimeInit()
+ .destroyer(BeanDestroyer.CloseableDestroyer.class)
+ .done());
+
+ syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClientRegistrations.class).unremovable()
+ .types(OidcClientRegistrations.class)
+ .supplier(recorder.createOidcClientRegistrationsBean(oidcClientRegistrations))
+ .scope(Singleton.class)
+ .setRuntimeInit()
+ .destroyer(BeanDestroyer.CloseableDestroyer.class)
+ .done());
+ }
+
+ public static class IsEnabled implements BooleanSupplier {
+ OidcClientRegistrationBuildTimeConfig config;
+
+ public boolean getAsBoolean() {
+ 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
new file mode 100644
index 00000000000000..98d78afa9acf14
--- /dev/null
+++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java
@@ -0,0 +1,16 @@
+package io.quarkus.oidc.client.registration.deployment;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigRoot;
+
+/**
+ * Build time configuration for OIDC client registration.
+ */
+@ConfigRoot
+public class OidcClientRegistrationBuildTimeConfig {
+ /**
+ * If the OIDC client registration extension is enabled.
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean enabled;
+}
diff --git a/extensions/oidc-client-registration/pom.xml b/extensions/oidc-client-registration/pom.xml
new file mode 100644
index 00000000000000..0ccf96d1d9c775
--- /dev/null
+++ b/extensions/oidc-client-registration/pom.xml
@@ -0,0 +1,19 @@
+
+
+
+ quarkus-extensions-parent
+ io.quarkus
+ 999-SNAPSHOT
+
+ 4.0.0
+
+ quarkus-oidc-client-registration-parent
+ Quarkus - OpenID Connect Dynamic Client Registration
+ pom
+
+ deployment
+ runtime
+
+
diff --git a/extensions/oidc-client-registration/runtime/pom.xml b/extensions/oidc-client-registration/runtime/pom.xml
new file mode 100644
index 00000000000000..5a7b0038d55bb4
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+ quarkus-oidc-client-registration-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-oidc-client-registration
+ Quarkus - OpenID Connect Dynamic Client Registration - Runtime
+ Register clients with OpenID Connect providers
+
+
+ io.quarkus
+ quarkus-core
+
+
+ io.quarkus
+ quarkus-vertx
+
+
+ io.quarkus
+ quarkus-oidc-common
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+
+
+ io.quarkus.oidc-client-registration
+
+
+
+
+ maven-compiler-plugin
+
+
+ default-compile
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${project.version}
+
+
+
+ -AlegacyConfigRoot=true
+
+
+
+
+
+
+
+
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java
new file mode 100644
index 00000000000000..832460f2c0b1bd
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java
@@ -0,0 +1,115 @@
+package io.quarkus.oidc.client.registration;
+
+import java.util.List;
+import java.util.Map;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+
+import io.quarkus.oidc.common.runtime.AbstractJsonObject;
+import io.quarkus.oidc.common.runtime.OidcConstants;
+
+public class ClientMetadata extends AbstractJsonObject {
+
+ public ClientMetadata() {
+ super();
+ }
+
+ public ClientMetadata(String json) {
+ super(json);
+ }
+
+ public ClientMetadata(JsonObject json) {
+ super(json);
+ }
+
+ public ClientMetadata(ClientMetadata metadata) {
+ super(metadata.getJsonObject());
+ }
+
+ public String getClientId() {
+ return super.getString(OidcConstants.CLIENT_ID);
+ }
+
+ public String getClientSecret() {
+ return super.getString(OidcConstants.CLIENT_SECRET);
+ }
+
+ public String getClientName() {
+ return super.getString(OidcConstants.CLIENT_METADATA_CLIENT_NAME);
+ }
+
+ public List getRedirectUris() {
+ return getListOfStrings(OidcConstants.CLIENT_METADATA_REDIRECT_URIS);
+ }
+
+ public List getPostLogoutUris() {
+ return getListOfStrings(OidcConstants.CLIENT_METADATA_POST_LOGOUT_URIS);
+ }
+
+ public String getMetadataString() {
+ return super.getJsonString();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static Builder builder(ClientMetadata m) {
+ return new Builder(m.getJsonObject());
+ }
+
+ public static class Builder {
+
+ JsonObjectBuilder builder;
+ boolean built = false;
+
+ Builder() {
+ builder = Json.createObjectBuilder();
+ }
+
+ Builder(JsonObject json) {
+ builder = Json.createObjectBuilder(json);
+ }
+
+ public Builder clientName(String clientName) {
+ if (built) {
+ throw new IllegalStateException();
+ }
+ builder.add(OidcConstants.CLIENT_METADATA_CLIENT_NAME, clientName);
+ return this;
+ }
+
+ public Builder redirectUri(String redirectUri) {
+ if (built) {
+ throw new IllegalStateException();
+ }
+ builder.add(OidcConstants.CLIENT_METADATA_REDIRECT_URIS,
+ Json.createArrayBuilder().add(redirectUri).build());
+ return this;
+ }
+
+ public Builder postLogoutUri(String postLogoutUri) {
+ if (built) {
+ throw new IllegalStateException();
+ }
+ builder.add(OidcConstants.CLIENT_METADATA_POST_LOGOUT_URIS,
+ Json.createArrayBuilder().add(postLogoutUri).build());
+ return this;
+ }
+
+ public Builder extraProps(Map extraProps) {
+ if (built) {
+ throw new IllegalStateException();
+ }
+ builder.addAll(Json.createObjectBuilder(extraProps));
+ return this;
+ }
+
+ public ClientMetadata build() {
+ built = true;
+ return new ClientMetadata(builder.build());
+ }
+ }
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java
new file mode 100644
index 00000000000000..0c650f498c42d2
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java
@@ -0,0 +1,44 @@
+package io.quarkus.oidc.client.registration;
+
+import java.io.Closeable;
+import java.util.List;
+
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+
+/**
+ * OIDC client registration.
+ */
+public interface OidcClientRegistration extends Closeable {
+ /**
+ * Client registered at start-up with the configured metadata.
+ *
+ * @return {@link RegisteredClient}, null if no configured metadata is available.
+ */
+ Uni registeredClient();
+
+ /**
+ * Register new client
+ *
+ * @param client client metadata for registering a new client
+ * @return Uni
+ */
+ Uni registerClient(ClientMetadata client);
+
+ /**
+ * Register one or more new clients
+ *
+ * @param clients list of client metadata for registering new clients
+ * @return Uni
+ */
+ Multi registerClients(List clients);
+
+ /**
+ * Read an already registered client.
+ *
+ * @param registrationUri Address of the registration endpoint for the client.
+ * @param registrationToken Registration token of the client
+ * @return registered client.
+ */
+ Uni readClient(String registrationUri, String registrationToken);
+}
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
new file mode 100644
index 00000000000000..871d3a515c4c7d
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java
@@ -0,0 +1,76 @@
+package io.quarkus.oidc.client.registration;
+
+import java.util.HashMap;
+import java.util.Map;
+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 {
+
+ /**
+ * 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<>();
+ }
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java
new file mode 100644
index 00000000000000..4a5b8801803da4
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java
@@ -0,0 +1,42 @@
+package io.quarkus.oidc.client.registration;
+
+import java.io.Closeable;
+import java.util.Map;
+
+import io.smallrye.mutiny.Uni;
+
+/**
+ * OIDC client registrations
+ */
+public interface OidcClientRegistrations extends Closeable {
+
+ /**
+ * Default OIDC client registration.
+ *
+ * @return {@link OidcClientRegistration}, null if no OIDC client registration configuration is available.
+ */
+ OidcClientRegistration getClientRegistration();
+
+ /**
+ * Return a named OIDC client registration
+ *
+ * @param id OIDC client registration id.
+ * @return {@link OidcClientRegistration}, null if no named OIDC client registration configuration is available.
+ */
+ OidcClientRegistration getClientRegistration(String id);
+
+ /**
+ * Return a map of all OIDC client registrations created from configured OIDC client registration configurations.
+ *
+ * @return Map of OIDC client registrations
+ */
+ Map getClientRegistrations();
+
+ /**
+ * Create a new OIDC client registration
+ *
+ * @param oidcConfig OIDC client registration configuration
+ * @return Uni
+ */
+ Uni newClientRegistration(OidcClientRegistrationConfig oidcConfig);
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java
new file mode 100644
index 00000000000000..1181d5e880d739
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java
@@ -0,0 +1,51 @@
+package io.quarkus.oidc.client.registration;
+
+import java.io.Closeable;
+
+import io.smallrye.mutiny.Uni;
+
+/**
+ * Client registered with {@link OidcClientConfiguration}
+ */
+public interface RegisteredClient extends Closeable {
+
+ /**
+ * Return current metadata of the registered client.
+ *
+ * @return Metadata of the registered client.
+ */
+ ClientMetadata metadata();
+
+ /**
+ * Return this client's registration URI.
+ *
+ * @return Address of the registration endpoint for this client.
+ */
+ String registrationUri();
+
+ /**
+ * Return this client's registration token.
+ *
+ * @return Registration token of this client.
+ */
+ String registrationToken();
+
+ /**
+ * Read current metadata of the registered client from this client's registration endpoint.
+ *
+ * @return Registered client containing current metadata.
+ */
+ Uni read();
+
+ /**
+ * Update metadata of the registered client using this client's registration endpoint.
+ *
+ * @return Registered client containing updated metadata.
+ */
+ Uni update(ClientMetadata metadata);
+
+ /**
+ * Delete registered client from this client's registration endpoint.
+ */
+ Uni delete();
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java
new file mode 100644
index 00000000000000..d83149887269a9
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java
@@ -0,0 +1,24 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+/**
+ * Exception which indicates that an injected {@link OidcClientRegistration} is disabled
+ * with the `quarkus.oidc-client-registration.registration-enabled=false` property.
+ */
+@SuppressWarnings("serial")
+public class DisabledOidcClientRegistrationException extends RuntimeException {
+ public DisabledOidcClientRegistrationException() {
+
+ }
+
+ public DisabledOidcClientRegistrationException(String errorMessage) {
+ this(errorMessage, null);
+ }
+
+ public DisabledOidcClientRegistrationException(Throwable cause) {
+ this(null, cause);
+ }
+
+ public DisabledOidcClientRegistrationException(String errorMessage, Throwable cause) {
+ super(errorMessage, cause);
+ }
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java
new file mode 100644
index 00000000000000..67f2378bb5a572
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java
@@ -0,0 +1,24 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+/**
+ * Exception which indicates that an error has occurred during the {@link OidcClientRegistration}
+ * initialization, default client registration or subsequent operations with the client registration endpoint.
+ */
+@SuppressWarnings("serial")
+public class OidcClientRegistrationException extends RuntimeException {
+ public OidcClientRegistrationException() {
+
+ }
+
+ public OidcClientRegistrationException(String errorMessage) {
+ this(errorMessage, null);
+ }
+
+ public OidcClientRegistrationException(Throwable cause) {
+ this(null, cause);
+ }
+
+ public OidcClientRegistrationException(String errorMessage, Throwable cause) {
+ super(errorMessage, cause);
+ }
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java
new file mode 100644
index 00000000000000..70d52b2c1f3c7b
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java
@@ -0,0 +1,244 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObjectBuilder;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig.Metadata;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.quarkus.oidc.common.OidcEndpoint;
+import io.quarkus.oidc.common.OidcEndpoint.Type;
+import io.quarkus.oidc.common.OidcRequestContextProperties;
+import io.quarkus.oidc.common.OidcRequestFilter;
+import io.quarkus.oidc.common.runtime.OidcCommonUtils;
+import io.quarkus.oidc.common.runtime.OidcConstants;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+import io.smallrye.mutiny.groups.UniOnItem;
+import io.smallrye.mutiny.subscription.MultiEmitter;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.json.JsonObject;
+import io.vertx.mutiny.core.buffer.Buffer;
+import io.vertx.mutiny.ext.web.client.HttpRequest;
+import io.vertx.mutiny.ext.web.client.HttpResponse;
+import io.vertx.mutiny.ext.web.client.WebClient;
+
+public class OidcClientRegistrationImpl implements OidcClientRegistration {
+ private static final Logger LOG = Logger.getLogger(OidcClientRegistrationImpl.class);
+ private static final String APPLICATION_JSON = "application/json";
+ private static final String AUTHORIZATION_HEADER = String.valueOf(HttpHeaders.AUTHORIZATION);
+ private static final String DEFAULT_ID = "Default";
+
+ private final WebClient client;
+ private final long connectionDelayInMillisecs;
+ private final String registrationUri;
+ private final OidcClientRegistrationConfig oidcConfig;
+ private final Map> filters;
+ private final RegisteredClient registeredClient;
+ private volatile boolean closed;
+
+ public OidcClientRegistrationImpl(WebClient client, long connectionDelayInMillisecs,
+ String registrationUri,
+ OidcClientRegistrationConfig oidcConfig, RegisteredClient registeredClient,
+ Map> oidcRequestFilters) {
+ this.client = client;
+ this.connectionDelayInMillisecs = connectionDelayInMillisecs;
+ this.registrationUri = registrationUri;
+ this.oidcConfig = oidcConfig;
+ this.filters = oidcRequestFilters;
+ this.registeredClient = registeredClient;
+ }
+
+ @Override
+ public Uni registeredClient() {
+ if (registeredClient != null) {
+ return Uni.createFrom().item(registeredClient);
+ } else if (oidcConfig.registerEarly) {
+ return Uni.createFrom().nullItem();
+ } else {
+ ClientMetadata metadata = createMetadata(oidcConfig.metadata);
+ if (metadata.getJsonObject().isEmpty()) {
+ LOG.debugf("%s client registration is skipped because its metadata is not configured",
+ oidcConfig.id.orElse(DEFAULT_ID));
+ return Uni.createFrom().nullItem();
+ } else {
+ return registerClient(client, registrationUri,
+ oidcConfig, filters, metadata.getMetadataString())
+ .onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
+ .retry()
+ .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION,
+ OidcCommonUtils.CONNECTION_BACKOFF_DURATION)
+ .expireIn(connectionDelayInMillisecs);
+ }
+ }
+ }
+
+ @Override
+ public Uni registerClient(ClientMetadata metadata) {
+ LOG.debugf("Register client metadata: %s", metadata.getMetadataString());
+ checkClosed();
+ return postRequest(client, registrationUri, oidcConfig, filters, metadata.getMetadataString())
+ .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, filters));
+ }
+
+ @Override
+ public Multi registerClients(List metadataList) {
+ LOG.debugf("Register clients");
+ checkClosed();
+ return Multi.createFrom().emitter(new Consumer>() {
+ @Override
+ public void accept(MultiEmitter super RegisteredClient> multiEmitter) {
+ try {
+ AtomicInteger emitted = new AtomicInteger();
+ for (ClientMetadata metadata : metadataList) {
+ postRequest(client, registrationUri, oidcConfig, filters, metadata.getMetadataString())
+ .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, filters))
+ .subscribe().with(new Consumer() {
+ @Override
+ public void accept(RegisteredClient client) {
+ multiEmitter.emit(client);
+ if (emitted.incrementAndGet() == metadataList.size()) {
+ multiEmitter.complete();
+ }
+ }
+ });
+ }
+ } catch (Exception ex) {
+ multiEmitter.fail(ex);
+ }
+ }
+ });
+ }
+
+ static Uni registerClient(WebClient client,
+ String registrationUri,
+ OidcClientRegistrationConfig oidcConfig,
+ Map> filters,
+ String clientRegJson) {
+ return postRequest(client, registrationUri, oidcConfig, filters, clientRegJson)
+ .transform(resp -> newRegisteredClient(resp, client, registrationUri, oidcConfig, filters));
+ }
+
+ static UniOnItem> postRequest(WebClient client, String registrationUri,
+ OidcClientRegistrationConfig oidcConfig,
+ Map> filters, String clientRegJson) {
+ HttpRequest request = client.postAbs(registrationUri);
+ request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), APPLICATION_JSON);
+ request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON);
+ if (oidcConfig.initialToken.orElse(null) != null) {
+ request.putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + oidcConfig.initialToken.get());
+ }
+ // Retry up to three times with a one-second delay between the retries if the connection is closed
+ Buffer buffer = Buffer.buffer(clientRegJson);
+ Uni> response = filter(request, filters, buffer).sendBuffer(buffer)
+ .onFailure(ConnectException.class)
+ .retry()
+ .atMost(oidcConfig.connectionRetryCount)
+ .onFailure().transform(t -> {
+ LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t);
+ // don't wrap it to avoid information leak
+ return new OidcClientRegistrationException("OIDC Server is not available");
+ });
+ return response.onItem();
+ }
+
+ static private HttpRequest filter(HttpRequest request, Map> filters,
+ Buffer body) {
+ if (!filters.isEmpty()) {
+ OidcRequestContextProperties props = new OidcRequestContextProperties();
+ for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters,
+ OidcEndpoint.Type.CLIENT_REGISTRATION)) {
+ filter.filter(request, body, props);
+ }
+ }
+ return request;
+ }
+
+ static private RegisteredClient newRegisteredClient(HttpResponse resp,
+ WebClient client, String registrationUri, OidcClientRegistrationConfig oidcConfig,
+ Map> filters) {
+ if (resp.statusCode() == 200 || resp.statusCode() == 201) {
+ JsonObject json = resp.bodyAsJsonObject();
+ LOG.debugf("Client has been succesfully registered: %s", json.toString());
+
+ String registrationClientUri = (String) json.remove(OidcConstants.REGISTRATION_CLIENT_URI);
+ String registrationToken = (String) json.remove(OidcConstants.REGISTRATION_ACCESS_TOKEN);
+
+ ClientMetadata metadata = new ClientMetadata(json.toString());
+
+ return new RegisteredClientImpl(client, oidcConfig, filters, metadata,
+ registrationClientUri, registrationToken);
+ } else {
+ String errorMessage = resp.bodyAsString();
+ LOG.errorf("Client registeration has failed: status: %d, error message: %s", resp.statusCode(),
+ errorMessage);
+ throw new OidcClientRegistrationException(errorMessage);
+ }
+ }
+
+ @Override
+ public Uni readClient(String registrationUri, String registrationToken) {
+ @SuppressWarnings("resource")
+ RegisteredClient newClient = new RegisteredClientImpl(client, oidcConfig,
+ filters, createMetadata(oidcConfig.metadata), registrationUri, registrationToken);
+ return newClient.read();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ try {
+ client.close();
+ } catch (Exception ex) {
+ }
+ closed = true;
+ }
+ }
+
+ private void checkClosed() {
+ if (closed) {
+ throw new IllegalStateException("OIDC Client Registration is closed");
+ }
+ }
+
+ static class ClientRegistrationHelper {
+ RegisteredClient client;
+ String registrationUri;
+
+ ClientRegistrationHelper(RegisteredClient client, String registrationUri) {
+ this.client = client;
+ this.registrationUri = registrationUri;
+ }
+ }
+
+ static ClientMetadata createMetadata(Metadata metadata) {
+ JsonObjectBuilder json = Json.createObjectBuilder();
+ if (metadata.clientName.isPresent()) {
+ json.add(OidcConstants.CLIENT_METADATA_CLIENT_NAME, metadata.clientName.get());
+ }
+ if (metadata.redirectUri.isPresent()) {
+ json.add(OidcConstants.CLIENT_METADATA_REDIRECT_URIS,
+ Json.createArrayBuilder().add(metadata.redirectUri.get()));
+ }
+ if (metadata.postLogoutUri.isPresent()) {
+ json.add(OidcConstants.POST_LOGOUT_REDIRECT_URI,
+ Json.createArrayBuilder().add(metadata.postLogoutUri.get()));
+ }
+ for (Map.Entry entry : metadata.extraProps.entrySet()) {
+ json.add(entry.getKey(), entry.getValue());
+ }
+
+ return new ClientMetadata(json.build());
+ }
+}
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
new file mode 100644
index 00000000000000..73b91bb5c4aff3
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java
@@ -0,0 +1,273 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig.Metadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistrations;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.quarkus.oidc.common.OidcEndpoint;
+import io.quarkus.oidc.common.OidcRequestContextProperties;
+import io.quarkus.oidc.common.OidcRequestFilter;
+import io.quarkus.oidc.common.runtime.OidcCommonUtils;
+import io.quarkus.runtime.annotations.Recorder;
+import io.quarkus.runtime.configuration.ConfigurationException;
+import io.quarkus.tls.TlsConfiguration;
+import io.quarkus.tls.TlsConfigurationRegistry;
+import io.smallrye.mutiny.Multi;
+import io.smallrye.mutiny.Uni;
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.client.WebClientOptions;
+import io.vertx.mutiny.ext.web.client.WebClient;
+
+@Recorder
+public class OidcClientRegistrationRecorder {
+
+ private static final Logger LOG = Logger.getLogger(OidcClientRegistrationRecorder.class);
+ private static final String DEFAULT_ID = "Default";
+
+ public OidcClientRegistrations setup(OidcClientRegistrationsConfig oidcClientRegsConfig,
+ Supplier vertx, Supplier registrySupplier) {
+ var defaultTlsConfiguration = registrySupplier.get().getDefault().orElse(null);
+
+ OidcClientRegistration defaultClientReg = createOidcClientRegistration(oidcClientRegsConfig.defaultClientRegistration,
+ defaultTlsConfiguration, vertx);
+
+ Map staticOidcClientRegs = new HashMap<>();
+
+ for (Map.Entry config : oidcClientRegsConfig.namedClientRegistrations
+ .entrySet()) {
+ staticOidcClientRegs.put(config.getKey(),
+ createOidcClientRegistration(config.getValue(), defaultTlsConfiguration, vertx));
+ }
+
+ return new OidcClientRegistrationsImpl(defaultClientReg, staticOidcClientRegs,
+ new Function>() {
+ @Override
+ public Uni apply(OidcClientRegistrationConfig config) {
+ return createOidcClientRegistrationUni(config, defaultTlsConfiguration, vertx);
+ }
+ });
+ }
+
+ private static boolean isEmptyMetadata(Metadata m) {
+ return m.clientName.isEmpty() && m.redirectUri.isEmpty()
+ && m.postLogoutUri.isEmpty() && m.extraProps.isEmpty();
+ }
+
+ public Supplier createOidcClientRegistrationBean(OidcClientRegistrations oidcClientRegs) {
+ return new Supplier() {
+
+ @Override
+ public OidcClientRegistration get() {
+ return oidcClientRegs.getClientRegistration();
+ }
+ };
+ }
+
+ public Supplier createOidcClientRegistrationsBean(OidcClientRegistrations oidcClientRegs) {
+ return new Supplier() {
+
+ @Override
+ public OidcClientRegistrations get() {
+ return oidcClientRegs;
+ }
+ };
+ }
+
+ public static OidcClientRegistration createOidcClientRegistration(OidcClientRegistrationConfig oidcConfig,
+ TlsConfiguration tlsConfig, Supplier vertxSupplier) {
+ return createOidcClientRegistrationUni(oidcConfig, tlsConfig, vertxSupplier).await()
+ .atMost(oidcConfig.connectionTimeout);
+ }
+
+ public static Uni createOidcClientRegistrationUni(OidcClientRegistrationConfig oidcConfig,
+ TlsConfiguration tlsConfig, Supplier vertxSupplier) {
+ if (!oidcConfig.registrationEnabled) {
+ String message = String.format("'%s' client registration configuration is disabled", "");
+ LOG.debug(message);
+ return Uni.createFrom().item(new DisabledOidcClientRegistration(message));
+ }
+
+ try {
+ if (oidcConfig.authServerUrl.isEmpty() && !OidcCommonUtils.isAbsoluteUrl(oidcConfig.registrationPath)) {
+ if (isEmptyMetadata(oidcConfig.metadata)) {
+ return Uni.createFrom().nullItem();
+ }
+ throw new ConfigurationException(
+ "Either 'quarkus.oidc-client-registration.auth-server-url' or absolute 'quarkus.oidc-client-registration.registration-path' URL must be set");
+ }
+ OidcCommonUtils.verifyEndpointUrl(getEndpointUrl(oidcConfig));
+ } catch (Throwable t) {
+ LOG.error(t.getMessage());
+ String message = String.format("'%s' client registration configuration is not initialized",
+ oidcConfig.id.orElse("Default"));
+ return Uni.createFrom().failure(new RuntimeException(message));
+ }
+
+ WebClientOptions options = new WebClientOptions();
+
+ OidcCommonUtils.setHttpClientOptions(oidcConfig, options, tlsConfig);
+
+ final io.vertx.mutiny.core.Vertx vertx = new io.vertx.mutiny.core.Vertx(vertxSupplier.get());
+ WebClient client = WebClient.create(vertx, options);
+
+ Map> oidcRequestFilters = OidcCommonUtils.getOidcRequestFilters();
+
+ Uni clientRegConfigUni = null;
+ if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.registrationPath)) {
+ clientRegConfigUni = Uni.createFrom().item(
+ new OidcConfigurationMetadata(oidcConfig.registrationPath.get()));
+ } else {
+ String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig);
+ if (!oidcConfig.getDiscoveryEnabled().orElse(true)) {
+ clientRegConfigUni = Uni.createFrom()
+ .item(new OidcConfigurationMetadata(
+ OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath)));
+ } else {
+ clientRegConfigUni = discoverRegistrationUri(client, oidcRequestFilters, authServerUriString.toString(), vertx,
+ oidcConfig);
+ }
+ }
+ return clientRegConfigUni.onItemOrFailure()
+ .transformToUni(new BiFunction>() {
+
+ @Override
+ public Uni apply(OidcConfigurationMetadata metadata, Throwable t) {
+ if (t != null) {
+ throw toOidcClientRegException(getEndpointUrl(oidcConfig), t);
+ }
+
+ if (metadata.clientRegistrationUri == null) {
+ throw new ConfigurationException(
+ "OpenId Connect Provider client registration endpoint URL is not configured and can not be discovered");
+ }
+
+ final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
+
+ ClientMetadata clientMetadata = OidcClientRegistrationImpl.createMetadata(oidcConfig.metadata);
+ if (!oidcConfig.registerEarly) {
+ LOG.debugf("%s client registration is delayed",
+ oidcConfig.id.orElse(DEFAULT_ID));
+ return Uni.createFrom().item(new OidcClientRegistrationImpl(client,
+ connectionDelayInMillisecs,
+ metadata.clientRegistrationUri,
+ oidcConfig,
+ null,
+ oidcRequestFilters));
+ } else if (clientMetadata.getJsonObject().isEmpty()) {
+ LOG.debugf("%s client registration is skipped because its metadata is not configured",
+ oidcConfig.id.orElse(DEFAULT_ID));
+ return Uni.createFrom().item(new OidcClientRegistrationImpl(client,
+ connectionDelayInMillisecs,
+ metadata.clientRegistrationUri,
+ oidcConfig,
+ null,
+ oidcRequestFilters));
+ } else {
+ return OidcClientRegistrationImpl.registerClient(client,
+ metadata.clientRegistrationUri,
+ oidcConfig, oidcRequestFilters, clientMetadata.getMetadataString())
+ .onFailure(OidcCommonUtils.oidcEndpointNotAvailable())
+ .retry()
+ .withBackOff(OidcCommonUtils.CONNECTION_BACKOFF_DURATION,
+ OidcCommonUtils.CONNECTION_BACKOFF_DURATION)
+ .expireIn(connectionDelayInMillisecs)
+ .onItemOrFailure()
+ .transform(new BiFunction() {
+
+ @Override
+ public OidcClientRegistration apply(RegisteredClient r, Throwable t2) {
+ RegisteredClient registeredClient;
+ if (t2 != null) {
+ LOG.errorf("%s client registartion failed: %s, it can be retried later",
+ oidcConfig.id.orElse(DEFAULT_ID), t2.getMessage());
+ registeredClient = null;
+ } else {
+ registeredClient = r;
+ LOG.debugf("Registered client id: %s", r.metadata().getClientId());
+ }
+ return new OidcClientRegistrationImpl(client,
+ connectionDelayInMillisecs,
+ metadata.clientRegistrationUri,
+ oidcConfig,
+ registeredClient,
+ oidcRequestFilters);
+ }
+ });
+ }
+ }
+ });
+ }
+
+ private static String getEndpointUrl(OidcClientRegistrationConfig oidcConfig) {
+ return oidcConfig.authServerUrl.isPresent() ? oidcConfig.authServerUrl.get() : oidcConfig.registrationPath.get();
+ }
+
+ private static Uni discoverRegistrationUri(WebClient client,
+ Map> oidcRequestFilters,
+ String authServerUrl, io.vertx.mutiny.core.Vertx vertx, OidcClientRegistrationConfig oidcConfig) {
+ final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig);
+ return OidcCommonUtils
+ .discoverMetadata(client, oidcRequestFilters, new OidcRequestContextProperties(), authServerUrl,
+ connectionDelayInMillisecs, vertx,
+ oidcConfig.useBlockingDnsLookup)
+ .onItem().transform(json -> new OidcConfigurationMetadata(json.getString("registration_endpoint")));
+ }
+
+ protected static OidcClientRegistrationException toOidcClientRegException(String authServerUrlString, Throwable cause) {
+ return new OidcClientRegistrationException(OidcCommonUtils.formatConnectionErrorMessage(authServerUrlString), cause);
+ }
+
+ private static class DisabledOidcClientRegistration implements OidcClientRegistration {
+ String message;
+
+ DisabledOidcClientRegistration(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public Uni registeredClient() {
+ throw new DisabledOidcClientRegistrationException(message);
+ }
+
+ @Override
+ public Uni registerClient(ClientMetadata reg) {
+ throw new DisabledOidcClientRegistrationException(message);
+ }
+
+ @Override
+ public Multi registerClients(List regs) {
+ throw new DisabledOidcClientRegistrationException(message);
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ @Override
+ public Uni readClient(String registrationUri, String registrationToken) {
+ throw new DisabledOidcClientRegistrationException(message);
+ }
+
+ }
+
+ private static class OidcConfigurationMetadata {
+ private final String clientRegistrationUri;
+
+ OidcConfigurationMetadata(String clientRegistrationUri) {
+ this.clientRegistrationUri = clientRegistrationUri;
+ }
+ }
+
+}
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
new file mode 100644
index 00000000000000..5d2772fdc7fd70
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java
@@ -0,0 +1,28 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+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;
+
+@ConfigRoot(name = "oidc-client-registration", phase = ConfigPhase.RUN_TIME)
+public class OidcClientRegistrationsConfig {
+
+ /**
+ * The default client registration.
+ */
+ @ConfigItem(name = ConfigItem.PARENT)
+ public OidcClientRegistrationConfig defaultClientRegistration;
+
+ /**
+ * Additional named client registrations.
+ */
+ @ConfigDocSection
+ @ConfigDocMapKey("id")
+ @ConfigItem(name = ConfigItem.PARENT)
+ public Map namedClientRegistrations;
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java
new file mode 100644
index 00000000000000..7f4fd68fedeff4
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java
@@ -0,0 +1,56 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.function.Function;
+
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.quarkus.oidc.client.registration.OidcClientRegistrations;
+import io.smallrye.mutiny.Uni;
+
+public class OidcClientRegistrationsImpl implements OidcClientRegistrations, Closeable {
+ private OidcClientRegistration defaultClientReg;
+ private Map staticOidcClientRegs;
+ Function> newOidcClientReg;
+
+ public OidcClientRegistrationsImpl() {
+ }
+
+ public OidcClientRegistrationsImpl(OidcClientRegistration defaultClientReg,
+ Map staticOidcClientRegs,
+ Function> newOidcClientReg) {
+ this.defaultClientReg = defaultClientReg;
+ this.staticOidcClientRegs = staticOidcClientRegs;
+ this.newOidcClientReg = newOidcClientReg;
+ }
+
+ @Override
+ public OidcClientRegistration getClientRegistration() {
+ return defaultClientReg;
+ }
+
+ @Override
+ public OidcClientRegistration getClientRegistration(String id) {
+ return staticOidcClientRegs.get(id);
+ }
+
+ public Map getClientRegistrations() {
+ return Collections.unmodifiableMap(staticOidcClientRegs);
+ }
+
+ @Override
+ public Uni newClientRegistration(OidcClientRegistrationConfig oidcConfig) {
+ return newOidcClientReg.apply(oidcConfig);
+ }
+
+ @Override
+ public void close() throws IOException {
+ defaultClientReg.close();
+ for (OidcClientRegistration clientReg : staticOidcClientRegs.values()) {
+ clientReg.close();
+ }
+ }
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java
new file mode 100644
index 00000000000000..5ade2e79ff2997
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java
@@ -0,0 +1,226 @@
+package io.quarkus.oidc.client.registration.runtime;
+
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.json.Json;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonValue;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.quarkus.oidc.common.OidcEndpoint;
+import io.quarkus.oidc.common.OidcEndpoint.Type;
+import io.quarkus.oidc.common.OidcRequestContextProperties;
+import io.quarkus.oidc.common.OidcRequestFilter;
+import io.quarkus.oidc.common.runtime.OidcCommonUtils;
+import io.quarkus.oidc.common.runtime.OidcConstants;
+import io.smallrye.mutiny.Uni;
+import io.smallrye.mutiny.groups.UniOnItem;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.mutiny.core.buffer.Buffer;
+import io.vertx.mutiny.ext.web.client.HttpRequest;
+import io.vertx.mutiny.ext.web.client.HttpResponse;
+import io.vertx.mutiny.ext.web.client.WebClient;
+
+public class RegisteredClientImpl implements RegisteredClient {
+ private static final Logger LOG = Logger.getLogger(RegisteredClientImpl.class);
+
+ private static final String APPLICATION_JSON = "application/json";
+ private static final String AUTHORIZATION_HEADER = String.valueOf(HttpHeaders.AUTHORIZATION);
+ //https://datatracker.ietf.org/doc/html/rfc7592.html#section-2.2
+ private static final Set PRIVATE_PROPERTIES = Set.of(OidcConstants.CLIENT_METADATA_SECRET_EXPIRES_AT,
+ OidcConstants.CLIENT_METADATA_ID_ISSUED_AT);
+
+ private final WebClient client;
+ private final OidcClientRegistrationConfig oidcConfig;
+ private final String registrationClientUri;
+ private final String registrationToken;
+ private final ClientMetadata registeredMetadata;
+ private final Map> filters;
+ private volatile boolean closed;
+
+ public RegisteredClientImpl(WebClient client, OidcClientRegistrationConfig oidcConfig,
+ Map> oidcRequestFilters,
+ ClientMetadata registeredMetadata, String registrationClientUri, String registrationToken) {
+ this.client = client;
+ this.oidcConfig = oidcConfig;
+ this.registrationClientUri = registrationClientUri;
+ this.registrationToken = registrationToken;
+ this.registeredMetadata = registeredMetadata;
+ this.filters = oidcRequestFilters;
+ }
+
+ @Override
+ public ClientMetadata metadata() {
+ checkClosed();
+ return new ClientMetadata(registeredMetadata.getMetadataString());
+ }
+
+ @Override
+ public Uni read() {
+ checkClosed();
+ checkClientRequestUri();
+ HttpRequest request = client.getAbs(registrationClientUri);
+ request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON);
+ return makeRequest(request, Buffer.buffer())
+ .transform(resp -> newRegisteredClient(resp));
+ }
+
+ @Override
+ public Uni update(ClientMetadata newMetadata) {
+
+ checkClosed();
+ checkClientRequestUri();
+ if (newMetadata.getClientId() != null && !registeredMetadata.getClientId().equals(newMetadata.getClientId())) {
+ throw new OidcClientRegistrationException("Client id can not be modified");
+ }
+ if (newMetadata.getClientSecret() != null
+ && !registeredMetadata.getClientSecret().equals(newMetadata.getClientSecret())) {
+ throw new OidcClientRegistrationException("Client secret can not be modified");
+ }
+
+ JsonObjectBuilder builder = Json.createObjectBuilder();
+
+ JsonObject newJsonObject = newMetadata.getJsonObject();
+ JsonObject currentJsonObject = registeredMetadata.getJsonObject();
+
+ LOG.debugf("Current client metadata: %s", currentJsonObject.toString());
+
+ // Try to ensure the same order of properties as in the original metadata
+ for (Map.Entry entry : currentJsonObject.entrySet()) {
+ if (PRIVATE_PROPERTIES.contains(entry.getKey())) {
+ continue;
+ }
+ boolean newPropValue = newJsonObject.containsKey(entry.getKey());
+ builder.add(entry.getKey(), newPropValue ? newJsonObject.get(entry.getKey()) : entry.getValue());
+ }
+ for (Map.Entry entry : newJsonObject.entrySet()) {
+ if (PRIVATE_PROPERTIES.contains(entry.getKey())) {
+ continue;
+ }
+ if (!currentJsonObject.containsKey(entry.getKey())) {
+ builder.add(entry.getKey(), entry.getValue());
+ }
+ }
+ JsonObject json = builder.build();
+
+ LOG.debugf("Updated client metadata: %s", json.toString());
+
+ HttpRequest request = client.putAbs(registrationClientUri);
+ request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), APPLICATION_JSON);
+ request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON);
+ return makeRequest(request, Buffer.buffer(json.toString()))
+ .transform(resp -> newRegisteredClient(resp));
+ }
+
+ @Override
+ public Uni delete() {
+ checkClosed();
+ checkClientRequestUri();
+
+ return makeRequest(client.deleteAbs(registrationClientUri), Buffer.buffer())
+ .transformToUni(resp -> deleteResponse(resp));
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!closed) {
+ try {
+ client.close();
+ } catch (Exception ex) {
+ }
+ closed = true;
+ }
+ }
+
+ private UniOnItem> makeRequest(HttpRequest request, Buffer buffer) {
+ if (registrationToken != null) {
+ request.putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + registrationToken);
+ }
+ // Retry up to three times with a one-second delay between the retries if the connection is closed
+ Uni> response = filter(request, buffer).sendBuffer(buffer)
+ .onFailure(ConnectException.class)
+ .retry()
+ .atMost(oidcConfig.connectionRetryCount)
+ .onFailure().transform(t -> {
+ LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t);
+ // don't wrap it to avoid information leak
+ return new OidcClientRegistrationException("OIDC Server is not available");
+ });
+ return response.onItem();
+ }
+
+ private HttpRequest filter(HttpRequest request, Buffer body) {
+ if (!filters.isEmpty()) {
+ OidcRequestContextProperties props = new OidcRequestContextProperties();
+ for (OidcRequestFilter filter : OidcCommonUtils.getMatchingOidcRequestFilters(filters,
+ OidcEndpoint.Type.CLIENT_CONFIGURATION)) {
+ filter.filter(request, body, props);
+ }
+ }
+ return request;
+ }
+
+ private RegisteredClient newRegisteredClient(HttpResponse resp) {
+ if (resp.statusCode() >= 200 && resp.statusCode() < 300) {
+ io.vertx.core.json.JsonObject json = resp.bodyAsJsonObject();
+ LOG.debugf("Client metadata has been succesfully updated: %s", json.toString());
+
+ String newRegistrationClientUri = (String) json.remove(OidcConstants.REGISTRATION_CLIENT_URI);
+ String newRegistrationToken = (String) json.remove(OidcConstants.REGISTRATION_ACCESS_TOKEN);
+
+ return new RegisteredClientImpl(client, oidcConfig, filters, new ClientMetadata(json.toString()),
+ (newRegistrationClientUri != null ? newRegistrationClientUri : registrationClientUri),
+ (newRegistrationToken != null ? newRegistrationToken : registrationToken));
+ } else {
+ String errorMessage = resp.bodyAsString();
+ LOG.debugf("Client configuration update has failed: status: %d, error message: %s", resp.statusCode(),
+ errorMessage);
+ throw new OidcClientRegistrationException(errorMessage);
+ }
+ }
+
+ private Uni deleteResponse(HttpResponse resp) {
+ if (resp.statusCode() == 200) {
+ LOG.debug("Client has been succesfully deleted");
+ return Uni.createFrom().voidItem();
+ } else {
+ String errorMessage = resp.bodyAsString();
+ LOG.debugf("Client delete request has failed: status: %d, error message: %s", resp.statusCode(),
+ errorMessage);
+ return Uni.createFrom().voidItem();
+ }
+ }
+
+ private void checkClosed() {
+ if (closed) {
+ throw new IllegalStateException("Registered OIDC Client is closed");
+ }
+ }
+
+ private void checkClientRequestUri() {
+ if (registrationClientUri == null) {
+ throw new OidcClientRegistrationException(
+ "Registered OIDC Client can not make requests to the client configuration endpoint");
+ }
+ }
+
+ @Override
+ public String registrationUri() {
+ return this.registrationClientUri;
+ }
+
+ @Override
+ public String registrationToken() {
+ return this.registrationToken;
+ }
+
+}
diff --git a/extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 00000000000000..b397f0b0970596
--- /dev/null
+++ b/extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,16 @@
+---
+artifact: ${project.groupId}:${project.artifactId}:${project.version}
+name: "OpenID Connect Dynamic Client Registration"
+metadata:
+ keywords:
+ - "oauth2"
+ - "openid-connect"
+ - "oidc"
+ - "oidc-client"
+ - "oidc-client-registration"
+ guide: "https://quarkus.io/guides/security-openid-connect-client-registration"
+ categories:
+ - "security"
+ status: "experimental"
+ config:
+ - "quarkus.oidc-client-registration."
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 5b40ddb43d9c69..f6c180a979e647 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
@@ -5,14 +5,14 @@
import java.util.Map;
import java.util.Optional;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig;
+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 OidcCommonConfig {
+public class OidcClientConfig extends OidcClientCommonConfig {
/**
* A unique OIDC client identifier. It must be set when OIDC clients are created dynamically
diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java
index 11c65e893cf2c3..20a3b6d186e8b5 100644
--- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java
+++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/OidcClientImpl.java
@@ -20,7 +20,7 @@
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Jwt.Source;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Jwt.Source;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.smallrye.mutiny.Uni;
diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java
index 362580ebf53237..18591c069964c1 100644
--- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java
+++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java
@@ -42,7 +42,15 @@ enum Type {
/**
* Applies to OIDC UserInfo endpoint requests
*/
- USERINFO
+ USERINFO,
+ /**
+ * Applies to OIDC client registration requests
+ */
+ CLIENT_REGISTRATION,
+ /**
+ * Applies to the configuration requests of the dynamically registered OIDC clients
+ */
+ CLIENT_CONFIGURATION
}
/**
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java
similarity index 68%
rename from extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java
rename to extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java
index 416848f07fcf2a..9959b2292f6ade 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java
+++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java
@@ -1,7 +1,9 @@
-package io.quarkus.oidc.runtime;
+package io.quarkus.oidc.common.runtime;
import java.io.StringReader;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -10,21 +12,23 @@
import jakarta.json.JsonNumber;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
+import jakarta.json.JsonString;
import jakarta.json.JsonValue;
-public class AbstractJsonObjectResponse {
+public abstract class AbstractJsonObject {
private String jsonString;
private JsonObject json;
- public AbstractJsonObjectResponse() {
+ protected AbstractJsonObject() {
+ json = Json.createObjectBuilder().build();
}
- public AbstractJsonObjectResponse(String jsonString) {
+ protected AbstractJsonObject(String jsonString) {
this(toJsonObject(jsonString));
this.jsonString = jsonString;
}
- public AbstractJsonObjectResponse(JsonObject json) {
+ protected AbstractJsonObject(JsonObject json) {
this.json = json;
}
@@ -50,7 +54,7 @@ public JsonObject getObject(String name) {
}
public JsonObject getJsonObject() {
- return json;
+ return Json.createObjectBuilder(json).build();
}
public Object get(String name) {
@@ -69,11 +73,24 @@ public Set> getAllProperties() {
return Collections.unmodifiableSet(json.entrySet());
}
- protected String getNonNullJsonString() {
+ protected String getJsonString() {
return jsonString == null ? json.toString() : jsonString;
}
- static JsonObject toJsonObject(String json) {
+ protected List getListOfStrings(String prop) {
+ JsonArray array = getArray(prop);
+ if (array == null) {
+ return null;
+ }
+ List list = new ArrayList();
+ for (JsonValue value : array) {
+ list.add(((JsonString) value).getString());
+ }
+
+ return list;
+ }
+
+ public static JsonObject toJsonObject(String json) {
try (JsonReader jsonReader = Json.createReader(new StringReader(json))) {
return jsonReader.readObject();
}
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
new file mode 100644
index 00000000000000..6150000e6837ac
--- /dev/null
+++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java
@@ -0,0 +1,497 @@
+package io.quarkus.oidc.common.runtime;
+
+import java.util.HashMap;
+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 {
+ /**
+ * 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();
+
+ /**
+ * 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.
+ */
+ @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 {
+
+ /**
+ * 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.
+ */
+ @ConfigItem
+ public Optional secret = Optional.empty();
+
+ /**
+ * 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.
+ */
+ @ConfigItem
+ public Secret clientSecret = new Secret();
+
+ /**
+ * Client JSON Web Token (JWT) authentication methods
+ */
+ @ConfigItem
+ public Jwt jwt = new Jwt();
+
+ public Optional getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = Optional.of(secret);
+ }
+
+ public Secret getClientSecret() {
+ return clientSecret;
+ }
+
+ public void setClientSecret(Secret clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public Jwt getJwt() {
+ return jwt;
+ }
+
+ public void setJwt(Jwt jwt) {
+ this.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
+ */
+ @ConfigGroup
+ public static class Secret {
+
+ public static 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.
+ */
+ @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() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = Optional.of(value);
+ }
+
+ public Optional getMethod() {
+ return method;
+ }
+
+ public void setMethod(Method method) {
+ this.method = Optional.of(method);
+ }
+
+ public Provider getSecretProvider() {
+ return provider;
+ }
+
+ public void setSecretProvider(Provider secretProvider) {
+ this.provider = secretProvider;
+ }
+ }
+
+ /**
+ * 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
+ */
+ @ConfigGroup
+ public static class Jwt {
+
+ public static 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.
+ */
+ @ConfigItem(defaultValue = "client")
+ public Source source = Source.CLIENT;
+
+ /**
+ * If provided, indicates that JWT is signed using a secret key.
+ */
+ @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();
+
+ /**
+ * String representation of a private key. If provided, indicates that JWT is signed using a private key in PEM or
+ * JWK format.
+ * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`.
+ */
+ @ConfigItem
+ public Optional key = Optional.empty();
+
+ /**
+ * If provided, indicates that JWT is signed using a private key in PEM or JWK format.
+ * 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.
+ */
+ @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<>();
+
+ /**
+ * The signature algorithm used for the {@link #keyFile} property.
+ * 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;
+
+ /**
+ * 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.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean assertion = false;
+
+ public Optional getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = Optional.of(secret);
+ }
+
+ public int getLifespan() {
+ return lifespan;
+ }
+
+ public void setLifespan(int lifespan) {
+ this.lifespan = lifespan;
+ }
+
+ public Optional getTokenKeyId() {
+ return tokenKeyId;
+ }
+
+ public void setTokenKeyId(String tokenKeyId) {
+ this.tokenKeyId = Optional.of(tokenKeyId);
+ }
+
+ public Provider getSecretProvider() {
+ return secretProvider;
+ }
+
+ public void setSecretProvider(Provider secretProvider) {
+ this.secretProvider = secretProvider;
+ }
+
+ public Optional getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public void setSignatureAlgorithm(String signatureAlgorithm) {
+ this.signatureAlgorithm = Optional.of(signatureAlgorithm);
+ }
+
+ public Optional getAudience() {
+ return audience;
+ }
+
+ public void setAudience(String audience) {
+ this.audience = Optional.of(audience);
+ }
+
+ public Optional getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = Optional.of(key);
+ }
+
+ public Optional getKeyFile() {
+ return keyFile;
+ }
+
+ public void setKeyFile(String keyFile) {
+ this.keyFile = Optional.of(keyFile);
+ }
+
+ public Map getClaims() {
+ return claims;
+ }
+
+ public void setClaims(Map claims) {
+ this.claims = claims;
+ }
+
+ public Source getSource() {
+ return source;
+ }
+
+ public void setSource(Source source) {
+ this.source = source;
+ }
+
+ public boolean isAssertion() {
+ return assertion;
+ }
+
+ public void setAssertion(boolean assertion) {
+ this.assertion = 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();
+
+ /**
+ * 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
+ */
+ @ConfigItem
+ public Optional keyringName = Optional.empty();
+
+ /**
+ * The CredentialsProvider client secret key
+ */
+ @ConfigItem
+ public Optional key = Optional.empty();
+
+ public Optional getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = Optional.of(name);
+ }
+
+ public Optional getKeyringName() {
+ return keyringName;
+ }
+
+ public void setKeyringName(String keyringName) {
+ this.keyringName = Optional.of(keyringName);
+ }
+
+ public Optional getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = Optional.of(key);
+ }
+ }
+ }
+
+ public Optional getTokenPath() {
+ return tokenPath;
+ }
+
+ public void setTokenPath(String tokenPath) {
+ this.tokenPath = Optional.of(tokenPath);
+ }
+
+ public Optional getRevokePath() {
+ return revokePath;
+ }
+
+ public void setRevokePath(String revokePath) {
+ this.revokePath = Optional.of(revokePath);
+ }
+
+ public Optional getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = Optional.of(clientId);
+ }
+
+ public Optional getClientName() {
+ return clientName;
+ }
+
+ public void setClientName(String clientName) {
+ this.clientName = Optional.of(clientName);
+ }
+
+ public Credentials getCredentials() {
+ return credentials;
+ }
+
+ 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 d744d88358f847..24f3e3b41f95bf 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
@@ -2,12 +2,9 @@
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.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
@@ -15,8 +12,8 @@
public class OidcCommonConfig {
/**
* The base URL of the OpenID Connect (OIDC) server, for example, `https://host:port/auth`.
- * Do not set this property if the public key verification ({@link #publicKey}) or certificate chain verification only
- * ({@link #certificateChain}) is required.
+ * 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.
*/
@@ -31,34 +28,11 @@ public class OidcCommonConfig {
public Optional discoveryEnabled = Optional.empty();
/**
- * The OIDC token endpoint that issues access and refresh tokens;
- * specified as a relative path or absolute URL.
+ * 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 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();
-
- /**
- * 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.
- */
- @ConfigItem
- public Optional clientName = Optional.empty();
+ public Optional registrationPath = Optional.empty();
/**
* The duration to attempt the initial connection to an OIDC server.
@@ -97,12 +71,6 @@ public class OidcCommonConfig {
@ConfigItem
public OptionalInt maxPoolSize = OptionalInt.empty();
- /**
- * Credentials the OIDC adapter uses to authenticate to the OIDC server.
- */
- @ConfigItem
- public Credentials credentials = new Credentials();
-
/**
* Options to configure the proxy the OIDC adapter uses to talk with the OIDC server.
*/
@@ -115,415 +83,6 @@ public class OidcCommonConfig {
@ConfigItem
public Tls tls = new Tls();
- @ConfigGroup
- public static class 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.
- */
- @ConfigItem
- public Optional secret = Optional.empty();
-
- /**
- * 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.
- */
- @ConfigItem
- public Secret clientSecret = new Secret();
-
- /**
- * Client JSON Web Token (JWT) authentication methods
- */
- @ConfigItem
- public Jwt jwt = new Jwt();
-
- public Optional getSecret() {
- return secret;
- }
-
- public void setSecret(String secret) {
- this.secret = Optional.of(secret);
- }
-
- public Secret getClientSecret() {
- return clientSecret;
- }
-
- public void setClientSecret(Secret clientSecret) {
- this.clientSecret = clientSecret;
- }
-
- public Jwt getJwt() {
- return jwt;
- }
-
- public void setJwt(Jwt jwt) {
- this.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
- */
- @ConfigGroup
- public static class Secret {
-
- public static 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.
- */
- @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() {
- return value;
- }
-
- public void setValue(String value) {
- this.value = Optional.of(value);
- }
-
- public Optional getMethod() {
- return method;
- }
-
- public void setMethod(Method method) {
- this.method = Optional.of(method);
- }
-
- public Provider getSecretProvider() {
- return provider;
- }
-
- public void setSecretProvider(Provider secretProvider) {
- this.provider = secretProvider;
- }
- }
-
- /**
- * 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
- */
- @ConfigGroup
- public static class Jwt {
-
- public static 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.
- */
- @ConfigItem(defaultValue = "client")
- public Source source = Source.CLIENT;
-
- /**
- * If provided, indicates that JWT is signed using a secret key.
- */
- @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();
-
- /**
- * String representation of a private key. If provided, indicates that JWT is signed using a private key in PEM or
- * JWK format.
- * You can use the {@link #signatureAlgorithm} property to override the default key algorithm, `RS256`.
- */
- @ConfigItem
- public Optional key = Optional.empty();
-
- /**
- * If provided, indicates that JWT is signed using a private key in PEM or JWK format.
- * 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.
- */
- @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<>();
-
- /**
- * The signature algorithm used for the {@link #keyFile} property.
- * 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;
-
- /**
- * 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.
- */
- @ConfigItem(defaultValue = "false")
- public boolean assertion = false;
-
- public Optional getSecret() {
- return secret;
- }
-
- public void setSecret(String secret) {
- this.secret = Optional.of(secret);
- }
-
- public int getLifespan() {
- return lifespan;
- }
-
- public void setLifespan(int lifespan) {
- this.lifespan = lifespan;
- }
-
- public Optional getTokenKeyId() {
- return tokenKeyId;
- }
-
- public void setTokenKeyId(String tokenKeyId) {
- this.tokenKeyId = Optional.of(tokenKeyId);
- }
-
- public Provider getSecretProvider() {
- return secretProvider;
- }
-
- public void setSecretProvider(Provider secretProvider) {
- this.secretProvider = secretProvider;
- }
-
- public Optional getSignatureAlgorithm() {
- return signatureAlgorithm;
- }
-
- public void setSignatureAlgorithm(String signatureAlgorithm) {
- this.signatureAlgorithm = Optional.of(signatureAlgorithm);
- }
-
- public Optional getAudience() {
- return audience;
- }
-
- public void setAudience(String audience) {
- this.audience = Optional.of(audience);
- }
-
- public Optional getKey() {
- return key;
- }
-
- public void setKey(String key) {
- this.key = Optional.of(key);
- }
-
- public Optional getKeyFile() {
- return keyFile;
- }
-
- public void setKeyFile(String keyFile) {
- this.keyFile = Optional.of(keyFile);
- }
-
- public Map getClaims() {
- return claims;
- }
-
- public void setClaims(Map claims) {
- this.claims = claims;
- }
-
- public Source getSource() {
- return source;
- }
-
- public void setSource(Source source) {
- this.source = source;
- }
-
- public boolean isAssertion() {
- return assertion;
- }
-
- public void setAssertion(boolean assertion) {
- this.assertion = 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();
-
- /**
- * 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
- */
- @ConfigItem
- public Optional keyringName = Optional.empty();
-
- /**
- * The CredentialsProvider client secret key
- */
- @ConfigItem
- public Optional key = Optional.empty();
-
- public Optional getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = Optional.of(name);
- }
-
- public Optional getKeyringName() {
- return keyringName;
- }
-
- public void setKeyringName(String keyringName) {
- this.keyringName = Optional.of(keyringName);
- }
-
- public Optional getKey() {
- return key;
- }
-
- public void setKey(String key) {
- this.key = Optional.of(key);
- }
- }
- }
-
@ConfigGroup
public static class Tls {
public enum Verification {
@@ -721,44 +280,12 @@ public void setAuthServerUrl(String authServerUrl) {
this.authServerUrl = Optional.of(authServerUrl);
}
- public Optional getTokenPath() {
- return tokenPath;
- }
-
- public void setTokenPath(String tokenPath) {
- this.tokenPath = Optional.of(tokenPath);
- }
-
- public Optional getRevokePath() {
- return revokePath;
- }
-
- public void setRevokePath(String revokePath) {
- this.revokePath = Optional.of(revokePath);
- }
-
- public Optional getClientId() {
- return clientId;
- }
-
- public void setClientId(String clientId) {
- this.clientId = Optional.of(clientId);
+ public Optional getRegistrationPath() {
+ return registrationPath;
}
- public Optional getClientName() {
- return clientName;
- }
-
- public void setClientName(String clientName) {
- this.clientName = Optional.of(clientName);
- }
-
- public Credentials getCredentials() {
- return credentials;
- }
-
- public void setCredentials(Credentials credentials) {
- this.credentials = credentials;
+ public void setRegistrationPath(String registrationPath) {
+ this.registrationPath = Optional.of(registrationPath);
}
public Optional isDiscoveryEnabled() {
@@ -792,4 +319,12 @@ public OptionalInt getMaxPoolSize() {
public void setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = OptionalInt.of(maxPoolSize);
}
+
+ public Optional getDiscoveryEnabled() {
+ return discoveryEnabled;
+ }
+
+ 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/OidcCommonUtils.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java
index 0f0881c594a9a8..3651d01aaf3868 100644
--- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java
+++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java
@@ -41,9 +41,9 @@
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Provider;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Provider;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret;
import io.quarkus.oidc.common.runtime.OidcCommonConfig.Tls.Verification;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.runtime.util.ClassPathUtils;
@@ -88,7 +88,7 @@ public static void verifyEndpointUrl(String endpointUrl) {
}
}
- public static void verifyCommonConfiguration(OidcCommonConfig oidcConfig, boolean clientIdOptional,
+ public static void verifyCommonConfiguration(OidcClientCommonConfig oidcConfig, boolean clientIdOptional,
boolean isServerConfig) {
final String configPrefix = isServerConfig ? "quarkus.oidc." : "quarkus.oidc-client.";
if (!clientIdOptional && !oidcConfig.getClientId().isPresent()) {
@@ -386,7 +386,7 @@ public static Key clientJwtKey(Credentials creds) {
}
}
- public static String signJwtWithKey(OidcCommonConfig oidcConfig, String tokenRequestUri, Key key) {
+ public static String signJwtWithKey(OidcClientCommonConfig oidcConfig, String tokenRequestUri, Key key) {
// 'jti' and 'iat' claims are created by default, 'iat' - is set to the current time
JwtSignatureBuilder builder = Jwt
.claims(additionalClaims(oidcConfig.credentials.jwt.getClaims()))
@@ -440,7 +440,7 @@ public static void verifyConfigurationId(String defaultId, String configKey, Opt
}
- public static String initClientSecretBasicAuth(OidcCommonConfig oidcConfig) {
+ public static String initClientSecretBasicAuth(OidcClientCommonConfig oidcConfig) {
if (isClientSecretBasicAuthRequired(oidcConfig.credentials)) {
return basicSchemeValue(oidcConfig.getClientId().get(), clientSecret(oidcConfig.credentials));
}
@@ -453,7 +453,7 @@ public static String basicSchemeValue(String name, String secret) {
}
- public static Key initClientJwtKey(OidcCommonConfig oidcConfig) {
+ public static Key initClientJwtKey(OidcClientCommonConfig oidcConfig) {
if (isClientJwtAuthRequired(oidcConfig.credentials)) {
return clientJwtKey(oidcConfig.credentials);
}
diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java
index 2fca611b957ca9..8dc67f4f41e115 100644
--- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java
+++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java
@@ -76,4 +76,13 @@ public final class OidcConstants {
public static final String OPENID_SCOPE = "openid";
public static final String NONCE = "nonce";
+
+ public static final String REGISTRATION_CLIENT_URI = "registration_client_uri";
+ public static final String REGISTRATION_ACCESS_TOKEN = "registration_access_token";
+
+ public static final String CLIENT_METADATA_CLIENT_NAME = "client_name";
+ public static final String CLIENT_METADATA_REDIRECT_URIS = "redirect_uris";
+ public static final String CLIENT_METADATA_POST_LOGOUT_URIS = "post_logout_redirect_uris";
+ public static final String CLIENT_METADATA_SECRET_EXPIRES_AT = "client_secret_expires_at";
+ public static final String CLIENT_METADATA_ID_ISSUED_AT = "client_id_issued_at";
}
diff --git a/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java b/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java
index ae280acf797f48..43cdb78f429a7f 100644
--- a/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java
+++ b/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java
@@ -51,7 +51,7 @@ public void testProxyOptionsWithHostWithScheme() throws Exception {
@Test
public void testJwtTokenWithScope() throws Exception {
- OidcCommonConfig cfg = new OidcCommonConfig();
+ OidcClientCommonConfig cfg = new OidcClientCommonConfig();
cfg.setClientId("client");
cfg.credentials.jwt.claims.put("scope", "read,write");
PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java
index a0441497fff318..ba3cef2cbc0488 100644
--- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java
+++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java
@@ -150,6 +150,27 @@ public class DevServicesConfig {
@ConfigItem(defaultValue = "true")
public boolean createRealm;
+ /**
+ * Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them as
+ * `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` properties, if the {@link #createRealm} property is set to
+ * true.
+ *
+ * Set to `false` if clients have to be created using either the Keycloak Administration Console or
+ * the Keycloak Admin API provided by {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager}
+ * or registered dynamically.
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean createClient;
+
+ /**
+ * Specifies whether to start the container even if the default OIDC tenant is disabled.
+ *
+ * Setting this property to true may be necessary in a multi-tenant OIDC setup, especially when OIDC tenants are created
+ * dynamically.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean startWithDisabledTenant = false;
+
/**
* A map of Keycloak usernames to passwords.
*
diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java
index 9086c453f4e278..3296c8295661c6 100644
--- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java
+++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java
@@ -270,8 +270,8 @@ private Map prepareConfiguration(
boolean createDefaultRealm = (realmReps == null || realmReps.isEmpty()) && capturedDevServicesConfiguration.createRealm;
- String oidcClientId = getOidcClientId(createDefaultRealm);
- String oidcClientSecret = getOidcClientSecret(createDefaultRealm);
+ String oidcClientId = getOidcClientId();
+ String oidcClientSecret = getOidcClientSecret();
String oidcApplicationType = getOidcApplicationType();
Map users = getUsers(capturedDevServicesConfiguration.users, createDefaultRealm);
@@ -307,8 +307,10 @@ private Map prepareConfiguration(
configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl);
configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl);
configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType);
- configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId);
- configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret);
+ if (capturedDevServicesConfiguration.createClient) {
+ configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId);
+ configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret);
+ }
configProperties.put(OIDC_USERS, users.entrySet().stream()
.map(e -> e.toString()).collect(Collectors.joining(",")));
configProperties.put(KEYCLOAK_REALMS, realmNames.stream().collect(Collectors.joining(",")));
@@ -337,7 +339,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild
LOG.debug("Not starting Dev Services for Keycloak as it has been disabled in the config");
return null;
}
- if (!isOidcTenantEnabled()) {
+ if (!isOidcTenantEnabled() && !capturedDevServicesConfiguration.startWithDisabledTenant) {
LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.tenant.enabled' is false");
return null;
}
@@ -665,7 +667,9 @@ private void createDefaultRealm(WebClient client, String token, String keycloakU
List errors) {
RealmRepresentation realm = createDefaultRealmRep();
- realm.getClients().add(createClient(oidcClientId, oidcClientSecret));
+ if (capturedDevServicesConfiguration.createClient) {
+ realm.getClients().add(createClient(oidcClientId, oidcClientSecret));
+ }
for (Map.Entry entry : users.entrySet()) {
realm.getUsers().add(createUser(entry.getKey(), entry.getValue(), getUserRoles(entry.getKey())));
}
@@ -848,13 +852,17 @@ private static String getOidcApplicationType() {
return ConfigProvider.getConfig().getOptionalValue(APPLICATION_TYPE_CONFIG_KEY, String.class).orElse("service");
}
- private static String getOidcClientId(boolean createRealm) {
+ private static String getOidcClientId() {
+ boolean isService = "service".equals(getOidcApplicationType());
+ // if the application type is web-app or hybrid, OidcRecorder will enforce that the client id and secret are configured
return ConfigProvider.getConfig().getOptionalValue(CLIENT_ID_CONFIG_KEY, String.class)
- .orElse(createRealm ? "quarkus-app" : "");
+ .orElse(!isService ? "quarkus-app" : "");
}
- private static String getOidcClientSecret(boolean createRealm) {
+ private static String getOidcClientSecret() {
+ boolean isService = "service".equals(getOidcApplicationType());
+ // if the application type is web-app or hybrid, OidcRecorder will enforce that the client id and secret are configured
return ConfigProvider.getConfig().getOptionalValue(CLIENT_SECRET_CONFIG_KEY, String.class)
- .orElse(createRealm ? "secret" : "");
+ .orElse(!isService ? "secret" : "");
}
}
diff --git a/extensions/oidc/runtime/pom.xml b/extensions/oidc/runtime/pom.xml
index 6f14545605b5ed..25bb4a266db359 100644
--- a/extensions/oidc/runtime/pom.xml
+++ b/extensions/oidc/runtime/pom.xml
@@ -62,7 +62,7 @@
io.quarkus
quarkus-extension-maven-plugin
-
+
io.quarkus.oidc
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java
index 17d7998233c19e..9281aa23e226e1 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java
@@ -15,6 +15,7 @@ public class OidcConfigurationMetadata {
public static final String JWKS_ENDPOINT = "jwks_uri";
public static final String USERINFO_ENDPOINT = "userinfo_endpoint";
public static final String END_SESSION_ENDPOINT = "end_session_endpoint";
+ private static final String REGISTRATION_ENDPOINT = "registration_endpoint";
public static final String SCOPES_SUPPORTED = "scopes_supported";
private final String discoveryUri;
@@ -24,6 +25,7 @@ public class OidcConfigurationMetadata {
private final String jsonWebKeySetUri;
private final String userInfoUri;
private final String endSessionUri;
+ private final String registrationUri;
private final String issuer;
private final JsonObject json;
@@ -33,6 +35,7 @@ public OidcConfigurationMetadata(String tokenUri,
String jsonWebKeySetUri,
String userInfoUri,
String endSessionUri,
+ String registrationUri,
String issuer) {
this.discoveryUri = null;
this.tokenUri = tokenUri;
@@ -41,6 +44,7 @@ public OidcConfigurationMetadata(String tokenUri,
this.jsonWebKeySetUri = jsonWebKeySetUri;
this.userInfoUri = userInfoUri;
this.endSessionUri = endSessionUri;
+ this.registrationUri = registrationUri;
this.issuer = issuer;
this.json = null;
}
@@ -64,6 +68,8 @@ public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMe
localMetadataConfig == null ? null : localMetadataConfig.userInfoUri);
this.endSessionUri = getMetadataValue(wellKnownConfig, END_SESSION_ENDPOINT,
localMetadataConfig == null ? null : localMetadataConfig.endSessionUri);
+ this.registrationUri = getMetadataValue(wellKnownConfig, REGISTRATION_ENDPOINT,
+ localMetadataConfig == null ? null : localMetadataConfig.registrationUri);
this.issuer = getMetadataValue(wellKnownConfig, ISSUER,
localMetadataConfig == null ? null : localMetadataConfig.issuer);
this.json = wellKnownConfig;
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 290121ce29559b..a5cf9a739d8205 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
@@ -9,6 +9,7 @@
import java.util.Optional;
import java.util.OptionalInt;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig;
import io.quarkus.oidc.common.runtime.OidcCommonConfig;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcConfig;
@@ -20,7 +21,7 @@
import io.quarkus.security.identity.SecurityIdentityAugmentor;
@ConfigGroup
-public class OidcTenantConfig extends OidcCommonConfig {
+public class OidcTenantConfig extends OidcClientCommonConfig {
/**
* A unique tenant identifier. It can be set by {@code TenantConfigResolver} providers, which
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java
index 109d675f47652a..3b7698ee79114f 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java
@@ -5,14 +5,14 @@
import jakarta.json.JsonObject;
+import io.quarkus.oidc.common.runtime.AbstractJsonObject;
import io.quarkus.oidc.common.runtime.OidcConstants;
-import io.quarkus.oidc.runtime.AbstractJsonObjectResponse;
/**
* Represents a token introspection result
*
*/
-public class TokenIntrospection extends AbstractJsonObjectResponse {
+public class TokenIntrospection extends AbstractJsonObject {
public TokenIntrospection() {
}
@@ -64,6 +64,6 @@ public String getClientId() {
}
public String getIntrospectionString() {
- return getNonNullJsonString();
+ return getJsonString();
}
}
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java
index 766c86c3f31d21..befd307d2a4933 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java
@@ -4,9 +4,9 @@
import org.eclipse.microprofile.jwt.Claims;
-import io.quarkus.oidc.runtime.AbstractJsonObjectResponse;
+import io.quarkus.oidc.common.runtime.AbstractJsonObject;
-public class UserInfo extends AbstractJsonObjectResponse {
+public class UserInfo extends AbstractJsonObject {
private static final String EMAIL = "email";
private static final String NAME = "name";
@@ -26,7 +26,7 @@ public UserInfo(JsonObject json) {
}
public String getUserInfoString() {
- return getNonNullJsonString();
+ return getJsonString();
}
public String getName() {
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
index 7ce98b95596064..8ef9891a04f093 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java
@@ -37,6 +37,7 @@
import io.quarkus.oidc.Redirect;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.UserInfo;
+import io.quarkus.oidc.common.runtime.AbstractJsonObject;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationCompletionException;
@@ -72,6 +73,7 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha
static final String STATE_COOKIE_RESTORE_PATH = "restore-path";
static final Uni VOID_UNI = Uni.createFrom().voidItem();
static final String NO_OIDC_COOKIES_AVAILABLE = "no_oidc_cookies";
+ static final String HTTP_SCHEME = "http";
private static final String INTERNAL_IDTOKEN_HEADER = "internal";
private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class);
@@ -964,7 +966,7 @@ private String generateInternalIdToken(TenantConfigContext context, UserInfo use
Long accessTokenExpiresInSecs) {
JwtClaimsBuilder builder = Jwt.claims();
if (currentIdToken != null) {
- AbstractJsonObjectResponse currentIdTokenJson = new AbstractJsonObjectResponse(
+ AbstractJsonObject currentIdTokenJson = new AbstractJsonObject(
OidcUtils.decodeJwtContentAsString(currentIdToken)) {
};
for (String claim : currentIdTokenJson.getPropertyNames()) {
@@ -1205,6 +1207,9 @@ static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcCo
}
private String buildUri(RoutingContext context, boolean forceHttps, String path) {
+ if (path.startsWith(HTTP_SCHEME)) {
+ return path;
+ }
String authority = URI.create(context.request().absoluteURI()).getAuthority();
return buildUri(context, forceHttps, authority, path);
}
@@ -1347,12 +1352,19 @@ private Uni getCodeFlowTokensUni(RoutingContext context
String code, String codeVerifier) {
// 'redirect_uri': it must match the 'redirect_uri' query parameter which was used during the code request.
- String redirectPath = getRedirectPath(configContext.oidcConfig, context);
- if (configContext.oidcConfig.authentication.redirectPath.isPresent()
- && !configContext.oidcConfig.authentication.redirectPath.get().equals(context.request().path())) {
- LOG.warnf("Token redirect path %s does not match the current request path", context.request().path());
- return Uni.createFrom().failure(new AuthenticationFailedException("Wrong redirect path"));
+ Optional configuredRedirectPath = configContext.oidcConfig.authentication.redirectPath;
+ if (configuredRedirectPath.isPresent()) {
+ String requestPath = configuredRedirectPath.get().startsWith(HTTP_SCHEME)
+ ? buildUri(context, configContext.oidcConfig.authentication.forceRedirectHttpsScheme.orElse(false),
+ context.request().path())
+ : context.request().path();
+ if (!configuredRedirectPath.get().equals(requestPath)) {
+ LOG.warnf("Token redirect path %s does not match the current request path", requestPath);
+ return Uni.createFrom().failure(new AuthenticationFailedException("Wrong redirect path"));
+ }
}
+
+ String redirectPath = getRedirectPath(configContext.oidcConfig, context);
String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath);
LOG.debugf("Token request redirect_uri parameter: %s", redirectUriParam);
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java
index 1b42daf190431f..0aa05ccaa0c5d9 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java
@@ -39,6 +39,7 @@
import io.quarkus.oidc.TokenCustomizer;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.oidc.UserInfo;
+import io.quarkus.oidc.common.runtime.AbstractJsonObject;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcProviderClient.UserInfoResponse;
@@ -277,7 +278,7 @@ private TokenVerificationResult verifyJwtTokenInternal(String token,
private String customizeJwtToken(String token) {
if (tokenCustomizer != null) {
- JsonObject headers = AbstractJsonObjectResponse.toJsonObject(
+ JsonObject headers = AbstractJsonObject.toJsonObject(
OidcUtils.decodeJwtHeadersAsString(token));
headers = tokenCustomizer.customizeHeaders(headers);
if (headers != null) {
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java
index bcd9db16df7618..513c0de323a681 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java
@@ -18,7 +18,7 @@
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret.Method;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.smallrye.mutiny.Uni;
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 a07b003bc6a541..83398259ad3091 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
@@ -604,8 +604,9 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi
String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath);
String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath);
String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath);
+ String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath);
return new OidcConfigurationMetadata(tokenUri,
- introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri,
+ introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri,
oidcConfig.token.issuer.orElse(null));
}
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java
index a87f909dbbef2a..9a1e95130b5e01 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/providers/KnownOidcProviders.java
@@ -4,7 +4,7 @@
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret.Method;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
public class KnownOidcProviders {
diff --git a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java
index fc593643f13958..93571c0a964873 100644
--- a/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java
+++ b/extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/KnownOidcProvidersTest.java
@@ -12,7 +12,7 @@
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.OidcTenantConfig.Authentication.ResponseMode;
import io.quarkus.oidc.OidcTenantConfig.Provider;
-import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method;
+import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret.Method;
import io.quarkus.oidc.runtime.providers.KnownOidcProviders;
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
diff --git a/extensions/pom.xml b/extensions/pom.xml
index f4dd9ca4af42a0..c11bbaa9679a13 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -142,6 +142,7 @@
oidc-client
oidc-client-filter
oidc-client-reactive-filter
+ oidc-client-registration
oidc-client-graphql
oidc-token-propagation
oidc-token-propagation-reactive
diff --git a/integration-tests/oidc-client-registration/pom.xml b/integration-tests/oidc-client-registration/pom.xml
new file mode 100644
index 00000000000000..4bca0039beda59
--- /dev/null
+++ b/integration-tests/oidc-client-registration/pom.xml
@@ -0,0 +1,159 @@
+
+
+
+ quarkus-integration-tests-parent
+ io.quarkus
+ 999-SNAPSHOT
+ ../
+
+ 4.0.0
+
+ quarkus-integration-test-oidc-client-registration
+ Quarkus - Integration Tests - OIDC Client Registration
+ Module that tests dynamic OIDC Client Registration
+
+
+
+ io.quarkus
+ quarkus-rest
+
+
+ io.quarkus
+ quarkus-rest-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+ io.quarkus
+ quarkus-oidc
+
+
+ io.quarkus
+ quarkus-oidc-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+ io.quarkus
+ quarkus-oidc-client-registration
+
+
+ io.quarkus
+ quarkus-oidc-client-registration-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.htmlunit
+ htmlunit
+ test
+
+
+
+
+
+
+ src/main/resources
+ true
+
+
+
+
+ maven-surefire-plugin
+
+ true
+
+
+
+ maven-failsafe-plugin
+
+ true
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+ test-keycloak
+
+
+ test-containers
+
+
+
+
+
+ maven-surefire-plugin
+
+ false
+
+
+
+ maven-failsafe-plugin
+
+ false
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+
diff --git a/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java
new file mode 100644
index 00000000000000..25f74805c2aa2b
--- /dev/null
+++ b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java
@@ -0,0 +1,169 @@
+package io.quarkus.it.keycloak;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+import jakarta.json.Json;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+import io.quarkus.oidc.OidcRequestContext;
+import io.quarkus.oidc.OidcTenantConfig;
+import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
+import io.quarkus.oidc.TenantConfigResolver;
+import io.quarkus.oidc.client.registration.ClientMetadata;
+import io.quarkus.oidc.client.registration.OidcClientRegistration;
+import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig;
+import io.quarkus.oidc.client.registration.OidcClientRegistrations;
+import io.quarkus.oidc.client.registration.RegisteredClient;
+import io.quarkus.oidc.common.runtime.OidcConstants;
+import io.quarkus.runtime.StartupEvent;
+import io.smallrye.mutiny.Uni;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class CustomTenantConfigResolver implements TenantConfigResolver {
+
+ @Inject
+ OidcClientRegistration clientReg;
+
+ @Inject
+ OidcClientRegistrations clientRegs;
+
+ @Inject
+ @ConfigProperty(name = "quarkus.oidc.auth-server-url")
+ String authServerUrl;
+
+ volatile RegisteredClient defaultRegClientOnStartup;
+ volatile RegisteredClient tenantRegClientOnStartup;
+ volatile RegisteredClient regClientDynamically;
+ volatile RegisteredClient regClientDynamicTenant;
+
+ volatile Map regClientsMulti;
+
+ void onStartup(@Observes StartupEvent event) {
+
+ // Default OIDC client registration, client is registered at startup
+ defaultRegClientOnStartup = clientReg.registeredClient().await().indefinitely();
+ if (!"Default Client".equals(defaultRegClientOnStartup.metadata().getClientName())) {
+ throw new RuntimeException("Unexpected cient name");
+ }
+
+ // Confirm that access to the client-specific registration endpoint works.
+ defaultRegClientOnStartup = defaultRegClientOnStartup.update(
+ ClientMetadata.builder().clientName("Default Client Updated").build()).await()
+ .indefinitely();
+
+ // Read using RegisteredClient.read
+ RegisteredClient defaultRegClientOnStartup2 = defaultRegClientOnStartup.read().await().indefinitely();
+
+ // Read using OidcClientRegistration.readClient(regUri, regToken)
+ defaultRegClientOnStartup = clientReg
+ .readClient(defaultRegClientOnStartup.registrationUri(),
+ defaultRegClientOnStartup.registrationToken())
+ .await().indefinitely();
+
+ if (!defaultRegClientOnStartup2.metadata().getClientId().equals(
+ defaultRegClientOnStartup2.metadata().getClientId())) {
+ throw new RuntimeException("Inconsistent read results");
+ }
+
+ // Custom 'tenant-client' OIDC client registration, client is registered at startup
+ tenantRegClientOnStartup = clientRegs.getClientRegistration("tenant-client").registeredClient()
+ .await().indefinitely();
+
+ // Two custom OIDC client registrations registered right now using the same registration endpoint
+ // as one which was used to register defaultRegClientOnStartup and defaultRegClientOnStartup clients
+ // at startup
+ ClientMetadata clientMetadataMulti1 = createMetadata("http://localhost:8081/protected/multi1", "Multi1 Client");
+ ClientMetadata clientMetadataMulti2 = createMetadataWithBuilder("http://localhost:8081/protected/multi2",
+ "Multi2 Client");
+
+ Uni