From 4a13c4557ec961f6ad5a3b470546c09dc694bcdd 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 | 279 +++ .../deployment/pom.xml | 88 + .../OidcClientRegistrationBuildStep.java | 67 + ...OidcClientRegistrationBuildTimeConfig.java | 16 + extensions/oidc-client-registration/pom.xml | 19 + .../oidc-client-registration/runtime/pom.xml | 56 + .../client/registration/ClientMetadata.java | 51 + .../registration/OidcClientRegistration.java | 18 + .../OidcClientRegistrationConfig.java | 70 + .../registration/OidcClientRegistrations.java | 17 + .../client/registration/RegisteredClient.java | 15 + ...sabledOidcClientRegistrationException.java | 20 + .../OidcClientConfigurationException.java | 20 + .../OidcClientRegistrationException.java | 20 + .../runtime/OidcClientRegistrationImpl.java | 197 ++ .../OidcClientRegistrationRecorder.java | 286 +++ .../OidcClientRegistrationsConfig.java | 28 + .../runtime/OidcClientRegistrationsImpl.java | 56 + .../runtime/RegisteredClientImpl.java | 216 ++ .../resources/META-INF/quarkus-extension.yaml | 16 + .../quarkus/oidc/client/OidcClientConfig.java | 4 +- .../io/quarkus/oidc/common/OidcEndpoint.java | 10 +- .../common/runtime/AbstractJsonObject.java} | 33 +- .../runtime/OidcClientCommonConfig.java | 85 + .../oidc/common/runtime/OidcCommonConfig.java | 85 +- .../oidc/common/runtime/OidcCommonUtils.java | 8 +- .../oidc/common/runtime/OidcConstants.java | 9 + .../common/runtime/OidcCommonUtilsTest.java | 2 +- .../keycloak/DevServicesConfig.java | 21 + .../KeycloakDevServicesProcessor.java | 30 +- .../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 +- .../io/quarkus/oidc/runtime/OidcRecorder.java | 3 +- extensions/pom.xml | 1 + .../oidc-client-registration/pom.xml | 159 ++ .../keycloak/CustomTenantConfigResolver.java | 175 ++ .../it/keycloak/ProtectedResource.java | 69 + .../src/main/resources/application.properties | 18 + .../src/main/resources/quarkus-realm.json | 2068 +++++++++++++++++ .../OidcClientRegistrationInGraalITCase.java | 7 + .../keycloak/OidcClientRegistrationTest.java | 133 ++ integration-tests/pom.xml | 1 + 50 files changed, 4450 insertions(+), 111 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/OidcClientConfigurationException.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 4428c604076c22..5d8465778904da 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -921,6 +921,16 @@ quarkus-rest-client-oidc-filter-deployment ${project.version} + + io.quarkus + quarkus-oidc-client-registration + ${project.version} + + + io.quarkus + quarkus-oidc-client-registration-deployment + ${project.version} + io.quarkus quarkus-oidc-client-graphql diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 58e502a49d66eb..497136eab4c929 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -73,6 +73,7 @@ public enum Feature { OBSERVABILITY, OIDC, OIDC_CLIENT, + OIDC_CLIENT_REGISTRATION, RESTEASY_CLIENT_OIDC_FILTER, REST_CLIENT_OIDC_FILTER, OIDC_CLIENT_GRAPHQL_CLIENT_INTEGRATION, diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 2eab94b25086ba..357c1d4c43db64 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -1721,6 +1721,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 2271533e112450..44d15558569121 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1733,6 +1733,19 @@ + + io.quarkus + quarkus-oidc-client-registration-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-oidc-common-deployment diff --git a/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc new file mode 100644 index 00000000000000..32f2eacaef22e3 --- /dev/null +++ b/docs/src/main/asciidoc/security-openid-connect-client-registration.adoc @@ -0,0 +1,279 @@ +//// +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 extension for OpenID Connect dynamic client registration and management. + +This includes the following: + + - Using `quarkus-oidc-client-registration` to 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. 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. + +== 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. + + +=== 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 these registered clients. + +For example: + +[source,java] +---- +package io.quarkus.it.keycloak; + +import java.net.URI; +import java.util.List; +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.ShutdownEvent; +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")) { + return Uni.createFrom().item(createTenantConfig("registered-client", clientReg.registeredClient())); + } else if (routingContext.request().path().endsWith("/protected/tenant")) { + return Uni.createFrom().item(createTenantConfig("registered-client-tenant", + clientRegs.getClientRegistration("tenant-client").registeredClient())); + } + return null; + } + + 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 + +You can register new clients on demand. +You can add new clients to the existing, already configured `OidcClientConfiguration` or to a newly created `OidcClientConfiguration`. + +Configure 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.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.OidcClientRegistrations; +import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig; +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 Map regClientsMulti; + + void onStartup(@Observes StartupEvent event) { + // Two custom OIDC client registrations registered right now + ClientMetadata clientMetadataMulti1 = createMetadata("http://localhost:8081/protected/multi1", "Multi1 Client"); + ClientMetadata clientMetadataMulti2 = createMetadata("http://localhost:8081/protected/multi2", "Multi2 Client"); + + Map regClientsMulti = clientReg.registerClients(List.of(clientMetadataMulti1, clientMetadataMulti2)) + .collect().asMap(r -> URI.create(r.metadata().getRedirectUris().get(0)).getPath(), r -> r) + .await().indefinitely(); + } + + + @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()); + } 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)); + + } 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 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()); + } +} + +---- + +[[configuration-reference]] +== Configuration reference + +include::{generated-dir}/config/quarkus-oidc-client-registration.adoc[opts=optional, leveloffset=+1] + +== References + +* https://openid.net/specs/openid-connect-registration-1_0.html[OIDC client registration] +* https://datatracker.ietf.org/doc/html/rfc7592[OAuth2 Dynamic Client Registration Management Protocol] +* https://www.keycloak.org/docs/latest/securing_apps/#_client_registration[Keycloak Dynamic Client Registration Service] +* xref:security-oidc-bearer-token-authentication.adoc[OIDC Bearer token authentication] +* xref:security-oidc-code-flow-authentication.adoc[OIDC code flow mechanism for protecting web applications] +* xref:security-overview.adoc[Quarkus Security overview] diff --git a/extensions/oidc-client-registration/deployment/pom.xml b/extensions/oidc-client-registration/deployment/pom.xml new file mode 100644 index 00000000000000..454794eb9b5292 --- /dev/null +++ b/extensions/oidc-client-registration/deployment/pom.xml @@ -0,0 +1,88 @@ + + + + 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 + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + maven-surefire-plugin + + true + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + ${keycloak.docker.legacy.image} + false + + + + + + + + diff --git a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java new file mode 100644 index 00000000000000..f0d71493b33d20 --- /dev/null +++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildStep.java @@ -0,0 +1,67 @@ +package io.quarkus.oidc.client.registration.deployment; + +import java.util.function.BooleanSupplier; + +import jakarta.inject.Singleton; + +import io.quarkus.arc.BeanDestroyer; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.deployment.Feature; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; +import io.quarkus.oidc.client.registration.OidcClientRegistration; +import io.quarkus.oidc.client.registration.OidcClientRegistrations; +import io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationRecorder; +import io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationsConfig; +import io.quarkus.tls.TlsRegistryBuildItem; +import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; + +@BuildSteps(onlyIf = OidcClientRegistrationBuildStep.IsEnabled.class) +public class OidcClientRegistrationBuildStep { + + @BuildStep + ExtensionSslNativeSupportBuildItem enableSslInNative() { + return new ExtensionSslNativeSupportBuildItem(Feature.OIDC_CLIENT_REGISTRATION); + } + + @Record(ExecutionTime.RUNTIME_INIT) + @BuildStep + public void setup( + OidcClientRegistrationsConfig oidcConfig, + OidcClientRegistrationRecorder recorder, + CoreVertxBuildItem vertxBuildItem, + TlsRegistryBuildItem tlsRegistry, + BuildProducer syntheticBean) { + + OidcClientRegistrations oidcClientRegistrations = recorder.setup(oidcConfig, vertxBuildItem.getVertx(), + tlsRegistry.registry()); + + syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClientRegistration.class).unremovable() + .types(OidcClientRegistration.class) + .supplier(recorder.createOidcClientRegistrationBean(oidcClientRegistrations)) + .scope(Singleton.class) + .setRuntimeInit() + .destroyer(BeanDestroyer.CloseableDestroyer.class) + .done()); + + syntheticBean.produce(SyntheticBeanBuildItem.configure(OidcClientRegistrations.class).unremovable() + .types(OidcClientRegistrations.class) + .supplier(recorder.createOidcClientRegistrationsBean(oidcClientRegistrations)) + .scope(Singleton.class) + .setRuntimeInit() + .destroyer(BeanDestroyer.CloseableDestroyer.class) + .done()); + } + + public static class IsEnabled implements BooleanSupplier { + OidcClientRegistrationBuildTimeConfig config; + + public boolean getAsBoolean() { + return config.enabled; + } + } +} diff --git a/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java new file mode 100644 index 00000000000000..98d78afa9acf14 --- /dev/null +++ b/extensions/oidc-client-registration/deployment/src/main/java/io/quarkus/oidc/client/registration/deployment/OidcClientRegistrationBuildTimeConfig.java @@ -0,0 +1,16 @@ +package io.quarkus.oidc.client.registration.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +/** + * Build time configuration for OIDC client registration. + */ +@ConfigRoot +public class OidcClientRegistrationBuildTimeConfig { + /** + * If the OIDC client registration extension is enabled. + */ + @ConfigItem(defaultValue = "true") + public boolean enabled; +} diff --git a/extensions/oidc-client-registration/pom.xml b/extensions/oidc-client-registration/pom.xml new file mode 100644 index 00000000000000..0ccf96d1d9c775 --- /dev/null +++ b/extensions/oidc-client-registration/pom.xml @@ -0,0 +1,19 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-oidc-client-registration-parent + Quarkus - OpenID Connect Dynamic Client Registration + pom + + deployment + runtime + + diff --git a/extensions/oidc-client-registration/runtime/pom.xml b/extensions/oidc-client-registration/runtime/pom.xml new file mode 100644 index 00000000000000..2be72468aea9e5 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/pom.xml @@ -0,0 +1,56 @@ + + + + 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 + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java new file mode 100644 index 00000000000000..072653518f6cc8 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java @@ -0,0 +1,51 @@ +package io.quarkus.oidc.client.registration; + +import java.util.List; + +import jakarta.json.JsonObject; + +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(); + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java new file mode 100644 index 00000000000000..c69ac4d12cb62d --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistration.java @@ -0,0 +1,18 @@ +package io.quarkus.oidc.client.registration; + +import java.io.Closeable; +import java.util.List; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +public interface OidcClientRegistration extends Closeable { + ClientMetadata clientMetadata(); + + RegisteredClient registeredClient(); + + Uni registerClient(ClientMetadata reg); + + Multi registerClients(List regs); + +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java new file mode 100644 index 00000000000000..1d518179cb32a8 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrationConfig.java @@ -0,0 +1,70 @@ +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 clientRegistrationEnabled = true; + + /** + * Initial access token + */ + @ConfigItem + public Optional initialToken = Optional.empty(); + + /** + * Client metadata + */ + @ConfigItem + public Metadata metadata = new Metadata(); + + /** + * Client metadata + */ + @ConfigGroup + public static class Metadata { + /** + * Client name + */ + @ConfigItem + public Optional clientName = Optional.empty(); + + /** + * Redirect URI + */ + @ConfigItem + public Optional redirectUri = Optional.empty(); + + /** + * Post Logout URI + */ + @ConfigItem + public Optional postLogoutUri = Optional.empty(); + + /** + * Additional metadata properties + */ + @ConfigItem + public Map extraProps = new HashMap<>(); + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java new file mode 100644 index 00000000000000..435a0d002c8054 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/OidcClientRegistrations.java @@ -0,0 +1,17 @@ +package io.quarkus.oidc.client.registration; + +import java.io.Closeable; +import java.util.Map; + +import io.smallrye.mutiny.Uni; + +public interface OidcClientRegistrations extends Closeable { + OidcClientRegistration getClientRegistration(); + + OidcClientRegistration getClientRegistration(String id); + + Map getClientRegistrations(); + + Uni newClientRegistration(OidcClientRegistrationConfig oidcConfig); + +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java new file mode 100644 index 00000000000000..59da8825bfe1cf --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/RegisteredClient.java @@ -0,0 +1,15 @@ +package io.quarkus.oidc.client.registration; + +import java.io.Closeable; + +import io.smallrye.mutiny.Uni; + +public interface RegisteredClient extends Closeable { + ClientMetadata metadata(); + + Uni read(); + + Uni update(ClientMetadata metadata); + + Uni delete(); +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java new file mode 100644 index 00000000000000..12c84eda3fedac --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/DisabledOidcClientRegistrationException.java @@ -0,0 +1,20 @@ +package io.quarkus.oidc.client.registration.runtime; + +@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/OidcClientConfigurationException.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientConfigurationException.java new file mode 100644 index 00000000000000..70a8b0fd98ff76 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientConfigurationException.java @@ -0,0 +1,20 @@ +package io.quarkus.oidc.client.registration.runtime; + +@SuppressWarnings("serial") +public class OidcClientConfigurationException extends RuntimeException { + public OidcClientConfigurationException() { + + } + + public OidcClientConfigurationException(String errorMessage) { + this(errorMessage, null); + } + + public OidcClientConfigurationException(Throwable cause) { + this(null, cause); + } + + public OidcClientConfigurationException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java new file mode 100644 index 00000000000000..b224f92ef0ed6d --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationException.java @@ -0,0 +1,20 @@ +package io.quarkus.oidc.client.registration.runtime; + +@SuppressWarnings("serial") +public class OidcClientRegistrationException extends RuntimeException { + public OidcClientRegistrationException() { + + } + + public OidcClientRegistrationException(String errorMessage) { + this(errorMessage, null); + } + + public OidcClientRegistrationException(Throwable cause) { + this(null, cause); + } + + public OidcClientRegistrationException(String errorMessage, Throwable cause) { + super(errorMessage, cause); + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java new file mode 100644 index 00000000000000..73358c0dc7f7f7 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationImpl.java @@ -0,0 +1,197 @@ +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 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.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 final WebClient client; + private final String registrationUri; + private final OidcClientRegistrationConfig oidcConfig; + private final ClientMetadata configuredMetadata; + private final Map> filters; + private final RegisteredClient registeredClient; + private volatile boolean closed; + + public OidcClientRegistrationImpl(WebClient client, String registrationUri, + OidcClientRegistrationConfig oidcConfig, RegisteredClient registeredClient, + ClientMetadata configuredMetadata, + Map> oidcRequestFilters) { + this.client = client; + this.registrationUri = registrationUri; + this.oidcConfig = oidcConfig; + this.configuredMetadata = configuredMetadata; + this.filters = oidcRequestFilters; + this.registeredClient = registeredClient; + } + + @Override + public RegisteredClient registeredClient() { + return registeredClient; + } + + @Override + public ClientMetadata clientMetadata() { + return configuredMetadata; + } + + @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.debugf("Client registeration has failed: status: %d, error message: %s", resp.statusCode(), + errorMessage); + throw new OidcClientRegistrationException(errorMessage); + } + } + + @Override + public void close() throws IOException { + if (!closed) { + try { + client.close(); + } catch (Exception ex) { + } + closed = true; + } + } + + private void checkClosed() { + if (closed) { + throw new IllegalStateException("Oidc Client Registration is closed"); + } + } + + static class ClientRegistrationHelper { + RegisteredClient client; + String registrationUri; + + ClientRegistrationHelper(RegisteredClient client, String registrationUri) { + this.client = client; + this.registrationUri = registrationUri; + } + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java new file mode 100644 index 00000000000000..0a834867665c01 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java @@ -0,0 +1,286 @@ +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 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.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.oidc.common.runtime.OidcConstants; +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.clientRegistrationEnabled) { + 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 tokenUrisUni = null; + if (OidcCommonUtils.isAbsoluteUrl(oidcConfig.registrationPath)) { + tokenUrisUni = Uni.createFrom().item( + new OidcConfigurationMetadata(oidcConfig.registrationPath.get())); + } else { + String authServerUriString = OidcCommonUtils.getAuthServerUrl(oidcConfig); + if (!oidcConfig.getDiscoveryEnabled().orElse(true)) { + tokenUrisUni = Uni.createFrom() + .item(new OidcConfigurationMetadata( + OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath))); + } else { + tokenUrisUni = discoverRegistrationUri(client, oidcRequestFilters, authServerUriString.toString(), vertx, + oidcConfig); + } + } + return tokenUrisUni.onItemOrFailure() + .transformToUni(new BiFunction>() { + + @Override + public Uni apply(OidcConfigurationMetadata metadata, Throwable t) { + if (t != null) { + throw toOidcClientRegException(getEndpointUrl(oidcConfig), t); + } + + if (metadata.tokenRegistrationUri == null) { + throw new ConfigurationException( + "OpenId Connect Provider registration endpoint URL is not configured and can not be discovered"); + } + + ClientMetadata clientMetadata = createMetadata(oidcConfig.metadata); + if (clientMetadata.getJsonObject().isEmpty()) { + LOG.debugf("%s client registartion is skipped because its metadata is not configured", + oidcConfig.id.orElse(DEFAULT_ID)); + return Uni.createFrom().item(new OidcClientRegistrationImpl(client, + metadata.tokenRegistrationUri, + oidcConfig, + null, + clientMetadata, + oidcRequestFilters)); + } else { + final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); + return OidcClientRegistrationImpl.registerClient(client, + metadata.tokenRegistrationUri, + 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), t.getMessage()); + registeredClient = null; + } else { + registeredClient = r; + } + return new OidcClientRegistrationImpl(client, + metadata.tokenRegistrationUri, + oidcConfig, + registeredClient, + clientMetadata, + 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 RegisteredClient registeredClient() { + throw new DisabledOidcClientRegistrationException(message); + } + + @Override + public ClientMetadata clientMetadata() { + 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 { + } + + } + + private static class OidcConfigurationMetadata { + private final String tokenRegistrationUri; + + OidcConfigurationMetadata(String tokenRegistrationUri) { + this.tokenRegistrationUri = tokenRegistrationUri; + } + } + + private 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/OidcClientRegistrationsConfig.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java new file mode 100644 index 00000000000000..5d2772fdc7fd70 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsConfig.java @@ -0,0 +1,28 @@ +package io.quarkus.oidc.client.registration.runtime; + +import java.util.Map; + +import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(name = "oidc-client-registration", phase = ConfigPhase.RUN_TIME) +public class OidcClientRegistrationsConfig { + + /** + * The default client registration. + */ + @ConfigItem(name = ConfigItem.PARENT) + public OidcClientRegistrationConfig defaultClientRegistration; + + /** + * Additional named client registrations. + */ + @ConfigDocSection + @ConfigDocMapKey("id") + @ConfigItem(name = ConfigItem.PARENT) + public Map namedClientRegistrations; +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java new file mode 100644 index 00000000000000..7f4fd68fedeff4 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationsImpl.java @@ -0,0 +1,56 @@ +package io.quarkus.oidc.client.registration.runtime; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import io.quarkus.oidc.client.registration.OidcClientRegistration; +import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig; +import io.quarkus.oidc.client.registration.OidcClientRegistrations; +import io.smallrye.mutiny.Uni; + +public class OidcClientRegistrationsImpl implements OidcClientRegistrations, Closeable { + private OidcClientRegistration defaultClientReg; + private Map staticOidcClientRegs; + Function> newOidcClientReg; + + public OidcClientRegistrationsImpl() { + } + + public OidcClientRegistrationsImpl(OidcClientRegistration defaultClientReg, + Map staticOidcClientRegs, + Function> newOidcClientReg) { + this.defaultClientReg = defaultClientReg; + this.staticOidcClientRegs = staticOidcClientRegs; + this.newOidcClientReg = newOidcClientReg; + } + + @Override + public OidcClientRegistration getClientRegistration() { + return defaultClientReg; + } + + @Override + public OidcClientRegistration getClientRegistration(String id) { + return staticOidcClientRegs.get(id); + } + + public Map getClientRegistrations() { + return Collections.unmodifiableMap(staticOidcClientRegs); + } + + @Override + public Uni newClientRegistration(OidcClientRegistrationConfig oidcConfig) { + return newOidcClientReg.apply(oidcConfig); + } + + @Override + public void close() throws IOException { + defaultClientReg.close(); + for (OidcClientRegistration clientReg : staticOidcClientRegs.values()) { + clientReg.close(); + } + } +} diff --git a/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java new file mode 100644 index 00000000000000..56d12e95f736e1 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/RegisteredClientImpl.java @@ -0,0 +1,216 @@ +package io.quarkus.oidc.client.registration.runtime; + +import java.io.IOException; +import java.net.ConnectException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonValue; + +import org.jboss.logging.Logger; + +import io.quarkus.oidc.client.registration.ClientMetadata; +import io.quarkus.oidc.client.registration.OidcClientRegistrationConfig; +import io.quarkus.oidc.client.registration.RegisteredClient; +import io.quarkus.oidc.common.OidcEndpoint; +import io.quarkus.oidc.common.OidcEndpoint.Type; +import io.quarkus.oidc.common.OidcRequestContextProperties; +import io.quarkus.oidc.common.OidcRequestFilter; +import io.quarkus.oidc.common.runtime.OidcCommonUtils; +import io.quarkus.oidc.common.runtime.OidcConstants; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.groups.UniOnItem; +import io.vertx.core.http.HttpHeaders; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpRequest; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.client.WebClient; + +public class RegisteredClientImpl implements RegisteredClient { + private static final Logger LOG = Logger.getLogger(RegisteredClientImpl.class); + + private static final String APPLICATION_JSON = "application/json"; + private static final String AUTHORIZATION_HEADER = String.valueOf(HttpHeaders.AUTHORIZATION); + //https://datatracker.ietf.org/doc/html/rfc7592.html#section-2.2 + private static final Set PRIVATE_PROPERTIES = Set.of(OidcConstants.CLIENT_METADATA_SECRET_EXPIRES_AT, + OidcConstants.CLIENT_METADATA_ID_ISSUED_AT); + + private final WebClient client; + private final OidcClientRegistrationConfig oidcConfig; + private final String registrationClientUri; + private final String registrationToken; + private final ClientMetadata registeredMetadata; + private final Map> filters; + private volatile boolean closed; + + public RegisteredClientImpl(WebClient client, OidcClientRegistrationConfig oidcConfig, + Map> oidcRequestFilters, + ClientMetadata registeredMetadata, String registrationClientUri, String registrationToken) { + this.client = client; + this.oidcConfig = oidcConfig; + this.registrationClientUri = registrationClientUri; + this.registrationToken = registrationToken; + this.registeredMetadata = registeredMetadata; + this.filters = oidcRequestFilters; + } + + @Override + public ClientMetadata metadata() { + checkClosed(); + return new ClientMetadata(registeredMetadata.getMetadataString()); + } + + @Override + public Uni read() { + checkClosed(); + checkClientRequestUri(); + HttpRequest request = client.getAbs(registrationClientUri); + request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON); + return makeRequest(request, Buffer.buffer()) + .transform(resp -> newRegisteredClient(resp)); + } + + @Override + public Uni update(ClientMetadata newMetadata) { + + checkClosed(); + checkClientRequestUri(); + if (newMetadata.getClientId() != null && !registeredMetadata.getClientId().equals(newMetadata.getClientId())) { + throw new OidcClientRegistrationException("Client id can not be modified"); + } + if (newMetadata.getClientSecret() != null + && !registeredMetadata.getClientSecret().equals(newMetadata.getClientSecret())) { + throw new OidcClientRegistrationException("Client secret can not be modified"); + } + + JsonObjectBuilder builder = Json.createObjectBuilder(); + + JsonObject newJsonObject = newMetadata.getJsonObject(); + JsonObject currentJsonObject = registeredMetadata.getJsonObject(); + + LOG.debugf("Current client metadata: %s", currentJsonObject.toString()); + + // Try to ensure the same order of properties as in the original metadata + for (Map.Entry entry : currentJsonObject.entrySet()) { + if (PRIVATE_PROPERTIES.contains(entry.getKey())) { + continue; + } + boolean newPropValue = newJsonObject.containsKey(entry.getKey()); + builder.add(entry.getKey(), newPropValue ? newJsonObject.get(entry.getKey()) : entry.getValue()); + } + for (Map.Entry entry : newJsonObject.entrySet()) { + if (PRIVATE_PROPERTIES.contains(entry.getKey())) { + continue; + } + if (!currentJsonObject.containsKey(entry.getKey())) { + builder.add(entry.getKey(), entry.getValue()); + } + } + JsonObject json = builder.build(); + + LOG.debugf("Updated client metadata: %s", json.toString()); + + HttpRequest request = client.putAbs(registrationClientUri); + request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), APPLICATION_JSON); + request.putHeader(HttpHeaders.ACCEPT.toString(), APPLICATION_JSON); + return makeRequest(request, Buffer.buffer(json.toString())) + .transform(resp -> newRegisteredClient(resp)); + } + + @Override + public Uni delete() { + checkClosed(); + checkClientRequestUri(); + + return makeRequest(client.deleteAbs(registrationClientUri), Buffer.buffer()) + .transformToUni(resp -> deleteResponse(resp)); + } + + @Override + public void close() throws IOException { + if (!closed) { + try { + client.close(); + } catch (Exception ex) { + } + closed = true; + } + } + + private UniOnItem> makeRequest(HttpRequest request, Buffer buffer) { + if (registrationToken != null) { + request.putHeader(AUTHORIZATION_HEADER, OidcConstants.BEARER_SCHEME + " " + registrationToken); + } + // Retry up to three times with a one-second delay between the retries if the connection is closed + Uni> response = filter(request, buffer).sendBuffer(buffer) + .onFailure(ConnectException.class) + .retry() + .atMost(oidcConfig.connectionRetryCount) + .onFailure().transform(t -> { + LOG.warn("OIDC Server is not available:", t.getCause() != null ? t.getCause() : t); + // don't wrap it to avoid information leak + return new OidcClientConfigurationException("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 OidcClientConfigurationException(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 OidcClientConfigurationException( + "Registered OIDC Client can not make requests to the client configuration endpoint"); + } + } + +} diff --git a/extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 00000000000000..b397f0b0970596 --- /dev/null +++ b/extensions/oidc-client-registration/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,16 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "OpenID Connect Dynamic Client Registration" +metadata: + keywords: + - "oauth2" + - "openid-connect" + - "oidc" + - "oidc-client" + - "oidc-client-registration" + guide: "https://quarkus.io/guides/security-openid-connect-client-registration" + categories: + - "security" + status: "experimental" + config: + - "quarkus.oidc-client-registration." diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java index 5b40ddb43d9c69..f6c180a979e647 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/OidcClientConfig.java @@ -5,14 +5,14 @@ import java.util.Map; import java.util.Optional; -import io.quarkus.oidc.common.runtime.OidcCommonConfig; +import io.quarkus.oidc.common.runtime.OidcClientCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.runtime.annotations.ConfigDocMapKey; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @ConfigGroup -public class OidcClientConfig extends OidcCommonConfig { +public class OidcClientConfig extends OidcClientCommonConfig { /** * A unique OIDC client identifier. It must be set when OIDC clients are created dynamically diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java index 362580ebf53237..18591c069964c1 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/OidcEndpoint.java @@ -42,7 +42,15 @@ enum Type { /** * Applies to OIDC UserInfo endpoint requests */ - USERINFO + USERINFO, + /** + * Applies to OIDC client registration requests + */ + CLIENT_REGISTRATION, + /** + * Applies to the configuration requests of the dynamically registered OIDC clients + */ + CLIENT_CONFIGURATION } /** diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java similarity index 68% rename from extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java rename to extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java index 416848f07fcf2a..9959b2292f6ade 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/AbstractJsonObjectResponse.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/AbstractJsonObject.java @@ -1,7 +1,9 @@ -package io.quarkus.oidc.runtime; +package io.quarkus.oidc.common.runtime; import java.io.StringReader; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; @@ -10,21 +12,23 @@ import jakarta.json.JsonNumber; import jakarta.json.JsonObject; import jakarta.json.JsonReader; +import jakarta.json.JsonString; import jakarta.json.JsonValue; -public class AbstractJsonObjectResponse { +public abstract class AbstractJsonObject { private String jsonString; private JsonObject json; - public AbstractJsonObjectResponse() { + protected AbstractJsonObject() { + json = Json.createObjectBuilder().build(); } - public AbstractJsonObjectResponse(String jsonString) { + protected AbstractJsonObject(String jsonString) { this(toJsonObject(jsonString)); this.jsonString = jsonString; } - public AbstractJsonObjectResponse(JsonObject json) { + protected AbstractJsonObject(JsonObject json) { this.json = json; } @@ -50,7 +54,7 @@ public JsonObject getObject(String name) { } public JsonObject getJsonObject() { - return json; + return Json.createObjectBuilder(json).build(); } public Object get(String name) { @@ -69,11 +73,24 @@ public Set> getAllProperties() { return Collections.unmodifiableSet(json.entrySet()); } - protected String getNonNullJsonString() { + protected String getJsonString() { return jsonString == null ? json.toString() : jsonString; } - static JsonObject toJsonObject(String json) { + protected List getListOfStrings(String prop) { + JsonArray array = getArray(prop); + if (array == null) { + return null; + } + List list = new ArrayList(); + for (JsonValue value : array) { + list.add(((JsonString) value).getString()); + } + + return list; + } + + public static JsonObject toJsonObject(String json) { try (JsonReader jsonReader = Json.createReader(new StringReader(json))) { return jsonReader.readObject(); } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java new file mode 100644 index 00000000000000..3742b739a6e9c3 --- /dev/null +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java @@ -0,0 +1,85 @@ +package io.quarkus.oidc.common.runtime; + +import java.util.Optional; + +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(); + + public Optional getTokenPath() { + return tokenPath; + } + + public void setTokenPath(String tokenPath) { + this.tokenPath = Optional.of(tokenPath); + } + + public Optional getRevokePath() { + return revokePath; + } + + public void setRevokePath(String revokePath) { + this.revokePath = Optional.of(revokePath); + } + + public Optional getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = Optional.of(clientId); + } + + public Optional getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = Optional.of(clientName); + } + + public Credentials getCredentials() { + return credentials; + } + + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } +} diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonConfig.java index d744d88358f847..ca88e5ca1ec5d2 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 @@ -15,8 +15,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 +31,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 +74,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. */ @@ -721,44 +692,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 getRegistrationPath() { + return registrationPath; } - 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; + public void setRegistrationPath(String registrationPath) { + this.registrationPath = Optional.of(registrationPath); } public Optional isDiscoveryEnabled() { @@ -792,4 +731,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 33c6ce44bbe73c..b90a4d38cead70 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 @@ -88,7 +88,7 @@ public static void verifyEndpointUrl(String endpointUrl) { } } - public static void verifyCommonConfiguration(OidcCommonConfig oidcConfig, boolean clientIdOptional, + public static void verifyCommonConfiguration(OidcClientCommonConfig oidcConfig, boolean clientIdOptional, boolean isServerConfig) { final String configPrefix = isServerConfig ? "quarkus.oidc." : "quarkus.oidc-client."; if (!clientIdOptional && !oidcConfig.getClientId().isPresent()) { @@ -386,7 +386,7 @@ public static Key clientJwtKey(Credentials creds) { } } - public static String signJwtWithKey(OidcCommonConfig oidcConfig, String tokenRequestUri, Key key) { + public static String signJwtWithKey(OidcClientCommonConfig oidcConfig, String tokenRequestUri, Key key) { // 'jti' and 'iat' claims are created by default, 'iat' - is set to the current time JwtSignatureBuilder builder = Jwt .claims(additionalClaims(oidcConfig.credentials.jwt.getClaims())) @@ -440,7 +440,7 @@ public static void verifyConfigurationId(String defaultId, String configKey, Opt } - public static String initClientSecretBasicAuth(OidcCommonConfig oidcConfig) { + public static String initClientSecretBasicAuth(OidcClientCommonConfig oidcConfig) { if (isClientSecretBasicAuthRequired(oidcConfig.credentials)) { return basicSchemeValue(oidcConfig.getClientId().get(), clientSecret(oidcConfig.credentials)); } @@ -453,7 +453,7 @@ public static String basicSchemeValue(String name, String secret) { } - public static Key initClientJwtKey(OidcCommonConfig oidcConfig) { + public static Key initClientJwtKey(OidcClientCommonConfig oidcConfig) { if (isClientJwtAuthRequired(oidcConfig.credentials)) { return clientJwtKey(oidcConfig.credentials); } diff --git a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java index 2fca611b957ca9..8dc67f4f41e115 100644 --- a/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java +++ b/extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java @@ -76,4 +76,13 @@ public final class OidcConstants { public static final String OPENID_SCOPE = "openid"; public static final String NONCE = "nonce"; + + public static final String REGISTRATION_CLIENT_URI = "registration_client_uri"; + public static final String REGISTRATION_ACCESS_TOKEN = "registration_access_token"; + + public static final String CLIENT_METADATA_CLIENT_NAME = "client_name"; + public static final String CLIENT_METADATA_REDIRECT_URIS = "redirect_uris"; + public static final String CLIENT_METADATA_POST_LOGOUT_URIS = "post_logout_redirect_uris"; + public static final String CLIENT_METADATA_SECRET_EXPIRES_AT = "client_secret_expires_at"; + public static final String CLIENT_METADATA_ID_ISSUED_AT = "client_id_issued_at"; } diff --git a/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java b/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java index ae280acf797f48..43cdb78f429a7f 100644 --- a/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java +++ b/extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java @@ -51,7 +51,7 @@ public void testProxyOptionsWithHostWithScheme() throws Exception { @Test public void testJwtTokenWithScope() throws Exception { - OidcCommonConfig cfg = new OidcCommonConfig(); + OidcClientCommonConfig cfg = new OidcClientCommonConfig(); cfg.setClientId("client"); cfg.credentials.jwt.claims.put("scope", "read,write"); PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate(); diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java index 9f71002fb2e7fd..e6166fda74f348 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 @@ -147,6 +147,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 5be9dbb80fb336..ff9b8688cd2403 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 @@ -271,8 +271,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); @@ -302,8 +302,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(","))); @@ -332,7 +334,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; } @@ -345,7 +347,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild return null; } - if (!dockerStatusBuildItem.isDockerAvailable()) { + if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) { LOG.warn("Please configure 'quarkus.oidc.auth-server-url' or get a working docker instance"); return null; } @@ -660,7 +662,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()))); } @@ -843,13 +847,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/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java index 17d7998233c19e..9281aa23e226e1 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcConfigurationMetadata.java @@ -15,6 +15,7 @@ public class OidcConfigurationMetadata { public static final String JWKS_ENDPOINT = "jwks_uri"; public static final String USERINFO_ENDPOINT = "userinfo_endpoint"; public static final String END_SESSION_ENDPOINT = "end_session_endpoint"; + private static final String REGISTRATION_ENDPOINT = "registration_endpoint"; public static final String SCOPES_SUPPORTED = "scopes_supported"; private final String discoveryUri; @@ -24,6 +25,7 @@ public class OidcConfigurationMetadata { private final String jsonWebKeySetUri; private final String userInfoUri; private final String endSessionUri; + private final String registrationUri; private final String issuer; private final JsonObject json; @@ -33,6 +35,7 @@ public OidcConfigurationMetadata(String tokenUri, String jsonWebKeySetUri, String userInfoUri, String endSessionUri, + String registrationUri, String issuer) { this.discoveryUri = null; this.tokenUri = tokenUri; @@ -41,6 +44,7 @@ public OidcConfigurationMetadata(String tokenUri, this.jsonWebKeySetUri = jsonWebKeySetUri; this.userInfoUri = userInfoUri; this.endSessionUri = endSessionUri; + this.registrationUri = registrationUri; this.issuer = issuer; this.json = null; } @@ -64,6 +68,8 @@ public OidcConfigurationMetadata(JsonObject wellKnownConfig, OidcConfigurationMe localMetadataConfig == null ? null : localMetadataConfig.userInfoUri); this.endSessionUri = getMetadataValue(wellKnownConfig, END_SESSION_ENDPOINT, localMetadataConfig == null ? null : localMetadataConfig.endSessionUri); + this.registrationUri = getMetadataValue(wellKnownConfig, REGISTRATION_ENDPOINT, + localMetadataConfig == null ? null : localMetadataConfig.registrationUri); this.issuer = getMetadataValue(wellKnownConfig, ISSUER, localMetadataConfig == null ? null : localMetadataConfig.issuer); this.json = wellKnownConfig; diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index 899c6f454fc4d8..27d7d06f38f805 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.OptionalInt; +import io.quarkus.oidc.common.runtime.OidcClientCommonConfig; import io.quarkus.oidc.common.runtime.OidcCommonConfig; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.oidc.runtime.OidcConfig; @@ -20,7 +21,7 @@ import io.quarkus.security.identity.SecurityIdentityAugmentor; @ConfigGroup -public class OidcTenantConfig extends OidcCommonConfig { +public class OidcTenantConfig extends OidcClientCommonConfig { /** * A unique tenant identifier. It can be set by {@code TenantConfigResolver} providers, which diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java index 109d675f47652a..3b7698ee79114f 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/TokenIntrospection.java @@ -5,14 +5,14 @@ import jakarta.json.JsonObject; +import io.quarkus.oidc.common.runtime.AbstractJsonObject; import io.quarkus.oidc.common.runtime.OidcConstants; -import io.quarkus.oidc.runtime.AbstractJsonObjectResponse; /** * Represents a token introspection result * */ -public class TokenIntrospection extends AbstractJsonObjectResponse { +public class TokenIntrospection extends AbstractJsonObject { public TokenIntrospection() { } @@ -64,6 +64,6 @@ public String getClientId() { } public String getIntrospectionString() { - return getNonNullJsonString(); + return getJsonString(); } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java index 766c86c3f31d21..befd307d2a4933 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/UserInfo.java @@ -4,9 +4,9 @@ import org.eclipse.microprofile.jwt.Claims; -import io.quarkus.oidc.runtime.AbstractJsonObjectResponse; +import io.quarkus.oidc.common.runtime.AbstractJsonObject; -public class UserInfo extends AbstractJsonObjectResponse { +public class UserInfo extends AbstractJsonObject { private static final String EMAIL = "email"; private static final String NAME = "name"; @@ -26,7 +26,7 @@ public UserInfo(JsonObject json) { } public String getUserInfoString() { - return getNonNullJsonString(); + return getJsonString(); } public String getName() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index 7ce98b95596064..8ef9891a04f093 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -37,6 +37,7 @@ import io.quarkus.oidc.Redirect; import io.quarkus.oidc.SecurityEvent; import io.quarkus.oidc.UserInfo; +import io.quarkus.oidc.common.runtime.AbstractJsonObject; import io.quarkus.oidc.common.runtime.OidcCommonUtils; import io.quarkus.oidc.common.runtime.OidcConstants; import io.quarkus.security.AuthenticationCompletionException; @@ -72,6 +73,7 @@ public class CodeAuthenticationMechanism extends AbstractOidcAuthenticationMecha static final String STATE_COOKIE_RESTORE_PATH = "restore-path"; static final Uni VOID_UNI = Uni.createFrom().voidItem(); static final String NO_OIDC_COOKIES_AVAILABLE = "no_oidc_cookies"; + static final String HTTP_SCHEME = "http"; private static final String INTERNAL_IDTOKEN_HEADER = "internal"; private static final Logger LOG = Logger.getLogger(CodeAuthenticationMechanism.class); @@ -964,7 +966,7 @@ private String generateInternalIdToken(TenantConfigContext context, UserInfo use Long accessTokenExpiresInSecs) { JwtClaimsBuilder builder = Jwt.claims(); if (currentIdToken != null) { - AbstractJsonObjectResponse currentIdTokenJson = new AbstractJsonObjectResponse( + AbstractJsonObject currentIdTokenJson = new AbstractJsonObject( OidcUtils.decodeJwtContentAsString(currentIdToken)) { }; for (String claim : currentIdTokenJson.getPropertyNames()) { @@ -1205,6 +1207,9 @@ static ServerCookie createCookie(RoutingContext context, OidcTenantConfig oidcCo } private String buildUri(RoutingContext context, boolean forceHttps, String path) { + if (path.startsWith(HTTP_SCHEME)) { + return path; + } String authority = URI.create(context.request().absoluteURI()).getAuthority(); return buildUri(context, forceHttps, authority, path); } @@ -1347,12 +1352,19 @@ private Uni getCodeFlowTokensUni(RoutingContext context String code, String codeVerifier) { // 'redirect_uri': it must match the 'redirect_uri' query parameter which was used during the code request. - String redirectPath = getRedirectPath(configContext.oidcConfig, context); - if (configContext.oidcConfig.authentication.redirectPath.isPresent() - && !configContext.oidcConfig.authentication.redirectPath.get().equals(context.request().path())) { - LOG.warnf("Token redirect path %s does not match the current request path", context.request().path()); - return Uni.createFrom().failure(new AuthenticationFailedException("Wrong redirect path")); + Optional configuredRedirectPath = configContext.oidcConfig.authentication.redirectPath; + if (configuredRedirectPath.isPresent()) { + String requestPath = configuredRedirectPath.get().startsWith(HTTP_SCHEME) + ? buildUri(context, configContext.oidcConfig.authentication.forceRedirectHttpsScheme.orElse(false), + context.request().path()) + : context.request().path(); + if (!configuredRedirectPath.get().equals(requestPath)) { + LOG.warnf("Token redirect path %s does not match the current request path", requestPath); + return Uni.createFrom().failure(new AuthenticationFailedException("Wrong redirect path")); + } } + + String redirectPath = getRedirectPath(configContext.oidcConfig, context); String redirectUriParam = buildUri(context, isForceHttps(configContext.oidcConfig), redirectPath); LOG.debugf("Token request redirect_uri parameter: %s", redirectUriParam); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index dde4e3d77d34d3..f4e27752c305a6 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.security.AuthenticationFailedException; @@ -275,7 +276,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/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index a07b003bc6a541..83398259ad3091 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -604,8 +604,9 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi String jwksUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.jwksPath); String userInfoUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.userInfoPath); String endSessionUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.endSessionPath); + String registrationUri = OidcCommonUtils.getOidcEndpointUrl(authServerUriString, oidcConfig.registrationPath); return new OidcConfigurationMetadata(tokenUri, - introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, + introspectionUri, authorizationUri, jwksUri, userInfoUri, endSessionUri, registrationUri, oidcConfig.token.issuer.orElse(null)); } diff --git a/extensions/pom.xml b/extensions/pom.xml index c54dc9e46463f5..8126f9cda4df99 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -142,6 +142,7 @@ oidc-client oidc-client-filter oidc-client-reactive-filter + oidc-client-registration oidc-client-graphql oidc-token-propagation oidc-token-propagation-reactive diff --git a/integration-tests/oidc-client-registration/pom.xml b/integration-tests/oidc-client-registration/pom.xml new file mode 100644 index 00000000000000..4bca0039beda59 --- /dev/null +++ b/integration-tests/oidc-client-registration/pom.xml @@ -0,0 +1,159 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-oidc-client-registration + Quarkus - Integration Tests - OIDC Client Registration + Module that tests dynamic OIDC Client Registration + + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-oidc + + + io.quarkus + quarkus-oidc-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-oidc-client-registration + + + io.quarkus + quarkus-oidc-client-registration-deployment + ${project.version} + pom + test + + + * + * + + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.htmlunit + htmlunit + test + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-keycloak + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + diff --git a/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java new file mode 100644 index 00000000000000..66136de4807de6 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/java/io/quarkus/it/keycloak/CustomTenantConfigResolver.java @@ -0,0 +1,175 @@ +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.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; + + @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(); + + //regClientOnStartup = regClientOnStartup.update(createMetadata("http://localhost:8081/protected/postlogout")).await() + // .indefinitely(); + // Confirm that access to the client-specific registration endpoint works. + // Sending an update with Keycloak works but ID tokens end up without a preferred_username impacting tests + //so we just re-read the configuration + defaultRegClientOnStartup = defaultRegClientOnStartup.read().await().indefinitely(); + + // Custom 'tenant-client' OIDC client registration, client is registered at startup + tenantRegClientOnStartup = clientRegs.getClientRegistration("tenant-client").registeredClient(); + + // 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 = createMetadata("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(); + } + + void onShutdown(@Observes ShutdownEvent event) { + + if (defaultRegClientOnStartup != null) { + defaultRegClientOnStartup.delete().await().indefinitely(); + } + if (tenantRegClientOnStartup != null) { + tenantRegClientOnStartup.delete().await().indefinitely(); + } + if (regClientDynamically != null) { + regClientDynamically.delete().await().indefinitely(); + } + if (regClientDynamicTenant != null) { + regClientDynamicTenant.delete().await().indefinitely(); + } + if (regClientsMulti != null) { + for (RegisteredClient client : regClientsMulti.values()) { + client.delete().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().transform(cfg -> registeredClientDynamically(cfg.registeredClient())); + } 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 createMetadata(String postLogoutUri) { + return new ClientMetadata(Json.createObjectBuilder() + .add(OidcConstants.CLIENT_METADATA_POST_LOGOUT_URIS, Json.createArrayBuilder().add(postLogoutUri)) + .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 00000000000000..23501f0d2ef234 --- /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 00000000000000..8b5c3bab4ba765 --- /dev/null +++ b/integration-tests/oidc-client-registration/src/main/resources/application.properties @@ -0,0 +1,18 @@ +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.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 00000000000000..363a508e6d6259 --- /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 00000000000000..800a925d06e95a --- /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 00000000000000..8727fe753fd8cb --- /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: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/pom.xml b/integration-tests/pom.xml index 487eb8dc9de252..1534c9275ea518 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