From 36d84b440d966f017e8176934b11763ea16c1f66 Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 8 Feb 2024 18:38:15 +0000 Subject: [PATCH] Add a quarkus-oidc-client-registration extension --- bom/application/pom.xml | 10 + .../java/io/quarkus/deployment/Feature.java | 1 + devtools/bom-descriptor-json/pom.xml | 13 + docs/pom.xml | 13 + ...ty-openid-connect-client-registration.adoc | 513 ++++ .../deployment/pom.xml | 96 + .../OidcClientRegistrationBuildStep.java | 67 + ...OidcClientRegistrationBuildTimeConfig.java | 16 + extensions/oidc-client-registration/pom.xml | 19 + .../oidc-client-registration/runtime/pom.xml | 69 + .../client/registration/ClientMetadata.java | 115 + .../registration/OidcClientRegistration.java | 44 + .../OidcClientRegistrationConfig.java | 76 + .../registration/OidcClientRegistrations.java | 42 + .../client/registration/RegisteredClient.java | 51 + ...sabledOidcClientRegistrationException.java | 24 + .../OidcClientRegistrationException.java | 24 + .../runtime/OidcClientRegistrationImpl.java | 245 ++ .../OidcClientRegistrationRecorder.java | 273 +++ .../OidcClientRegistrationsConfig.java | 28 + .../runtime/OidcClientRegistrationsImpl.java | 56 + .../runtime/RegisteredClientImpl.java | 227 ++ .../resources/META-INF/quarkus-extension.yaml | 16 + .../quarkus/oidc/client/OidcClientConfig.java | 4 +- .../oidc/client/runtime/OidcClientImpl.java | 2 +- .../io/quarkus/oidc/common/OidcEndpoint.java | 10 +- .../common/runtime/AbstractJsonObject.java} | 33 +- .../runtime/OidcClientCommonConfig.java | 497 ++++ .../oidc/common/runtime/OidcCommonConfig.java | 497 +--- .../oidc/common/runtime/OidcCommonUtils.java | 14 +- .../oidc/common/runtime/OidcConstants.java | 9 + .../common/runtime/OidcCommonUtilsTest.java | 2 +- .../keycloak/DevServicesConfig.java | 21 + .../KeycloakDevServicesProcessor.java | 28 +- extensions/oidc/runtime/pom.xml | 2 +- .../oidc/OidcConfigurationMetadata.java | 6 + .../io/quarkus/oidc/OidcTenantConfig.java | 3 +- .../io/quarkus/oidc/TokenIntrospection.java | 6 +- .../main/java/io/quarkus/oidc/UserInfo.java | 6 +- .../runtime/CodeAuthenticationMechanism.java | 24 +- .../io/quarkus/oidc/runtime/OidcProvider.java | 3 +- .../oidc/runtime/OidcProviderClient.java | 2 +- .../io/quarkus/oidc/runtime/OidcRecorder.java | 3 +- .../runtime/providers/KnownOidcProviders.java | 2 +- .../oidc/runtime/KnownOidcProvidersTest.java | 2 +- extensions/pom.xml | 1 + .../oidc-client-registration/pom.xml | 159 ++ .../keycloak/CustomTenantConfigResolver.java | 169 ++ .../it/keycloak/ProtectedResource.java | 69 + .../src/main/resources/application.properties | 19 + .../src/main/resources/quarkus-realm.json | 2068 +++++++++++++++++ .../OidcClientRegistrationInGraalITCase.java | 7 + .../keycloak/OidcClientRegistrationTest.java | 133 ++ .../keycloak/CustomTenantConfigResolver.java | 4 +- integration-tests/pom.xml | 1 + 55 files changed, 5312 insertions(+), 532 deletions(-) create mode 100644 docs/src/main/asciidoc/security-openid-connect-client-registration.adoc create mode 100644 extensions/oidc-client-registration/deployment/pom.xml create mode 100644 extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java create mode 100644 extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java create mode 100644 extensions/oidc-client-registration/pom.xml create mode 100644 extensions/oidc-client-registration/runtime/pom.xml create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java create mode 100644 extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml rename extensions/{oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java => oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java} (68%) create mode 100644 extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java create mode 100644 integration-tests/oidc-client-registration/pom.xml create mode 100644 integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java create mode 100644 integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java create mode 100644 integration-tests/oidc-client-registration/src/main/resources/application.properties create mode 100644 integration-tests/oidc-client-registration/src/main/resources/quarkus-realm.json create mode 100644 integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationInGraalITCase.java create mode 100644 integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 1cd532072fcd9..1ee620dd88d0e 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 58e502a49d66e..497136eab4c92 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 32cf89bc60284..59f92096d0541 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 9c26e51cd450d..ee82231a6665a 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 0000000000000..0bb4a77c61742 --- /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 0000000000000..e4720ae98627a --- /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 0000000000000..f0d71493b33d2 --- /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 0000000000000..98d78afa9acf1 --- /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 0000000000000..0ccf96d1d9c77 --- /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 0000000000000..5a7b0038d55bb --- /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 0000000000000..832460f2c0b1b --- /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 0000000000000..0c650f498c42d --- /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 0000000000000..871d3a515c4c7 --- /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 0000000000000..4a5b8801803da --- /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 0000000000000..1181d5e880d73 --- /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 0000000000000..d83149887269a --- /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 0000000000000..67f2378bb5a57 --- /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 0000000000000..6877fb56e6672 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java @@ -0,0 +1,245 @@ +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 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) { + LOG.debug("Failed to close the client", 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 0000000000000..73b91bb5c4aff --- /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 0000000000000..5d2772fdc7fd7 --- /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 0000000000000..7f4fd68fedeff --- /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 0000000000000..c1357f837e06f --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java @@ -0,0 +1,227 @@ +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) { + LOG.debug("Failed to close the client", 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 0000000000000..b397f0b097059 --- /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 5b40ddb43d9c6..f6c180a979e64 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 11c65e893cf2c..20a3b6d186e8b 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 362580ebf5323..18591c069964c 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 416848f07fcf2..9959b2292f6ad 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 0000000000000..6150000e6837a --- /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 d744d88358f84..24f3e3b41f95b 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 0f0881c594a9a..3651d01aaf386 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 2fca611b957ca..8dc67f4f41e11 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 ae280acf797f4..43cdb78f429a7 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 a0441497fff31..ba3cef2cbc048 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 9086c453f4e27..3296c8295661c 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 6f14545605b5e..25bb4a266db35 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 17d7998233c19..9281aa23e226e 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 290121ce29559..a5cf9a739d820 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 109d675f47652..3b7698ee79114 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 766c86c3f31d2..befd307d2a493 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 7ce98b9559606..8ef9891a04f09 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 1b42daf190431..0aa05ccaa0c5d 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 bcd9db16df761..513c0de323a68 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 a07b003bc6a54..83398259ad309 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 a87f909dbbef2..9a1e95130b5e0 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 fc593643f1395..93571c0a96487 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 f4dd9ca4af42a..c11bbaa9679a1 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 0000000000000..4bca0039beda5 --- /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 0000000000000..25f74805c2aa2 --- /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> clients = clientReg + .registerClients(List.of(clientMetadataMulti1, clientMetadataMulti2)) + .collect().asMap(r -> URI.create(r.metadata().getRedirectUris().get(0)).getPath(), r -> r); + regClientsMulti = clients.await().indefinitely(); + } + + @Override + public Uni resolve(RoutingContext routingContext, + OidcRequestContext requestContext) { + if (routingContext.request().path().endsWith("/protected")) { + return Uni.createFrom().item(createTenantConfig("registered-client", defaultRegClientOnStartup.metadata())); + } else if (routingContext.request().path().endsWith("/protected/tenant")) { + return Uni.createFrom().item(createTenantConfig("registered-client-tenant", tenantRegClientOnStartup.metadata())); + } else if (routingContext.request().path().endsWith("/protected/dynamic")) { + // New client registration done dynamically at the request time, + // using the same registration endpoint used to register a default client at startup + OidcClientRegistrationConfig clientRegConfig = new OidcClientRegistrationConfig(); + clientRegConfig.registrationPath = Optional.of( + authServerUrl + "/clients-registrations/openid-connect"); + clientRegConfig.metadata.redirectUri = Optional.of("http://localhost:8081/protected/dynamic"); + clientRegConfig.metadata.clientName = Optional.of("Dynamic Client"); + + return clientRegs.newClientRegistration(clientRegConfig) + .onItem().transformToUni(cfg -> cfg.registeredClient()) + .onItem().transform(r -> registeredClientDynamically(r)); + } else if (routingContext.request().path().endsWith("/protected/dynamic-tenant")) { + // New client registration done dynamically at the request time, using a new configured + // an OIDC tenant specific registration endpoint + OidcClientRegistration tenantClientReg = clientRegs.getClientRegistration("dynamic-tenant"); + ClientMetadata metadata = createMetadata("http://localhost:8081/protected/dynamic-tenant", + "Dynamic Tenant Client"); + + return tenantClientReg.registerClient(metadata) + .onItem().transform(r -> registeredClientDynamicTenant(r)); + } else if (routingContext.request().path().endsWith("/protected/multi1")) { + return Uni.createFrom().item(createTenantConfig("registered-client-multi1", + regClientsMulti.get("/protected/multi1").metadata())); + } else if (routingContext.request().path().endsWith("/protected/multi2")) { + return Uni.createFrom().item(createTenantConfig("registered-client-multi2", + regClientsMulti.get("/protected/multi2").metadata())); + } + + return null; + } + + private OidcTenantConfig registeredClientDynamically(RegisteredClient newClient) { + + regClientDynamically = newClient; + + return createTenantConfig("registered-client-dynamically", regClientDynamically.metadata()); + } + + private OidcTenantConfig registeredClientDynamicTenant(RegisteredClient newClient) { + + regClientDynamicTenant = newClient; + + return createTenantConfig("registered-client-dynamic-tenant", regClientDynamicTenant.metadata()); + } + + 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 new ClientMetadata(Json.createObjectBuilder() + .add(OidcConstants.CLIENT_METADATA_REDIRECT_URIS, Json.createArrayBuilder().add(redirectUri)) + .add(OidcConstants.CLIENT_METADATA_CLIENT_NAME, clientName) + .build()); + } + + protected static ClientMetadata createMetadataWithBuilder(String redirectUri, String clientName) { + return ClientMetadata.builder().redirectUri(redirectUri).clientName(clientName).build(); + } +} diff --git a/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java new file mode 100644 index 0000000000000..23501f0d2ef23 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/ProtectedResource.java @@ -0,0 +1,69 @@ +package io.quarkus.it.keycloak; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.oidc.OidcSession; +import io.quarkus.oidc.OidcTenantConfig; +import io.quarkus.oidc.runtime.TenantConfigBean; +import io.quarkus.security.Authenticated; + +@Path("/protected") +@Authenticated +public class ProtectedResource { + + @Inject + @IdToken + JsonWebToken principal; + + @Inject + OidcSession session; + + @Inject + TenantConfigBean tenantConfigBean; + + @GET + public String principalName() { + return session.getTenantId() + ":" + getClientName() + ":" + principal.getName(); + } + + @GET + @Path("/tenant") + public String tenantPrincipalName() { + return session.getTenantId() + ":" + getClientName() + ":" + principal.getName(); + } + + @GET + @Path("/dynamic") + public String dynamicPrincipalName() { + return session.getTenantId() + ":" + getClientName() + ":" + principal.getName(); + } + + @GET + @Path("/dynamic-tenant") + public String dynamicTenantPrincipalName() { + return session.getTenantId() + ":" + getClientName() + ":" + principal.getName(); + } + + @GET + @Path("/multi1") + public String principalNameMulti1() { + return session.getTenantId() + ":" + getClientName() + ":" + principal.getName(); + } + + @GET + @Path("/multi2") + public String principalNameMulti2() { + return session.getTenantId() + ":" + getClientName() + ":" + principal.getName(); + } + + private String getClientName() { + OidcTenantConfig oidcConfig = tenantConfigBean.getDynamicTenantsConfig().get(session.getTenantId()) + .getOidcTenantConfig(); + return oidcConfig.getClientName().get(); + } +} diff --git a/integration-tests/oidc-client-registration/src/main/resources/application.properties b/integration-tests/oidc-client-registration/src/main/resources/application.properties new file mode 100644 index 0000000000000..3375eb5d00136 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/resources/application.properties @@ -0,0 +1,19 @@ +quarkus.keycloak.devservices.realm-path=quarkus-realm.json +quarkus.keycloak.devservices.start-with-disabled-tenant=true +quarkus.keycloak.devservices.show-logs=true + +quarkus.oidc.tenant-enabled=false +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 + +quarkus.oidc-client-registration.dynamic-tenant.auth-server-url=${quarkus.oidc.auth-server-url} + +quarkus.oidc-client-registration.tenant-client.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client-registration.tenant-client.register-early=false +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.log.category."org.htmlunit".level=ERROR +quarkus.log.category."io.quarkus.oidc.runtime".min-level=TRACE +quarkus.log.category."io.quarkus.oidc.runtime".level=TRACE diff --git a/integration-tests/oidc-client-registration/src/main/resources/quarkus-realm.json b/integration-tests/oidc-client-registration/src/main/resources/quarkus-realm.json new file mode 100644 index 0000000000000..363a508e6d625 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/resources/quarkus-realm.json @@ -0,0 +1,2068 @@ +{ + "id": "quarkus", + "realm": "quarkus", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "3ce83241-464b-4ca0-8f0f-17002a797aab", + "name": "admin", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "68615956-51ca-49ca-865a-f9cb2571b027", + "name": "confidential", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "c6d57a00-eb97-460d-91b0-89e6a94a7aa5", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "c50286f6-3562-473f-ad45-9767b982ff45", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + }, + { + "id": "d3246456-8f5d-4722-8364-a46a8d25dc7c", + "name": "user", + "composite": false, + "clientRole": false, + "containerId": "quarkus", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "4b24739e-3a0a-48d2-b202-713430d775d2", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "bcc6637a-294c-4529-a706-33b8c49f40fc", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "1238e880-907f-4e8b-a032-4d09a922adf8", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "183e58f4-136b-4c91-b20a-5c76857a671e", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "f65a9a54-d689-4c45-87cd-f177babdeaef", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "9aec187f-d623-45c7-a8b3-5aa32d115f50", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "52521d81-e7d6-4929-95cb-0a084c5bacb8", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "e92c753a-7b17-4adc-9962-04f24040e404", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "1285d11d-08f4-4753-b27e-d5f7b0e76fca", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "b0ee027f-5aa6-48eb-837f-4635590576ec", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "f1176efb-e24b-4fab-8b37-8265aefd10e1", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "61ac3405-ccbd-4cdf-8cac-c918e1d77e1f", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "968be265-6868-416a-91a1-e5bd882349ab", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "e77611fc-5ec5-4438-96c3-b291aae78d0c", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "f5163480-f5fc-4355-8be1-8cc96ff7d99d", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "manage-identity-providers", + "query-clients", + "view-users", + "view-identity-providers", + "view-events", + "view-clients", + "manage-events", + "query-realms", + "manage-clients", + "view-authorization", + "query-groups", + "query-users", + "manage-authorization", + "manage-users", + "manage-realm", + "create-client", + "view-realm", + "impersonation" + ] + } + }, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "165b24e1-9488-4cc7-87cd-e74b1cdc5619", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "6e633885-b1fb-4ca8-9ef9-7c4c8f8732e8", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "64ec1233-2cee-4d9b-ab6f-0bd06702c684", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + }, + { + "id": "683bddad-81c6-4dca-87b6-e14b0b2ae524", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "dd29e998-54e9-4067-884e-4f986e990c1d", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "backend-service": [ + { + "id": "5b9947c6-eb74-4de6-8623-0285720993f3", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "302430aa-3929-42cf-8ba2-2b9d2e71dc3a", + "attributes": {} + } + ], + "broker": [ + { + "id": "bee1f77b-34a9-4386-9eca-eb19db248394", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "2a02328b-6aa6-49a8-b56c-7036c273c70b", + "attributes": {} + } + ], + "account": [ + { + "id": "1ffcc7fe-50a8-4300-b172-10f651e5a5bd", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "d3ffeda8-8d57-4b63-ae1d-90f88bc4b068", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + }, + { + "id": "74f86380-8e18-407f-ad16-529044f9c7dc", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "attributes": {} + } + ] + } + }, + "groups": [], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "clients": [ + { + "id": "302430aa-3929-42cf-8ba2-2b9d2e71dc3a", + "clientId": "backend-service", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "secret", + "redirectUris": ["*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "1390addb-ba10-4455-a1ea-8455c3770cf1", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + }, + { + "id": "cdafda09-f6d9-41e3-87ef-6789e861689a", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "95b47211-912c-43f5-84ce-5bfbc761325d", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "dd29e998-54e9-4067-884e-4f986e990c1d", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "c41b709a-a012-4c69-89d7-4f926dba0619", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "c6e812f9-326b-4e66-9197-157a5d43b172", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "a951803a-79c7-46a6-8197-e32835286971", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "35b5a50f-a32a-4bd1-b4b3-50f0ade135c7", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/quarkus/account/", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "0136c3ef-0dfd-4b13-a6d0-2c8b6358edec", + "redirectUris": [ + "/realms/quarkus/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "2a02328b-6aa6-49a8-b56c-7036c273c70b", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "e1f7edd7-e15c-43b4-8736-ff8204d16836", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6517b152-0693-4b28-a798-a0deea3e8644", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/quarkus/console/", + "surrogateAuthRequired": false, + "enabled": true, + "clientAuthenticatorType": "client-secret", + "secret": "e571b211-2550-475d-b87f-116ff54091ee", + "redirectUris": [ + "/admin/quarkus/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "9c7093a9-4da1-47e4-b2a5-afe180782220", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "35bfd94e-681f-456a-bca0-0d0d8d986a96", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1f710637-5a3c-45f3-b4d3-74046993e0eb", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "eb0bdf87-6cda-4684-89a8-f7bd6f0c7bba", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "1ea39fbb-c692-4a1d-a143-a05b030889cb", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "f97bd1de-6c95-4c5b-804c-f8b354457453", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "55621a1e-cd6b-45a7-9f06-a678e0801b9c", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "6c4f32b0-8ae4-4b4b-b4fa-a053df0bbb3a", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + }, + { + "id": "2687cb87-1dbf-435c-8ef9-f2fe38127405", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "97aca0c9-7f14-4783-bb48-681de54f0b31", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "541f2eae-d481-4d00-be30-89f4f60d169f", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "eda935c3-7294-403c-85bd-fee7216af822", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "0b8c0161-5042-4912-a753-c262569ed5bc", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "d20498e8-4ec8-4496-9d8f-c09131dd5d15", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "7da35ca7-5c93-4d23-b6b7-761d80c966c8", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "a443a633-7cd2-406d-85f1-6e3d3173eff9", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "d04d2dd6-04fc-4230-90eb-7074056cfdee", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "ef68a07b-ed0a-418b-9c6d-7ecd58946813", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "144acdba-ee08-4349-b806-a4394bd5f351", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "4b435d62-1f62-4513-a131-208318731d7b", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "794b162d-460a-4465-b90d-66dabc4b3cce", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "779b131a-d0cc-420d-90b3-075b19210379", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "0e0f1e8d-60f9-4435-b753-136d70e56af8", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "8451d26b-904d-4858-9db1-87fe137c1172", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "011fe224-355f-4e3c-a3d4-6a325eec561d", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "06f656a1-67f1-4c53-92df-9e5823853191", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "03293b81-5599-4163-81b8-eb05c3d14ed2", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "d21642b7-8190-4de4-8d0d-09b0e505c02c", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "7eaa8ede-9a92-487a-9444-60a5d7355542", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "e7616dd3-8886-4d47-8645-74e4565d7606", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "83e275f7-b171-45fa-99c7-7c04f91fbe41", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "9eb470cc-8157-46f2-8233-8cae169c6591", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + }, + { + "id": "eebdefd0-c446-4bf3-b945-08db42f0ea92", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "37c62d93-c670-487c-8c3a-a6329a9924b0", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "58e57c6f-18bf-4347-9ab0-b8325ef522e0", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "5a4a2c20-fef2-40b5-9406-136475442b47", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } + ], + "defaultDefaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "defaultOptionalClientScopes": [ + "address", + "phone", + "microprofile-jwt", + "offline_access" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "9b4e5b69-1d07-489b-b8a5-07329c957141", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "trusted-hosts": [ + "172.17.0.1" + ], + "client-uris-must-match": [ + "false" + ] + } + }, + { + "id": "e2f513d3-44e3-435c-8b2a-68a5d384fd97", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "8fe9bd3a-a11c-4c97-948e-90ba7fbe008f", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-address-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "saml-user-attribute-mapper" + ] + } + }, + { + "id": "e9b76eee-365f-4b5f-80cb-316eb07b36fa", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "8ed9d103-7a79-47b4-9426-9e4a84340d22", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "43a5aac2-b395-4935-94cb-12f4d9b4eb05", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-role-list-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "a07e90f1-5662-4344-8529-f284c361a25e", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "4008d665-26c4-4056-a028-232bc0636029", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid" : [ "b04473d3-8395-4016-b455-19a9e951106b" ], + "secret" : [ "x68mMOVdz3qKWzltzReV0g" ], + "priority": [ "100" ] + } + }, + { + "id": "066f8625-06ba-4463-995f-93a058d2d800", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey" : [ "MIIEowIBAAKCAQEAn5T13suF8mlS+pJXp0U1bto41nW55wpcs+Rps8ZVCRyJKWqzwSCYnI7lm0rB2wBpAAO4OPoj1zlmVoFmBPsDU9Xf7rjsJb5LIzIQDCZY44aSDZt6RR+gakPiQvlzHyW/RozYpngDJF7TsTD7rdRF1xQ4RprfBF8fwK/xsU7pxbeom5xDHZhz3fiw8s+7UdbmnazDHfAjU58aUrLGgVRfUsuoHjtsptYlOIXEifaeMetXZE+HhqLYRHQPDap5fbBJl773Trosn7N9nmzN4x1xxGj9So21WC5UboQs9sAIVgizc4omjZ5Y4RN9HLH7G4YwJctNntzmnJhDui9zAO+zSQIDAQABAoIBADi+F7rTtVoft0Cfnok8o6Y58/HVxHdxiMryUd95iy0FN4RBi48FTx6D9QKFz25Ws/8sU2n3D51srIXf1u24b1N0/f39RQKaqk7mcyxOylaEuBQcj5pah4ihgKd92UBfBKdKV5LBo6RgD3e2yhbiHr8+UlBQqzH7vOef6Bm6zIbfmi3N88swAJhP0YizRZFklsbmLsK6nkwyro00CHJvPVKSBbM+ad+/zIBsLw56MvNngB5TuFguUgoljd6M1T2z4utmZGlTUqrfE1onAVLJZoGnRohyIr7dJEg6YxWR70PxsgmkDKyeRvet9P1trO0n+OSprusfrC3cHJStabap1V0CgYEA1A/CtsqTnjdYYsB19eumZgdpzUgNc/YEAzZ/OWb8yTLoB2ncci+63A1rXHUXAqJFY7vtjn5mxv7SuASNbUrzq+6KfZvC1x9XEtnczqT/ypunNfxmIZuj8Nuu6vtURguZ8kPPwdkI8toTizRFeRE5ZDBvoQryiEVYugfHaHT5vzsCgYEAwKWODwquI0Lv9BuwdNVrBXQpkKh3ZfYOA7i9xvhxlM7xUu8OMCwwCPn3r7vrW5APjTqX4h330mJ44SLEs+7gbCUs4BbJBLA6g0ChlHa9PTkxp6tk2nDF/B34fxiZSRkE85L+d+at0Dc3hnlzLCJCzJawGpoPniPU9e4w0p4dN0sCgYAsGnMGjS8SUrRhJWHjGXVr9tK8TOXvXhULjgP7rj2Yoqu7Dvs4DFEyft/7RKbad2EzEtyfLA64CDtO5jN7rYDsGxpWcVSeZPg5BXJ0z8AbJTArfCjJiJMZ/rZsTIUEZFlKF2xYBolj6JLz+pUQTtK+0YwF1D8ItFN1rTR9twZSDQKBgQC6sPXNX+VH6LuPTjIf1x8CxwLs3EXxOpV0R9kp9GRl+HJnk6GlT30xhcThufQo5KAdllXQXIhoiuNoEoCbevhj9Vbax1oBQCNERSMRNEzKAx46xd9TzYwgeo7x5E3QR/3DaoVOfu+cY5ZcrF/PulgP2kxJS1mtQD5GIpGP2oinpwKBgGqiqTFPqRcelx76vBvTU+Jp1zM62T4AotbMrSQR/oUvqHe5Ytj/SbZx+wbbHAiyGgV700Mosyviik83YEAbR3kdOPjgYvAJJW2Y3jEMdQ7MwriXz8XLh5BGmYfVjkSOJXed9ua9WlYLKOJeXXv191BbDvrx5NXuJyVVU4vJx3YZ" ], + "certificate" : [ "MIICnTCCAYUCBgFp4EYIrjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdwcm90ZWFuMB4XDTE5MDQwMjIyNTYxOVoXDTI5MDQwMjIyNTc1OVowEjEQMA4GA1UEAwwHcHJvdGVhbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+U9d7LhfJpUvqSV6dFNW7aONZ1uecKXLPkabPGVQkciSlqs8EgmJyO5ZtKwdsAaQADuDj6I9c5ZlaBZgT7A1PV3+647CW+SyMyEAwmWOOGkg2bekUfoGpD4kL5cx8lv0aM2KZ4AyRe07Ew+63URdcUOEaa3wRfH8Cv8bFO6cW3qJucQx2Yc934sPLPu1HW5p2swx3wI1OfGlKyxoFUX1LLqB47bKbWJTiFxIn2njHrV2RPh4ai2ER0Dw2qeX2wSZe+9066LJ+zfZ5szeMdccRo/UqNtVguVG6ELPbACFYIs3OKJo2eWOETfRyx+xuGMCXLTZ7c5pyYQ7ovcwDvs0kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVtmRKDb4OK5iSA46tagMBkp6L7WuPpCWuHGWwobEP+BecYsShW7zP3s12oA8SNSwbhvu0CRqgzxhuypgf3hKQFVU153Erv4hzkj+8S0s5LR/ZE7tDNY2lzJ3yQKXy3Md7EkuzzvOZ50MTrcSKAanWq/ZW1OTnrtGymj5zGJnTg7mMnJzEIGePxkvPu/QdchiPBLqxfZYm1jsFGY25djOC3N/KmVcRVmPRGuu6D8tBFHlKoPfZYPdbMvsvs24aupHKRcZ+ofTCpK+2Qo8c0pSSqeEYHGmuGqC6lC6ozxtxSABPO9Q1R1tZBU7Kg5HvXUwwmoVS3EGub46YbHqbmWMLg==" ], + "priority" : [ "100" ] + } + }, + { + "id": "19c225cc-b499-48b1-aed6-3e1dd5bcf04c", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid" : [ "96afd00e-85cf-4d35-b18e-061d3813d8b2" ], + "secret" : [ "qBFGKdUGf6xDgKphnRfoFzIzaFHJW4bYnZ9MinPFzN38X5_ctq-2u1q5RdZzeJukXvk2biHB8_s3DxWmmLZFsA" ], + "priority": [ "100" ], + "algorithm": [ "HS256" ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "55f3ddc5-0f36-496d-817f-3aa8f426ee45", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fc632231-21d0-44d5-a730-f6e8e0e2cebc", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "2d0ccc2f-888c-495f-91ae-dfffba572d33", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "b7ff5812-2bc2-4f8f-9913-bd3b97a08618", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "ddbfb446-21d8-44c2-a207-7f83d760e94f", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "21dc8a77-3900-46e7-b1e4-40f5bcbd9b8e", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "329ed4e1-d3a8-42aa-a9ff-991a0e8f2851", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "66b4a633-6ba0-41e2-944f-0b13369c1e78", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fce169a3-c245-4dc8-a3c5-295bfa7057a4", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "4c5476fa-9aef-440b-bd14-25bf8cbfcd16", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "75d65771-3bfb-4def-a539-656de7d1af58", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "a6a9036b-192e-461f-91c7-d8117435188d", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f86bdf88-8bee-480b-8e81-67dcd674e46c", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "6f87019e-c995-4049-b8bf-d08a9c3a13f3", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fadc7c73-7fae-4c28-ad69-51bb03ba17bf", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f961cb3c-c681-4dc7-8151-786a5c50ce08", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d930f23e-ae58-45b2-9e01-20691200c926", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "8d62b1dd-6066-454d-bc76-f783d50fecaa", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "f99be349-ce0b-44a4-9f70-73f57cb8c164", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "33ee7503-bd12-4e5a-903c-5ae580f48709", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "6970ebc8-0b24-414c-8544-3cc48b1a0e4c", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "d14b76f4-b608-4b13-b51c-b9e162ad784b", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "_browser_header.xRobotsTag": "none", + "webAuthnPolicyRpEntityName": "keycloak", + "failureFactor": "30", + "actionTokenGeneratedByUserLifespan": "300", + "maxDeltaTimeSeconds": "43200", + "webAuthnPolicySignatureAlgorithms": "ES256", + "offlineSessionMaxLifespan": "5184000", + "_browser_header.contentSecurityPolicyReportOnly": "", + "bruteForceProtected": "false", + "_browser_header.contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "_browser_header.xXSSProtection": "1; mode=block", + "_browser_header.xFrameOptions": "SAMEORIGIN", + "_browser_header.strictTransportSecurity": "max-age=31536000; includeSubDomains", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "permanentLockout": "false", + "quickLoginCheckMilliSeconds": "1000", + "webAuthnPolicyCreateTimeout": "0", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "maxFailureWaitSeconds": "900", + "minimumQuickLoginWaitSeconds": "60", + "webAuthnPolicyAvoidSameAuthenticatorRegister": "false", + "_browser_header.xContentTypeOptions": "nosniff", + "actionTokenGeneratedByAdminLifespan": "43200", + "waitIncrementSeconds": "60", + "offlineSessionMaxLifespanEnabled": "false" + }, + "users" : [ { + "id" : "af134cab-f41c-4675-b141-205f975db679", + "username" : "admin", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "NICTtwsvSxJ5hL8hLAuleDUv9jwZcuXgxviMXvR++cciyPtiIEStEaJUyfA9DOir59awjPrHOumsclPVjNBplA==", + "salt" : "T/2P5o5oxFJUEk68BRURRg==", + "hashIterations" : 27500, + "counter" : 0, + "algorithm" : "pbkdf2-sha256", + "digits" : 0, + "period" : 0, + "createdDate" : 1554245879354, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "admin", "user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "eb4123a3-b722-4798-9af5-8957f823657a", + "username" : "alice", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "A3okqV2T/ybXTVEgKfosoSjP8Yc9IZbFP/SY4cEd6hag7TABQrQ6nUSuwagGt96l8cw1DTijO75PqX6uiTXMzw==", + "salt" : "sl4mXx6T9FypPH/s9TngfQ==", + "hashIterations" : 27500, + "counter" : 0, + "algorithm" : "pbkdf2-sha256", + "digits" : 0, + "period" : 0, + "createdDate" : 1554245879116, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "1eed6a8e-a853-4597-b4c6-c4c2533546a0", + "username" : "jdoe", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "credentials" : [ { + "type" : "password", + "hashedSaltedValue" : "JV3DUNLjqOadjbBOtC4rvacQI553CGaDGAzBS8MR5ReCr7SwF3E6CsW3T7/XO8ITZAsch8+A/6loeuCoVLLJrg==", + "salt" : "uCbOH7HZtyDtMd0E9DG/nw==", + "hashIterations" : 27500, + "counter" : 0, + "algorithm" : "pbkdf2-sha256", + "digits" : 0, + "period" : 0, + "createdDate" : 1554245879227, + "config" : { } + } ], + "disableableCredentialTypes" : [ "password" ], + "requiredActions" : [ ], + "realmRoles" : [ "confidential", "user" ], + "notBefore" : 0, + "groups" : [ ] + }, { + "id" : "948c59ec-46ed-4d99-aa43-02900029b930", + "createdTimestamp" : 1554245880023, + "username" : "service-account-backend-service", + "enabled" : true, + "totp" : false, + "emailVerified" : false, + "email" : "service-account-backend-service@placeholder.org", + "serviceAccountClientId" : "backend-service", + "credentials" : [ ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "offline_access" ], + "clientRoles" : { + "backend-service" : [ "uma_protection" ], + "account" : [ "view-profile", "manage-account" ] + }, + "notBefore" : 0, + "groups" : [ ] + } ], + "keycloakVersion": "8.0.1", + "userManagedAccessAllowed": false +} diff --git a/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationInGraalITCase.java b/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationInGraalITCase.java new file mode 100644 index 0000000000000..800a925d06e95 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationInGraalITCase.java @@ -0,0 +1,7 @@ +package io.quarkus.it.keycloak; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class OidcClientRegistrationInGraalITCase extends OidcClientRegistrationTest { +} diff --git a/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java b/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java new file mode 100644 index 0000000000000..f752f5a5105d9 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/test/java/io/quarkus/it/keycloak/OidcClientRegistrationTest.java @@ -0,0 +1,133 @@ +package io.quarkus.it.keycloak; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.htmlunit.SilentCssErrorHandler; +import org.htmlunit.TextPage; +import org.htmlunit.WebClient; +import org.htmlunit.html.HtmlForm; +import org.htmlunit.html.HtmlPage; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class OidcClientRegistrationTest { + + @Test + public void testDefaultRegisteredClientOnStartup() throws IOException { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getInputByName("login").click(); + + assertEquals("registered-client:Default Client Updated:alice", textPage.getContent()); + } + } + + @Test + public void testTenantRegisteredClientOnStartup() throws IOException { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected/tenant"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getInputByName("login").click(); + + assertEquals("registered-client-tenant:Tenant Client:alice", textPage.getContent()); + } + } + + @Test + public void testRegisteredClientDynamically() throws IOException { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected/dynamic"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getInputByName("login").click(); + + assertEquals("registered-client-dynamically:Dynamic Client:alice", textPage.getContent()); + } + } + + @Test + public void testRegisteredClientDynamicTenant() throws IOException { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected/dynamic-tenant"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getInputByName("login").click(); + + assertEquals("registered-client-dynamic-tenant:Dynamic Tenant Client:alice", textPage.getContent()); + } + } + + @Test + public void testRegisteredClientMulti1() throws IOException { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected/multi1"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getInputByName("login").click(); + + assertEquals("registered-client-multi1:Multi1 Client:alice", textPage.getContent()); + } + } + + @Test + public void testRegisteredClientMulti2() throws IOException { + try (final WebClient webClient = createWebClient()) { + HtmlPage page = webClient.getPage("http://localhost:8081/protected/multi2"); + + assertEquals("Sign in to quarkus", page.getTitleText()); + + HtmlForm loginForm = page.getForms().get(0); + + loginForm.getInputByName("username").setValueAttribute("alice"); + loginForm.getInputByName("password").setValueAttribute("alice"); + + TextPage textPage = loginForm.getInputByName("login").click(); + + assertEquals("registered-client-multi2:Multi2 Client:alice", textPage.getContent()); + } + } + + private WebClient createWebClient() { + WebClient webClient = new WebClient(); + webClient.setCssErrorHandler(new SilentCssErrorHandler()); + return webClient; + } + +} diff --git a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java index b050fea923e23..8646416ae714b 100644 --- a/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java +++ b/integration-tests/oidc-tenancy/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -12,8 +12,8 @@ import io.quarkus.oidc.OidcTenantConfig.ApplicationType; import io.quarkus.oidc.OidcTenantConfig.Roles.Source; import io.quarkus.oidc.TenantConfigResolver; -import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials; -import io.quarkus.oidc.common.runtime.OidcCommonConfig.Credentials.Secret.Method; +import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials; +import io.quarkus.oidc.common.runtime.OidcClientCommonConfig.Credentials.Secret.Method; import io.smallrye.jwt.algorithm.SignatureAlgorithm; import io.smallrye.mutiny.Uni; import io.vertx.ext.web.RoutingContext; diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index ebffa37fc850d..f77304827c2ca 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -253,6 +253,7 @@ observability-lgtm oidc oidc-client + oidc-client-registration oidc-client-reactive oidc-client-wiremock oidc-token-propagation