diff --git a/Dockerfile b/Dockerfile
index 52011e8..69b0e40 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,21 +5,31 @@
# ----------------------------------------------------------------------------
# Build stage
-FROM maven:3-jdk-11-slim AS build
+FROM maven:3-openjdk-18-slim AS build
COPY pom.xml ./
COPY keycloak-config ./keycloak-config
-COPY jboss-fhir-provider ./jboss-fhir-provider
COPY keycloak-extensions ./keycloak-extensions
RUN mvn -B clean package -DskipTests
# Package stage
-FROM quay.io/keycloak/keycloak:18.0.0-legacy
+FROM quay.io/keycloak/keycloak:18.0.2
# This can be overridden, but without this I've found the db vendor-detection in Keycloak to be brittle
-ENV DB_VENDOR=H2
+ENV KC_HEALTH_ENABLED=true
+
+# Install custom providers
+#RUN curl -sL https://github.com/aerogear/keycloak-metrics-spi/releases/download/2.5.3/keycloak-metrics-spi-2.5.3.jar -o /opt/keycloak/providers/keycloak-metrics-spi-2.5.3.jar
+
+COPY --from=build keycloak-extensions/target/keycloak-extensions-*.jar /opt/keycloak/providers/
+
+RUN /opt/keycloak/bin/kc.sh build --health-enabled=true
+
+#for debug, show the config
+RUN /opt/keycloak/bin/kc.sh show-config
+
+#NOTE - This will run the server in developer mode. Production deployments should change 'start-dev' to 'start'
+# and will require additional configuration. See: https://www.keycloak.org/server/configuration
+ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start-dev"]
-COPY --from=build keycloak-extensions/target/keycloak-extensions-*.jar /opt/jboss/keycloak/standalone/deployments/
-COPY --from=build jboss-fhir-provider/target/jboss-modules/ /opt/jboss/keycloak/modules/system/layers/base/
-RUN rm -rf /opt/jboss/keycloak/docs
diff --git a/README.md b/README.md
index 11d40bf..9953663 100644
--- a/README.md
+++ b/README.md
@@ -64,17 +64,21 @@ Published Docker images from this project:
* [alvearie/smart-keycloak](https://quay.io/repository/alvearie/smart-keycloak) extends the official Keycloak image with the `keycloak-extensions` and their dependencies
* [alvearie/keycloak-config](https://quay.io/repository/alvearie/keycloak-config) packages the `keycloak-config` module on top of `adoptopenjdk/openjdk11-openj9:ubi` (for configuring Keycloak realms)
+**Warning** :
+The `alvearie/smart-keycloak` image starts Keycloak in development mode. Non-test deployments will need to use their own Dockerfile which starts the Keycloak in production mode and will require further configuration. See the Keycloak guides for more information. https://www.keycloak.org/guides
+
+
By default, the `alvearie/smart-keycloak` image will behave identical to the Keycloak image from which it extends.
Here is an example for running the image with a keycloak username and password of admin/admin:
```
-docker run -p 8080:8080 -p 8443:8443 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin alvearie/smart-keycloak
+docker run -p 8080:8080 -p 8443:8443 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin alvearie/smart-keycloak
```
Once you have it running, execute the `alvearie/keycloak-config` image to create or update a Keycloak realm with SMART App Launch support.
By default, `alvearie/keycloak-config` will use the following environment variables to connect to Keycloak and configure the KEYCLOAK_REALM with SMART App Launch support for a FHIR server at FHIR_BASE_URL:
- * KEYCLOAK_BASE_URL=http://host.docker.internal:8080/auth
+ * KEYCLOAK_BASE_URL=http://host.docker.internal:8080
* KEYCLOAK_USER=admin
* KEYCLOAK_PASSWORD=admin
* KEYCLOAK_REALM=test
@@ -85,7 +89,7 @@ Additionally, the default keycloak-config image will create a single Keycloak us
It is possible to override these environment variables via the command line (using the `-e` flag), or even to pass an entirely different configuration file by specifying a docker run command. For example, to update a Keycloak server that is listening on port 8081 of the docker host with a custom configuration, you could run a command like the following:
```
-docker run -v /local/config/dir:/config -e KEYCLOAK_BASE_URL=http://host.docker.internal:8081/auth alvearie/keycloak-config -configFile config/keycloak-config.json
+docker run -v /local/config/dir:/config -e KEYCLOAK_BASE_URL=http://host.docker.internal:8081 alvearie/keycloak-config -configFile config/keycloak-config.json
```
See https://github.com/Alvearie/keycloak-extensions-for-fhir/tree/main/keycloak-config/src/main/resources/config for the example configurations that are shipped with this image.
diff --git a/jboss-fhir-provider/pom.xml b/jboss-fhir-provider/pom.xml
deleted file mode 100644
index 74af1f3..0000000
--- a/jboss-fhir-provider/pom.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
- 4.0.0
-
- org.alvearie
- keycloak-extensions-parent
- 0.5.0-SNAPSHOT
-
- jboss-fhir-provider
- jar
-
-
-
- com.ibm.fhir
- fhir-provider
-
-
-
-
-
-
- de.smartics.maven.plugin
- smartics-jboss-modules-maven-plugin
- 2.1.5
-
-
- build-module
-
- create-modules-archive
-
- package
-
-
-
- src/main/resources
-
-
- com.github.ben-manes.caffeine
-
-
- com.github.stephenc
-
-
-
-
-
-
-
diff --git a/jboss-fhir-provider/src/main/resources/modules.xml b/jboss-fhir-provider/src/main/resources/modules.xml
deleted file mode 100644
index 8cbeec5..0000000
--- a/jboss-fhir-provider/src/main/resources/modules.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
- com.ibm.fhir
-
-
- org.glassfish
- jakarta.json
-
-
-
-
-
-
-
-
-
-
-
-
- .+
-
-
- com.github.ben-manes..+
- com.github.stephenc..+
- com.google.errorprone..+
-
-
-
- true
-
-
-
-
-
\ No newline at end of file
diff --git a/keycloak-config/Dockerfile b/keycloak-config/Dockerfile
index 1475b64..a00cee2 100644
--- a/keycloak-config/Dockerfile
+++ b/keycloak-config/Dockerfile
@@ -9,7 +9,7 @@
# > docker build . -f keycloak-config/Dockerfile
# Build stage
-FROM maven:3-jdk-11-slim AS build
+FROM maven:3-openjdk-18-slim AS build
COPY pom.xml ./
COPY keycloak-config ./keycloak-config
@@ -27,7 +27,7 @@ COPY --from=build --chown=1001:0 keycloak-config/target/dependency/* /jars/
COPY --from=build --chown=1001:0 keycloak-config/src/main/resources/config/keycloak-config.json /config/
COPY --from=build --chown=1001:0 keycloak-config/run.sh /
-ENV KEYCLOAK_BASE_URL=http://host.docker.internal:8080/auth \
+ENV KEYCLOAK_BASE_URL=http://host.docker.internal:8080 \
KEYCLOAK_REALM=test \
KEYCLOAK_USER=admin \
KEYCLOAK_PASSWORD=admin \
diff --git a/keycloak-config/pom.xml b/keycloak-config/pom.xml
index d6b2b69..c83d054 100644
--- a/keycloak-config/pom.xml
+++ b/keycloak-config/pom.xml
@@ -32,8 +32,8 @@
commons-text
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter-api
diff --git a/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java b/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java
index 5207192..a5117c9 100644
--- a/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java
+++ b/keycloak-config/src/main/java/org/alvearie/keycloak/config/KeycloakConfigurator.java
@@ -2,7 +2,7 @@
(C) Copyright IBM Corp. 2021
SPDX-License-Identifier: Apache-2.0
-*/
+ */
package org.alvearie.keycloak.config;
import java.util.ArrayList;
@@ -49,1073 +49,1125 @@
import jakarta.json.JsonValue;
public class KeycloakConfigurator {
- private final Keycloak adminClient;
-
- public KeycloakConfigurator(Keycloak client) {
- this.adminClient = client;
- }
-
- /**
- * Initializes the realm.
- * @param realmName the realm name
- * @param realmPg the realm property group
- * @throws Exception an Exception
- */
- public void initializeRealm(String realmName, PropertyGroup realmPg) throws Exception {
- System.out.println("initializing realm: " + realmName);
- // Create realm if it does not exist
- RealmsResource realms = adminClient.realms();
- RealmRepresentation realm = getRealmByName(realms, realmName);
- if (realm == null) {
- realm = new RealmRepresentation();
- realm.setRealm(realmName);
- realms.create(realm);
- realm = getRealmByName(realms, realmName);
- if (realm == null) {
- throw new RuntimeException("Unable to create realm");
- }
- }
-
- // Initialize client scopes
- PropertyGroup clientScopesPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPES);
- if (clientScopesPg != null) {
- for (PropertyEntry clientScopePe: clientScopesPg.getProperties()) {
- String clientScopeName = clientScopePe.getName();
- PropertyGroup clientScopePg = clientScopesPg.getPropertyGroup(clientScopeName);
- initializeClientScope(realms.realm(realmName).clientScopes(), clientScopeName, clientScopePg);
- }
- }
-
- // Update "default" default assigned client scopes
- List defaultClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_DEFAULT_CLIENT_SCOPES);
- if (defaultClientScopeNames != null) {
- List defaultClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), defaultClientScopeNames);
- if (defaultClientScopeIds != null) {
- List existingDefaultClientScopes = realms.realm(realmName).getDefaultDefaultClientScopes();
- for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) {
- if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) {
- realms.realm(realmName).removeDefaultDefaultClientScope(existingDefaultClientScope.getId());
- }
- else {
- defaultClientScopeIds.remove(existingDefaultClientScope.getId());
- }
- }
- for (String defaultClientScopeId : defaultClientScopeIds) {
- realms.realm(realmName).addDefaultDefaultClientScope(defaultClientScopeId);
- }
- }
- }
-
- // Update "default" optional assigned client scopes
- List optionalClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES);
- if (optionalClientScopeNames != null) {
- List optionalClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), optionalClientScopeNames);
- if (optionalClientScopeIds != null) {
- List existingOptionalClientScopes = realms.realm(realmName).getDefaultOptionalClientScopes();
- for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) {
- if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) {
- realms.realm(realmName).removeDefaultOptionalClientScope(existingOptionalClientScope.getId());
- }
- else {
- optionalClientScopeIds.remove(existingOptionalClientScope.getId());
- }
- }
- for (String defaultClientScopeId : optionalClientScopeIds) {
- realms.realm(realmName).addDefaultOptionalClientScope(defaultClientScopeId);
- }
- }
- }
-
- // Initialize clients
- PropertyGroup clientsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENTS);
- if (clientsPg != null) {
- for (PropertyEntry clientPe: clientsPg.getProperties()) {
- String clientName = clientPe.getName();
- PropertyGroup clientPg = clientsPg.getPropertyGroup(clientName);
- initializeClient(realms.realm(realmName).clients(), realms.realm(realmName).clientScopes(), clientName, clientPg);
- }
- }
-
- // Initialize identity providers
- PropertyGroup identityProvidersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDERS);
- if (identityProvidersPg != null) {
- for (PropertyEntry identityProviderPe: identityProvidersPg.getProperties()) {
- String identityProviderAlias = identityProviderPe.getName();
- PropertyGroup identityProviderPg = identityProvidersPg.getPropertyGroup(identityProviderAlias);
- initializeIdentityProvider(realms.realm(realmName).identityProviders(), identityProviderAlias, identityProviderPg);
- }
- }
-
- // Initialize authentication flows
- PropertyGroup authenticationFlowsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_AUTHENTICATION_FLOWS);
- if (authenticationFlowsPg != null) {
- for (PropertyEntry authenticationFlowPe : authenticationFlowsPg.getProperties()) {
- String authenticationFlowAlias = authenticationFlowPe.getName();
- PropertyGroup authenticationFlowPg = authenticationFlowsPg.getPropertyGroup(authenticationFlowAlias);
- initializeAuthenticationFlow(realms.realm(realmName).flows(), authenticationFlowAlias, authenticationFlowPg);
- }
- }
-
- // Initialize groups
- PropertyGroup groupsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_GROUPS);
- if (groupsPg != null) {
- for (PropertyEntry groupPe: groupsPg.getProperties()) {
- String groupName = groupPe.getName();
- PropertyGroup groupPg = groupsPg.getPropertyGroup(groupName);
- initializeGroup(realms.realm(realmName).groups(), groupName, groupPg);
- }
- }
-
- // Update "default" groups
- List defaultGroups = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_GROUPS);
- if (defaultGroups != null) {
- List defaultGroupIds = getGroupIds(realms.realm(realmName).groups(), defaultGroups);
- if (defaultGroupIds != null) {
- List existingDefaultGroups = realms.realm(realmName).getDefaultGroups();
- for (GroupRepresentation existingDefaultGroup : existingDefaultGroups) {
- if (!defaultGroupIds.contains(existingDefaultGroup.getId())) {
- realms.realm(realmName).removeDefaultGroup(existingDefaultGroup.getId());
- }
- else {
- defaultGroupIds.remove(existingDefaultGroup.getId());
- }
- }
- for (String defaultGroupId : defaultGroupIds) {
- realms.realm(realmName).addDefaultGroup(defaultGroupId);
- }
- }
- }
-
- // Initialize users
- PropertyGroup usersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_USERS);
- if (usersPg != null) {
- for (PropertyEntry userPe: usersPg.getProperties()) {
- String userName = userPe.getName();
- PropertyGroup userPg = usersPg.getPropertyGroup(userName);
- initializeUser(realms.realm(realmName).users(), realms.realm(realmName).groups(), userName, userPg);
- }
- }
-
- // Initialize events config
- PropertyGroup eventsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_EVENTS_CONFIG);
- if (eventsPg != null) {
- initializeEventsConfig(realm, eventsPg);
- }
-
- // Update realm settings
- String browserFlow = realmPg.getStringProperty(KeycloakConfig.PROP_BROWSER_FLOW);
- if (browserFlow != null) {
- realm.setBrowserFlow(browserFlow);
- }
- realm.setEnabled(realmPg.getBooleanProperty(KeycloakConfig.PROP_REALM_ENABLED));
- realms.realm(realmName).update(realm);
- }
-
- /**
- * @param realm
- * @param eventsPg
- */
- void initializeEventsConfig(RealmRepresentation realm, PropertyGroup eventsPg) {
- System.out.println("initializing events config");
-
- // Login events
- Boolean eventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS);
- if (eventsEnabled != null) {
- realm.setEventsEnabled(eventsEnabled);
- }
-
- Integer eventsExpiration = eventsPg.getIntProperty(KeycloakConfig.PROP_EVENTS_CONFIG_EXPIRATION);
- if (eventsExpiration != null) {
- realm.setEventsExpiration(Long.valueOf(eventsExpiration));
- }
-
- List saveTypes = null;
- try {
- saveTypes = eventsPg.getStringListProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_TYPES);
- } catch (Exception e) {
- System.err.println("Error while reading event save types from the config file:");
- e.printStackTrace();
- }
- if (saveTypes != null) {
- realm.setEnabledEventTypes(saveTypes);
- }
-
- // Admin events
- Boolean adminEventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS);
- if (adminEventsEnabled != null) {
- realm.setAdminEventsEnabled(adminEventsEnabled);
- }
- }
-
- /**
- * Initializes the client scopes.
- * @param clientScopes the client scopes resource
- * @param clientScopeName the client scope name
- * @param clientScopePg the client scope property group
- * @throws Exception an Exception
- */
- void initializeClientScope(ClientScopesResource clientScopes, String clientScopeName, PropertyGroup clientScopePg) throws Exception {
- System.out.println("initializing client scope: " + clientScopeName);
- // Create client scope if it does not exist
- ClientScopeRepresentation clientScope = getClientScopeByName(clientScopes, clientScopeName);
- if (clientScope == null) {
- clientScope = new ClientScopeRepresentation();
- clientScope.setName(clientScopeName);
- clientScopes.create(clientScope);
- clientScope = getClientScopeByName(clientScopes, clientScopeName);
- if (clientScope == null) {
- throw new RuntimeException("Unable to create client scope");
- }
- }
-
- // Update client scope settings
- clientScope.setDescription(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_DESCRIPTION));
- clientScope.setProtocol(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_PROTOCOL));
- PropertyGroup attributesPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_ATTRIBUTES);
- if (attributesPg != null) {
- Map attributes = clientScope.getAttributes();
- if (attributes == null) {
- attributes = new HashMap<>();
- }
- for (PropertyEntry attributePe: attributesPg.getProperties()) {
- String attributeKey = attributePe.getName();
- attributes.put(attributeKey, attributePe.getValue() != null ? attributePe.getValue().toString() : null);
- }
- clientScope.setAttributes(attributes);
- }
- clientScopes.get(clientScope.getId()).update(clientScope);
-
- // Initialize protocol mappers
- PropertyGroup mappersPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPERS);
- if (mappersPg != null) {
- for (PropertyEntry mapperPe: mappersPg.getProperties()) {
- String mapperName = mapperPe.getName();
- PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName);
- initializeProtocolMapper(clientScopes.get(clientScope.getId()).getProtocolMappers(), mapperName, mapperPg);
- }
- }
- }
-
- /**
- * Initializes the protocol mappers of the client scope.
- * @param protocolMappers the protocol mappers
- * @param mapperName the protocol mapper name
- * @param mapperPg the protocol mapper property group
- * @throws Exception an Exception
- */
- void initializeProtocolMapper(ProtocolMappersResource protocolMappers, String mapperName, PropertyGroup mapperPg) throws Exception {
- System.out.println("initializing protocol mapper: " + mapperName);
- // Create protocol mapper if it does not exist
- ProtocolMapperRepresentation protocolMapper = getProtocolMapperByName(protocolMappers, mapperName);
- if (protocolMapper == null) {
- protocolMapper = new ProtocolMapperRepresentation();
- protocolMapper.setName(mapperName);
- protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL));
- protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER));
- Response response = protocolMappers.createMapper(protocolMapper);
- protocolMapper = getProtocolMapperByName(protocolMappers, mapperName);
- if (protocolMapper == null) {
- throw new RuntimeException("Unable to create protocol mapper: " + response.readEntity(String.class));
- }
- }
-
- // Update protocol mapper settings
- protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL));
- protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER));
- PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG);
- if (configPg != null) {
- Map config = protocolMapper.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- protocolMapper.setConfig(config);
- }
- protocolMappers.update(protocolMapper.getId(), protocolMapper);
- }
-
- /**
- * Initializes the client.
- * @param clients the clients resource
- * @param clientScopes the client scopes resource
- * @param clientId the client id
- * @param clientPg the client property group
- * @throws Exception an Exception
- */
- void initializeClient(ClientsResource clients, ClientScopesResource clientScopes, String clientId, PropertyGroup clientPg) throws Exception {
- System.out.println("initializing client: " + clientId);
- // Create client if it does not exist
- ClientRepresentation client = getClientByClientId(clients, clientId);
- if (client == null) {
- client = new ClientRepresentation();
- client.setClientId(clientId);
- clients.create(client);
- client = getClientByClientId(clients, clientId);
- if (client == null) {
- throw new RuntimeException("Unable to create client");
- }
- }
-
- // Update client settings
- client.setName(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_NAME));
- client.setDescription(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_DESCRIPTION));
- client.setConsentRequired(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_CONSENT_REQUIRED));
- client.setStandardFlowEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_STANDARD_FLOW_ENABLED, true));
- client.setServiceAccountsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED, false));
-
- PropertyGroup attributePg = clientPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_ATTRIBUTES);
- if (attributePg != null) {
- setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED);
- }
-
- Boolean publicClient = clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_PUBLIC_CLIENT, false);
- client.setPublicClient(publicClient);
-
- if (!publicClient) {
- String clientAuthType = clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_AUTHENTICATOR_TYPE);
- client.setClientAuthenticatorType(clientAuthType);
-
- if ("client-jwt".equals(clientAuthType) && attributePg != null) {
- boolean useJwksUrl = Boolean.parseBoolean(attributePg.getStringProperty(KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL, "false"));
- if (useJwksUrl) {
- setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL);
- setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_JWKS_URL);
- }
- }
- }
-
- client.setDirectAccessGrantsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_DIRECT_ACCESS_ENABLED));
- client.setBearerOnly(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_BEARER_ONLY));
- client.setRootUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ROOT_URL));
- client.setRedirectUris(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_REDIRECT_URIS));
- client.setAdminUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ADMIN_URL));
- client.setWebOrigins(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_WEB_ORIGINS));
- clients.get(client.getId()).update(client);
-
- ClientResource cr = clients.get(client.getId());
-
- // Remove default client scopes that no longer apply and collect the ones to add
- List defaultClientScopeIdsToAdd = new ArrayList<>();
- List defaultClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_DEFAULT_CLIENT_SCOPES);
- if (defaultClientScopeNameStrings != null) {
- List defaultClientScopeIds = getClientScopeIds(clientScopes, defaultClientScopeNameStrings);
- if (defaultClientScopeIds != null) {
- List existingDefaultClientScopes = cr.getDefaultClientScopes();
- for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) {
- if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) {
- cr.removeDefaultClientScope(existingDefaultClientScope.getId());
- }
- else {
- defaultClientScopeIds.remove(existingDefaultClientScope.getId());
- }
- }
- defaultClientScopeIdsToAdd.addAll(defaultClientScopeIds);
- }
- }
-
- // Remove optional client scopes that no longer apply and collect the ones to add
- List optionalClientScopeIdsToAdd = new ArrayList<>();
- List optionalClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_OPTIONAL_CLIENT_SCOPES);
- if (optionalClientScopeNameStrings != null) {
- List optionalClientScopeIds = getClientScopeIds(clientScopes, optionalClientScopeNameStrings);
- if (optionalClientScopeIds != null) {
- List existingOptionalClientScopes = cr.getOptionalClientScopes();
- for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) {
- if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) {
- cr.removeDefaultClientScope(existingOptionalClientScope.getId());
- }
- else {
- optionalClientScopeIds.remove(existingOptionalClientScope.getId());
- }
- }
- optionalClientScopeIdsToAdd.addAll(optionalClientScopeIds);
- }
- }
-
- // Note: if a scope already exists in either list on the server, the add call will be ignored
- for (String clientScopeId : defaultClientScopeIdsToAdd) {
- cr.addDefaultClientScope(clientScopeId);
- }
- for (String clientScopeId : optionalClientScopeIdsToAdd) {
- cr.addOptionalClientScope(clientScopeId);
- }
- }
-
- /**
- * Client attributes are set a little differently, so this method encapsulates the logic to get the attribute map
- * and set a given property from a PropertyGroup that contains that attributes value in a property by the same name.
- * @param attributesPg
- * @param client
- * @param propName
- * @throws Exception
- */
- private void setAttribute(PropertyGroup attributesPg, ClientRepresentation client, String propName) throws Exception {
- client.getAttributes().put(propName, attributesPg.getStringProperty(propName));
- }
-
- /**
- * Initializes the identity provider.
- * @param identityProviders the identity providers resource
- * @param identityProviderAlias the identity provider alias
- * @param identityProviderPg the identity provider property group
- * @throws Exception an Exception
- */
- void initializeIdentityProvider(IdentityProvidersResource identityProviders, String identityProviderAlias, PropertyGroup identityProviderPg) throws Exception {
- System.out.println("initializing identity provider: " + identityProviderAlias);
- // Create identity provider if it does not exist
- IdentityProviderRepresentation identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias);
- if (identityProvider == null) {
- identityProvider = new IdentityProviderRepresentation();
- identityProvider.setAlias(identityProviderAlias);
- identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID));
- PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG);
- if (configPg != null) {
- Map config = identityProvider.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET);
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- identityProvider.setConfig(config);
- }
- identityProviders.create(identityProvider);
- identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias);
- if (identityProvider == null) {
- throw new RuntimeException("Unable to create identity provider");
- }
- }
-
- // Update identity provider settings
- identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID));
- identityProvider.setDisplayName(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_DISPLAY_NAME));
- identityProvider.setEnabled(identityProviderPg.getBooleanProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_ENABLED));
- identityProvider.setFirstBrokerLoginFlowAlias(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS));
- PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG);
- if (configPg != null) {
- Map config = identityProvider.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET);
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- identityProvider.setConfig(config);
- }
- identityProviders.get(identityProvider.getAlias()).update(identityProvider);
-
- // Initialize identity provider mappers
- PropertyGroup mappersPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPERS);
- if (mappersPg != null) {
- for (PropertyEntry mapperPe: mappersPg.getProperties()) {
- String mapperName = mapperPe.getName();
- PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName);
- initializeIdentityProviderMapper(identityProviders.get(identityProvider.getAlias()), identityProviderAlias, mapperName, mapperPg);
- }
- }
- }
-
- /**
- * Initializes the mappers of the identity provider.
- * @param identityProvider the identity provider
- * @param identityProviderAlias the identity provider alias
- * @param mapperName the identity provider mapper name
- * @param mapperPg the identity provider mapper property group
- * @throws Exception an Exception
- */
- void initializeIdentityProviderMapper(IdentityProviderResource identityProvider, String identityProviderAlias, String mapperName, PropertyGroup mapperPg) throws Exception {
- System.out.println("initializing identity provider mapper: " + mapperName);
- // Create protocol mapper if it does not exist
- IdentityProviderMapperRepresentation identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName);
- if (identityProviderMapper == null) {
- identityProviderMapper = new IdentityProviderMapperRepresentation();
- identityProviderMapper.setName(mapperName);
- identityProviderMapper.setIdentityProviderAlias(identityProviderAlias);
- identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER));
- PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
- if (configPg != null) {
- Map config = identityProviderMapper.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- identityProviderMapper.setConfig(config);
- }
- identityProvider.addMapper(identityProviderMapper);
- identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName);
- if (identityProviderMapper == null) {
- throw new RuntimeException("Unable to create identity provider mapper");
- }
- }
-
- // Update identity provider mapper settings
- identityProviderMapper.setIdentityProviderAlias(identityProviderAlias);
- identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER));
- PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
- if (configPg != null) {
- Map config = identityProviderMapper.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- identityProviderMapper.setConfig(config);
- }
- identityProvider.update(identityProviderMapper.getId(), identityProviderMapper);
- }
-
- /**
- * Initializes the authentication flow.
- * @param authMgmt the authorization management
- * @param authenticationFlowAlias the authentication flow alias
- * @param authenticationFlowPg the authentication flow property group
- * @throws Exception an Exception
- */
- void initializeAuthenticationFlow(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, PropertyGroup authenticationFlowPg) throws Exception {
- System.out.println("initializing authentication flow: " + authenticationFlowAlias);
- // Get authentication flow
- AuthenticationFlowRepresentation authenticationFlow = getAuthenticationFlowByAlias(authMgmt, authenticationFlowAlias);
- if (authenticationFlow == null) {
- authenticationFlow = new AuthenticationFlowRepresentation();
- authenticationFlow.setAlias(authenticationFlowAlias);
- authenticationFlow.setTopLevel(true);
- authenticationFlow.setProviderId(authenticationFlowPg.getStringProperty("providerId"));
- authenticationFlow.setBuiltIn(authenticationFlowPg.getBooleanProperty("builtIn"));
-
- Response response = authMgmt.createFlow(authenticationFlow);
-
- if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) {
- String path = response.getLocation().getPath();
- String id = path.substring(path.lastIndexOf("/") + 1);
- System.out.println("Created flow with id '" + id + "'");
- authenticationFlow.setId(id);
- updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow);
- } else {
- System.err.println("Failed to create flow; status code '" + response.getStatus() + "'");
- System.err.println(response.readEntity(String.class));
- }
- }
-
- updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow);
-
- // Update identity provider redirector
- for (PropertyEntry authExecutionPe: authenticationFlowPg.getProperties()) {
- String authExecutionType = authExecutionPe.getName();
- if (KeycloakConfig.PROP_IDENTITY_REDIRECTOR.equals(authExecutionType)) {
- PropertyGroup identityProviderRedirectorPg = authenticationFlowPg.getPropertyGroup(authExecutionType);
- String identityProviderRedirectorAlias = identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS);
- initializeIdentityProviderRedirector(authMgmt, authenticationFlowAlias, identityProviderRedirectorAlias, identityProviderRedirectorPg);
- }
- }
- }
-
- private void updateFlowWithExecutions(AuthenticationManagementResource authMgmt, PropertyGroup authenticationFlowPg,
- AuthenticationFlowRepresentation authenticationFlow) throws Exception {
- PropertyGroup authenticationExecutionsPg = authenticationFlowPg.getPropertyGroup("authenticationExecutions");
- JsonObject jsonObject = authenticationFlowPg.getJsonValue("authenticationExecutions").asJsonObject();
- for (String entry : jsonObject.keySet()) {
- PropertyGroup entryProps = authenticationExecutionsPg.getPropertyGroup(entry);
-
- HashMap executionParams = new HashMap();
-
- String description = entryProps.getStringProperty("description");
- executionParams.put("description", description);
-
- Boolean isFlow = entryProps.getBooleanProperty("authenticatorFlow", false);
- if (isFlow) {
- executionParams.put("alias", entry);
- executionParams.put("type", "basic-flow");
-
- AuthenticationExecutionInfoRepresentation executionFlow = getOrCreateExecution(authMgmt,
- authenticationFlow.getAlias(), entry, isFlow, executionParams);
-
- // the above "alias" actually gets saved as the display name for some reason, but the alias is what we need to add subflow executions
- executionFlow.setAlias(entry);
- executionFlow.setRequirement(entryProps.getStringProperty("requirement"));
- authMgmt.updateExecutions(authenticationFlow.getAlias(), executionFlow);
-
- PropertyGroup childExecutions = entryProps.getPropertyGroup("authenticationExecutions");
- for (PropertyEntry childEntry : childExecutions.getProperties()) {
- // TODO: see if we can get the display name from the authenticator provider_id somehow, instead of requiring it in our config
- String displayName = childEntry.getName();
- PropertyGroup childEntryPg = childExecutions.getPropertyGroup(displayName);
- String authenticator = childEntryPg.getStringProperty("authenticator");
-
- Boolean childIsFlow = childEntryPg.getBooleanProperty("authenticatorFlow", false);
- if (childIsFlow) {
- throw new UnsupportedOperationException("Nest subflows are not yet supported");
- }
-
- HashMap childExecutionParams = new HashMap();
- childExecutionParams.put("provider", authenticator);
- AuthenticationExecutionInfoRepresentation childExecution = getOrCreateExecution(authMgmt, entry, displayName, childIsFlow, childExecutionParams);
-
- String configAlias = childEntryPg.getStringProperty("configAlias");
- JsonValue configJson = childEntryPg.getJsonValue("config");
- if (configJson != null) {
- Map config = buildConfigMap(configJson, configAlias);
-
- AuthenticatorConfigRepresentation authenticatorConfig = getOrCreateAuthenticatorConfig(authMgmt, childExecution, configAlias, config);
- authenticatorConfig.setConfig(config);
- authMgmt.updateAuthenticatorConfig(authenticatorConfig.getId(), authenticatorConfig);
-
- childExecution.setAuthenticationConfig(configAlias);
- }
-
- childExecution.setRequirement(childEntryPg.getStringProperty("requirement"));
- authMgmt.updateExecutions(authenticationFlow.getAlias(), childExecution);
- }
- } else {
- executionParams.put("authenticator", entry);
- getOrCreateExecution(authMgmt, authenticationFlow.getAlias(), entry, isFlow, executionParams);
-
- // TODO authenticatorConfig
- executionParams.put("priority", Integer.toString(entryProps.getIntProperty("priority")));
- }
-
- }
- }
-
- private AuthenticatorConfigRepresentation getOrCreateAuthenticatorConfig(AuthenticationManagementResource authMgmt,
- AuthenticationExecutionInfoRepresentation execution, String configAlias, Map config) {
-
- AuthenticatorConfigRepresentation authenticatorConfig = null;
-
- String configId = execution.getAuthenticationConfig();
- if (configId != null) {
- authenticatorConfig = authMgmt.getAuthenticatorConfig(configId);
- } else {
- authenticatorConfig = new AuthenticatorConfigRepresentation();
- authenticatorConfig.setAlias(configAlias);
- Response response = authMgmt.newExecutionConfig(execution.getId(), authenticatorConfig);
-
- if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) {
- String path = response.getLocation().getPath();
- String id = path.substring(path.lastIndexOf("/") + 1);
- System.out.println("Created authenticator config with id '" + id + "'");
- authenticatorConfig.setId(id);
- } else {
- System.err.println("Failed to create authenticator config; status code '" + response.getStatus() + "'");
- System.err.println(response.readEntity(String.class));
- }
- }
-
- return authenticatorConfig;
- }
-
- private Map buildConfigMap(JsonValue configJson, String configAlias) {
- Map config = new HashMap();
- Set> entrySet = configJson.asJsonObject().entrySet();
- for (Entry configEntry : entrySet) {
- JsonValue value = configEntry.getValue();
- if (value instanceof JsonString) {
- config.put(configEntry.getKey(), ((JsonString) value).getString());
- } else {
- System.err.println("Expected config of type String, but found " + value.getValueType());
- }
- }
- return config;
- }
-
- private AuthenticationExecutionInfoRepresentation getOrCreateExecution(AuthenticationManagementResource authMgmt,
- String flowAlias, String displayName, boolean isFlow, HashMap executionParams) {
- AuthenticationExecutionInfoRepresentation savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName);
- if (savedExecution == null) {
- if (isFlow) {
- authMgmt.addExecutionFlow(flowAlias, executionParams);
- } else {
- authMgmt.addExecution(flowAlias, executionParams);
- }
- savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName);
- }
- if (savedExecution == null) {
- throw new RuntimeException("Unable to create execution '" + displayName + "'");
- }
- return savedExecution;
- }
-
- /**
- * Initializes the identity provider redirector.
- * @param authMgmt the authorization management
- * @param authenticationFlowAlias the authentication flow alias
- * @param identityProviderRedirectorAlias the identity provider redirector alias
- * @param identityProviderRedirectorPg the identity provider redirector property group
- * @throws Exception an Exception
- */
- void initializeIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, String identityProviderRedirectorAlias, PropertyGroup identityProviderRedirectorPg) throws Exception {
- System.out.println("initializing identity provider redirector: " + identityProviderRedirectorAlias);
- // Get identity provider redirector
- AuthenticationExecutionInfoRepresentation identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias);
- if (identityProviderRedirector == null) {
- throw new RuntimeException("Identity provider redirector does not exist");
- }
-
- // Update identity provider redirector
- identityProviderRedirector.setRequirement(identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT));
- authMgmt.updateExecutions(authenticationFlowAlias, identityProviderRedirector);
- identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias);
- if (identityProviderRedirector == null) {
- throw new RuntimeException("Identity provider redirector does not exist");
- }
-
- // Create config representation if it does not exist
- AuthenticatorConfigRepresentation configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null;
- if (configRepresentation == null) {
- configRepresentation = new AuthenticatorConfigRepresentation();
- configRepresentation.setAlias(identityProviderRedirectorAlias);
- PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
- if (configPg != null) {
- Map config = configRepresentation.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- configRepresentation.setConfig(config);
- }
- authMgmt.newExecutionConfig(identityProviderRedirector.getId(), configRepresentation);
- identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias);
- if (identityProviderRedirector == null) {
- throw new RuntimeException("Identity provider redirector does not exist");
- }
- configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null;
- if (configRepresentation == null) {
- throw new RuntimeException("Unable to create identity provider redirector");
- }
- }
-
- // Update config representation
- configRepresentation.setAlias(identityProviderRedirectorAlias);
- PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
- if (configPg != null) {
- Map config = configRepresentation.getConfig();
- if (config == null) {
- config = new HashMap<>();
- }
- for (PropertyEntry configPe: configPg.getProperties()) {
- String configKey = configPe.getName();
- config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
- }
- configRepresentation.setConfig(config);
- }
- authMgmt.updateAuthenticatorConfig(configRepresentation.getId(), configRepresentation);
- }
-
- /**
- * Initializes the group.
- * @param groups the groups resource
- * @param groupName the group name
- * @param groupPg the group property group
- * @throws Exception an Exception
- */
- void initializeGroup(GroupsResource groups, String groupName, PropertyGroup groupPg) throws Exception {
- System.out.println("initializing group: " + groupName);
- // Create group if it does not exist
- GroupRepresentation group = getGroupByName(groups, groupName);
- if (group == null) {
- group = new GroupRepresentation();
- group.setName(groupName);
- groups.add(group);
- group = getGroupByName(groups, groupName);
- if (group == null) {
- throw new RuntimeException("Unable to create group");
- }
- }
-
- // Update group settings
- PropertyGroup attributesPg = groupPg.getPropertyGroup(KeycloakConfig.PROP_GROUP_ATTRIBUTES);
- if (attributesPg != null) {
- Map> attributes = group.getAttributes();
- if (attributes == null) {
- attributes = new HashMap<>();
- }
- for (PropertyEntry attributePe: attributesPg.getProperties()) {
- String attributeKey = attributePe.getName();
- List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue());
- attributes.put(attributeKey, attributeValue);
- }
- group.setAttributes(attributes);
- }
- groups.group(group.getId()).update(group);
- }
-
- /**
- * Initializes the user.
- * @param users the users resource
- * @param groups the groups resource
- * @param userName the user name
- * @param userPg the user property group
- * @throws Exception an Exception
- */
- void initializeUser(UsersResource users, GroupsResource groups, String userName, PropertyGroup userPg) throws Exception {
- System.out.println("initializing user: " + userName);
- // Create user if it does not exist
- UserRepresentation user = getUserByName(users, userName);
- if (user == null) {
- user = new UserRepresentation();
- user.setUsername(userName);
- users.create(user);
- user = getUserByName(users, userName);
- if (user == null) {
- throw new RuntimeException("Unable to create user");
- }
- }
-
- // Update user settings
- user.setEnabled(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_ENABLED));
- PropertyGroup attributesPg = userPg.getPropertyGroup(KeycloakConfig.PROP_USER_ATTRIBUTES);
- if (attributesPg != null) {
- Map> attributes = user.getAttributes();
- if (attributes == null) {
- attributes = new HashMap<>();
- }
- for (PropertyEntry attributePe: attributesPg.getProperties()) {
- String attributeKey = attributePe.getName();
- List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue());
- attributes.put(attributeKey, attributeValue);
- }
- user.setAttributes(attributes);
- }
- CredentialRepresentation credential = new CredentialRepresentation();
- credential.setType(KeycloakConfig.KEYCLOAK_USER_PASSWORD_TYPE);
- credential.setTemporary(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_PASSWORD_TEMPORARY));
- credential.setValue(userPg.getStringProperty(KeycloakConfig.PROP_USER_PASSWORD));
- user.setCredentials(Arrays.asList(credential));
- users.get(user.getId()).update(user);
-
- // Update user group memberships
- List groupIds = getGroupIds(groups, userPg.getStringListProperty(KeycloakConfig.PROP_USER_GROUPS));
- if (groupIds != null) {
- List existingGroupIds = getGroupIds(groups, user.getGroups());
- for (String existingGroupId : existingGroupIds) {
- if (!groupIds.contains(existingGroupId)) {
- users.get(user.getId()).leaveGroup(existingGroupId);
- }
- else {
- groupIds.remove(existingGroupId);
- }
- }
- for (String groupId : groupIds) {
- users.get(user.getId()).joinGroup(groupId);
- }
- }
- }
-
- /**
- * Gets the realm by name.
- * @param realmsResource the realms resource
- * @param realmName the realm name
- * @return the realm, or null if not found
- */
- private RealmRepresentation getRealmByName(RealmsResource realmsResource, String realmName) {
- for (RealmRepresentation realm : realmsResource.findAll()) {
- if (realmName.equals(realm.getRealm())) {
- return realm;
- }
- }
- return null;
- }
-
- /**
- * Gets the client scope by name.
- * @param clientScopes the client scopes
- * @param clientScopeName the client scope name
- * @return the client scope, or null if not found
- */
- private ClientScopeRepresentation getClientScopeByName(ClientScopesResource clientScopes, String clientScopeName) {
- for (ClientScopeRepresentation clientScope : clientScopes.findAll()) {
- if (clientScopeName.equals(clientScope.getName())) {
- return clientScope;
- }
- }
- return null;
- }
-
- /**
- * Gets the client scope IDs by name.
- * @param clientScopes the client scopes
- * @param clientScopeNames the client scope names
- * @return the client scope IDs
- */
- private List getClientScopeIds(ClientScopesResource clientScopes, List clientScopeNames) {
- List clientScopeIds = new ArrayList<>();
- Map nameToIdMap = clientScopes.findAll().stream().collect(Collectors.toMap(c -> c.getName(), c -> c.getId()));
-
- for (String clientScopeName : clientScopeNames) {
- if (nameToIdMap.containsKey(clientScopeName)) {
- clientScopeIds.add(nameToIdMap.get(clientScopeName));
- } else {
- System.err.println("Skipping client scope '" + clientScopeName + "'; unable to find id for client scope with this name");
- }
- }
- return clientScopeIds;
- }
-
- /**
- * Gets the client by client ID.
- * @param adminClient the clients
- * @param clientName the client name
- * @return the client, or null if not found
- */
- private ClientRepresentation getClientByClientId(ClientsResource clients, String clientId) {
- for (ClientRepresentation client : clients.findAll()) {
- if (clientId.equals(client.getClientId())) {
- return client;
- }
- }
- return null;
- }
-
- /**
- * Gets the protocol mapper by name.
- * @param protocolMappers the protocol mappers
- * @param mapperName the mapper name
- * @return the protocol mapper, or null if not found
- */
- private ProtocolMapperRepresentation getProtocolMapperByName(ProtocolMappersResource protocolMappers, String mapperName) {
- for (ProtocolMapperRepresentation protocolMapper : protocolMappers.getMappers()) {
- if (mapperName.equals(protocolMapper.getName())) {
- return protocolMapper;
- }
- }
- return null;
- }
-
- /**
- * Gets the identity provider by provider alias.
- * @param identityProviders the identity providers
- * @param identityProviderAlias the identity provider alias
- * @return the identity provider, or null if not found
- */
- private IdentityProviderRepresentation getIdentityProviderByAlias(IdentityProvidersResource identityProviders, String identityProviderAlias) {
- for (IdentityProviderRepresentation identityProvider : identityProviders.findAll()) {
- if (identityProviderAlias.equals(identityProvider.getAlias())) {
- return identityProvider;
- }
- }
- return null;
- }
-
- /**
- * Gets the identity provider mapper by name.
- * @param identity provider the identity provider
- * @param mapperName the mapper name
- * @return the identity provider mapper, or null if not found
- */
- private IdentityProviderMapperRepresentation getIdentityProvideMapperByName(IdentityProviderResource identityProvider, String mapperName) {
- for (IdentityProviderMapperRepresentation identityProviderMapper : identityProvider.getMappers()) {
- if (mapperName.equals(identityProviderMapper.getName())) {
- return identityProviderMapper;
- }
- }
- return null;
- }
-
-
- /**
- * Gets the authentication flow by alias.
- * @param authMgmt the authorization management
- * @param authenticationFlowAlias the authentication flow alias
- * @return the authorization flow, or null if not found
- */
- private AuthenticationFlowRepresentation getAuthenticationFlowByAlias(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) {
- for (AuthenticationFlowRepresentation flow : authMgmt.getFlows()) {
- if (authenticationFlowAlias.equals(flow.getAlias())) {
- return flow;
- }
- }
- return null;
- }
-
- /**
- * Gets the authentication execution by alias.
- * @param authMgmt the authorization management
- * @param authenticationFlowAlias the authentication flow alias
- * @return the execution info, or null if not found
- */
- private AuthenticationExecutionInfoRepresentation getExecutionByDisplayName(AuthenticationManagementResource authMgmt, String authenticationFlowAlias,
- String executionDisplayName) {
- for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) {
- if (executionDisplayName.equals(execution.getDisplayName())) {
- return execution;
- }
- }
- return null;
- }
-
- /**
- * Gets the identity provider redirector by alias.
- * @param authMgmt the authorization management
- * @param authenticationFlowAlias the authentication flow alias
- * @return the authorization flow, or null if not found
- */
- private AuthenticationExecutionInfoRepresentation getIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) {
- for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) {
- if (KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR.equals(execution.getProviderId())) {
- return execution;
- }
- }
- return null;
- }
-
-
- /**
- * Gets the group by name.
- * @param groups the groups
- * @param groupName the group name
- * @return the group, or null if not found
- */
- private GroupRepresentation getGroupByName(GroupsResource groups, String groupName) {
- for (GroupRepresentation group : groups.groups()) {
- if (groupName.equals(group.getName())) {
- return group;
- }
- }
- return null;
- }
-
- /**
- * Gets the group IDs by name.
- * @param groups the groups
- * @param groupNames the group names
- * @return the group IDs
- */
- private List getGroupIds(GroupsResource groups, List groupNames) {
- List groupIds = new ArrayList<>();
- for (GroupRepresentation group : groups.groups()) {
- if (groupNames != null && groupNames.contains(group.getName())) {
- groupIds.add(group.getId());
- }
- }
- return groupIds;
- }
-
- /**
- * Gets the user by name.
- * @param users the users
- * @param userName the user name
- * @return the user, or null if not found
- */
- private UserRepresentation getUserByName(UsersResource users, String userName) {
- for (UserRepresentation user : users.list()) {
- if (userName.equals(user.getUsername())) {
- return user;
- }
- }
- return null;
- }
+ private final Keycloak adminClient;
+
+ public KeycloakConfigurator(Keycloak client) {
+ this.adminClient = client;
+ }
+
+ /**
+ * Initializes the realm.
+ * @param realmName the realm name
+ * @param realmPg the realm property group
+ * @throws Exception an Exception
+ */
+ public void initializeRealm(String realmName, PropertyGroup realmPg) throws Exception {
+ System.out.println("initializing realm: " + realmName);
+ // Create realm if it does not exist
+ RealmsResource realms = adminClient.realms();
+ RealmRepresentation realm = getRealmByName(realms, realmName);
+ if (realm == null) {
+ realm = new RealmRepresentation();
+ realm.setRealm(realmName);
+ realms.create(realm);
+ realm = getRealmByName(realms, realmName);
+ if (realm == null) {
+ throw new RuntimeException("Unable to create realm");
+ }
+ }
+
+ // Initialize client scopes
+ PropertyGroup clientScopesPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPES);
+ if (clientScopesPg != null) {
+ for (PropertyEntry clientScopePe: clientScopesPg.getProperties()) {
+ String clientScopeName = clientScopePe.getName();
+ PropertyGroup clientScopePg = clientScopesPg.getPropertyGroup(clientScopeName);
+ initializeClientScope(realms.realm(realmName).clientScopes(), clientScopeName, clientScopePg);
+ }
+ }
+
+ // Update "default" default assigned client scopes
+ List defaultClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_DEFAULT_CLIENT_SCOPES);
+ if (defaultClientScopeNames != null) {
+ List defaultClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), defaultClientScopeNames);
+ if (defaultClientScopeIds != null) {
+ List existingDefaultClientScopes = realms.realm(realmName).getDefaultDefaultClientScopes();
+ for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) {
+ if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) {
+ realms.realm(realmName).removeDefaultDefaultClientScope(existingDefaultClientScope.getId());
+ }
+ else {
+ defaultClientScopeIds.remove(existingDefaultClientScope.getId());
+ }
+ }
+ for (String defaultClientScopeId : defaultClientScopeIds) {
+ realms.realm(realmName).addDefaultDefaultClientScope(defaultClientScopeId);
+ }
+ }
+ }
+
+ // Update "default" optional assigned client scopes
+ List optionalClientScopeNames = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES);
+ if (optionalClientScopeNames != null) {
+ List optionalClientScopeIds = getClientScopeIds(realms.realm(realmName).clientScopes(), optionalClientScopeNames);
+ if (optionalClientScopeIds != null) {
+ List existingOptionalClientScopes = realms.realm(realmName).getDefaultOptionalClientScopes();
+ for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) {
+ if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) {
+ realms.realm(realmName).removeDefaultOptionalClientScope(existingOptionalClientScope.getId());
+ }
+ else {
+ optionalClientScopeIds.remove(existingOptionalClientScope.getId());
+ }
+ }
+ for (String defaultClientScopeId : optionalClientScopeIds) {
+ realms.realm(realmName).addDefaultOptionalClientScope(defaultClientScopeId);
+ }
+ }
+ }
+
+ // Initialize clients
+ PropertyGroup clientsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_CLIENTS);
+ if (clientsPg != null) {
+ for (PropertyEntry clientPe: clientsPg.getProperties()) {
+ String clientName = clientPe.getName();
+ PropertyGroup clientPg = clientsPg.getPropertyGroup(clientName);
+ initializeClient(realms.realm(realmName).clients(), realms.realm(realmName).clientScopes(), clientName, clientPg);
+ }
+ }
+
+ // Initialize authentication flows
+ PropertyGroup authenticationFlowsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_AUTHENTICATION_FLOWS);
+ if (authenticationFlowsPg != null) {
+ for (PropertyEntry authenticationFlowPe : authenticationFlowsPg.getProperties()) {
+ String authenticationFlowAlias = authenticationFlowPe.getName();
+ PropertyGroup authenticationFlowPg = authenticationFlowsPg.getPropertyGroup(authenticationFlowAlias);
+ initializeAuthenticationFlow(realms.realm(realmName).flows(), authenticationFlowAlias,
+ authenticationFlowPg);
+ }
+ }
+
+ // Initialize identity providers
+ PropertyGroup identityProvidersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDERS);
+ if (identityProvidersPg != null) {
+ for (PropertyEntry identityProviderPe: identityProvidersPg.getProperties()) {
+ String identityProviderAlias = identityProviderPe.getName();
+ PropertyGroup identityProviderPg = identityProvidersPg.getPropertyGroup(identityProviderAlias);
+ initializeIdentityProvider(realms.realm(realmName).identityProviders(), identityProviderAlias, identityProviderPg);
+ }
+ }
+
+ // Initialize groups
+ PropertyGroup groupsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_GROUPS);
+ if (groupsPg != null) {
+ for (PropertyEntry groupPe: groupsPg.getProperties()) {
+ String groupName = groupPe.getName();
+ PropertyGroup groupPg = groupsPg.getPropertyGroup(groupName);
+ initializeGroup(realms.realm(realmName).groups(), groupName, groupPg);
+ }
+ }
+
+ // Update "default" groups
+ List defaultGroups = realmPg.getStringListProperty(KeycloakConfig.PROP_DEFAULT_GROUPS);
+ if (defaultGroups != null) {
+ List defaultGroupIds = getGroupIds(realms.realm(realmName).groups(), defaultGroups);
+ if (defaultGroupIds != null) {
+ List existingDefaultGroups = realms.realm(realmName).getDefaultGroups();
+ for (GroupRepresentation existingDefaultGroup : existingDefaultGroups) {
+ if (!defaultGroupIds.contains(existingDefaultGroup.getId())) {
+ realms.realm(realmName).removeDefaultGroup(existingDefaultGroup.getId());
+ }
+ else {
+ defaultGroupIds.remove(existingDefaultGroup.getId());
+ }
+ }
+ for (String defaultGroupId : defaultGroupIds) {
+ realms.realm(realmName).addDefaultGroup(defaultGroupId);
+ }
+ }
+ }
+
+ // Initialize users
+ PropertyGroup usersPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_USERS);
+ if (usersPg != null) {
+ for (PropertyEntry userPe: usersPg.getProperties()) {
+ String userName = userPe.getName();
+ PropertyGroup userPg = usersPg.getPropertyGroup(userName);
+ initializeUser(realms.realm(realmName).users(), realms.realm(realmName).groups(), userName, userPg);
+ }
+ }
+
+ // Initialize events config
+ PropertyGroup eventsPg = realmPg.getPropertyGroup(KeycloakConfig.PROP_EVENTS_CONFIG);
+ if (eventsPg != null) {
+ initializeEventsConfig(realm, eventsPg);
+ }
+
+ // Update realm settings
+ String browserFlow = realmPg.getStringProperty(KeycloakConfig.PROP_BROWSER_FLOW);
+ if (browserFlow != null) {
+ realm.setBrowserFlow(browserFlow);
+ }
+ realm.setEnabled(realmPg.getBooleanProperty(KeycloakConfig.PROP_REALM_ENABLED));
+ realms.realm(realmName).update(realm);
+ }
+
+ /**
+ * @param realm
+ * @param eventsPg
+ */
+ void initializeEventsConfig(RealmRepresentation realm, PropertyGroup eventsPg) {
+ System.out.println("initializing events config");
+
+ // Login events
+ Boolean eventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS);
+ if (eventsEnabled != null) {
+ realm.setEventsEnabled(eventsEnabled);
+ }
+
+ Integer eventsExpiration = eventsPg.getIntProperty(KeycloakConfig.PROP_EVENTS_CONFIG_EXPIRATION);
+ if (eventsExpiration != null) {
+ realm.setEventsExpiration(Long.valueOf(eventsExpiration));
+ }
+
+ List saveTypes = null;
+ try {
+ saveTypes = eventsPg.getStringListProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_TYPES);
+ } catch (Exception e) {
+ System.err.println("Error while reading event save types from the config file:");
+ e.printStackTrace();
+ }
+ if (saveTypes != null) {
+ realm.setEnabledEventTypes(saveTypes);
+ }
+
+ // Admin events
+ Boolean adminEventsEnabled = eventsPg.getBooleanProperty(KeycloakConfig.PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS);
+ if (adminEventsEnabled != null) {
+ realm.setAdminEventsEnabled(adminEventsEnabled);
+ }
+ }
+
+ /**
+ * Initializes the client scopes.
+ * @param clientScopes the client scopes resource
+ * @param clientScopeName the client scope name
+ * @param clientScopePg the client scope property group
+ * @throws Exception an Exception
+ */
+ void initializeClientScope(ClientScopesResource clientScopes, String clientScopeName, PropertyGroup clientScopePg) throws Exception {
+ System.out.println("initializing client scope: " + clientScopeName);
+ // Create client scope if it does not exist
+ ClientScopeRepresentation clientScope = getClientScopeByName(clientScopes, clientScopeName);
+ if (clientScope == null) {
+ clientScope = new ClientScopeRepresentation();
+ clientScope.setName(clientScopeName);
+ clientScopes.create(clientScope);
+ clientScope = getClientScopeByName(clientScopes, clientScopeName);
+ if (clientScope == null) {
+ throw new RuntimeException("Unable to create client scope");
+ }
+ }
+
+ // Update client scope settings
+ clientScope.setDescription(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_DESCRIPTION));
+ clientScope.setProtocol(clientScopePg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_PROTOCOL));
+ PropertyGroup attributesPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_ATTRIBUTES);
+ if (attributesPg != null) {
+ Map attributes = clientScope.getAttributes();
+ if (attributes == null) {
+ attributes = new HashMap<>();
+ }
+ for (PropertyEntry attributePe: attributesPg.getProperties()) {
+ String attributeKey = attributePe.getName();
+ attributes.put(attributeKey, attributePe.getValue() != null ? attributePe.getValue().toString() : null);
+ }
+ clientScope.setAttributes(attributes);
+ }
+ clientScopes.get(clientScope.getId()).update(clientScope);
+
+ // Initialize protocol mappers
+ PropertyGroup mappersPg = clientScopePg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPERS);
+ if (mappersPg != null) {
+ for (PropertyEntry mapperPe: mappersPg.getProperties()) {
+ String mapperName = mapperPe.getName();
+ PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName);
+ initializeProtocolMapper(clientScopes.get(clientScope.getId()).getProtocolMappers(), mapperName, mapperPg);
+ }
+ }
+ }
+
+ /**
+ * Initializes the protocol mappers of the client scope.
+ * @param protocolMappers the protocol mappers
+ * @param mapperName the protocol mapper name
+ * @param mapperPg the protocol mapper property group
+ * @throws Exception an Exception
+ */
+ void initializeProtocolMapper(ProtocolMappersResource protocolMappers, String mapperName, PropertyGroup mapperPg) throws Exception {
+ System.out.println("initializing protocol mapper: " + mapperName);
+ // Create protocol mapper if it does not exist
+ ProtocolMapperRepresentation protocolMapper = getProtocolMapperByName(protocolMappers, mapperName);
+ if (protocolMapper == null) {
+ protocolMapper = new ProtocolMapperRepresentation();
+ protocolMapper.setName(mapperName);
+ protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL));
+ protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER));
+ Response response = protocolMappers.createMapper(protocolMapper);
+ protocolMapper = getProtocolMapperByName(protocolMappers, mapperName);
+ if (protocolMapper == null) {
+ throw new RuntimeException("Unable to create protocol mapper: " + response.readEntity(String.class));
+ }
+ }
+
+ // Update protocol mapper settings
+ protocolMapper.setProtocol(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL));
+ protocolMapper.setProtocolMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER));
+ PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG);
+ if (configPg != null) {
+ Map config = protocolMapper.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ protocolMapper.setConfig(config);
+ }
+ protocolMappers.update(protocolMapper.getId(), protocolMapper);
+ }
+
+ /**
+ * Initializes the client.
+ * @param clients the clients resource
+ * @param clientScopes the client scopes resource
+ * @param clientId the client id
+ * @param clientPg the client property group
+ * @throws Exception an Exception
+ */
+ void initializeClient(ClientsResource clients, ClientScopesResource clientScopes, String clientId, PropertyGroup clientPg) throws Exception {
+ System.out.println("initializing client: " + clientId);
+ // Create client if it does not exist
+ ClientRepresentation client = getClientByClientId(clients, clientId);
+ if (client == null) {
+ client = new ClientRepresentation();
+ client.setClientId(clientId);
+ clients.create(client);
+ client = getClientByClientId(clients, clientId);
+ if (client == null) {
+ throw new RuntimeException("Unable to create client");
+ }
+ }
+
+ // Update client settings
+ client.setName(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_NAME));
+ client.setDescription(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_DESCRIPTION));
+ client.setConsentRequired(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_CONSENT_REQUIRED));
+ client.setStandardFlowEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_STANDARD_FLOW_ENABLED, true));
+ client.setServiceAccountsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED, false));
+
+ PropertyGroup attributePg = clientPg.getPropertyGroup(KeycloakConfig.PROP_CLIENT_ATTRIBUTES);
+ if (attributePg != null) {
+ setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED);
+ }
+
+ Boolean publicClient = clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_PUBLIC_CLIENT, false);
+ client.setPublicClient(publicClient);
+
+ if (!publicClient) {
+ String clientAuthType = clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_AUTHENTICATOR_TYPE);
+ client.setClientAuthenticatorType(clientAuthType);
+
+ if ("client-jwt".equals(clientAuthType) && attributePg != null) {
+ boolean useJwksUrl = Boolean.parseBoolean(attributePg.getStringProperty(KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL, "false"));
+ if (useJwksUrl) {
+ setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_USE_JWKS_URL);
+ setAttribute(attributePg, client, KeycloakConfig.PROP_CLIENT_ATTR_JWKS_URL);
+ }
+ }
+ }
+
+ client.setDirectAccessGrantsEnabled(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_DIRECT_ACCESS_ENABLED));
+ client.setBearerOnly(clientPg.getBooleanProperty(KeycloakConfig.PROP_CLIENT_BEARER_ONLY));
+ client.setRootUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ROOT_URL));
+ client.setRedirectUris(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_REDIRECT_URIS));
+ client.setAdminUrl(clientPg.getStringProperty(KeycloakConfig.PROP_CLIENT_ADMIN_URL));
+ client.setWebOrigins(clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_WEB_ORIGINS));
+ clients.get(client.getId()).update(client);
+
+ ClientResource cr = clients.get(client.getId());
+
+ // Remove default client scopes that no longer apply and collect the ones to add
+ List defaultClientScopeIdsToAdd = new ArrayList<>();
+ List defaultClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_DEFAULT_CLIENT_SCOPES);
+ if (defaultClientScopeNameStrings != null) {
+ List defaultClientScopeIds = getClientScopeIds(clientScopes, defaultClientScopeNameStrings);
+ if (defaultClientScopeIds != null) {
+ List existingDefaultClientScopes = cr.getDefaultClientScopes();
+ for (ClientScopeRepresentation existingDefaultClientScope : existingDefaultClientScopes) {
+ if (!defaultClientScopeIds.contains(existingDefaultClientScope.getId())) {
+ cr.removeDefaultClientScope(existingDefaultClientScope.getId());
+ }
+ else {
+ defaultClientScopeIds.remove(existingDefaultClientScope.getId());
+ }
+ }
+ defaultClientScopeIdsToAdd.addAll(defaultClientScopeIds);
+ }
+ }
+
+ // Remove optional client scopes that no longer apply and collect the ones to add
+ List optionalClientScopeIdsToAdd = new ArrayList<>();
+ List optionalClientScopeNameStrings = clientPg.getStringListProperty(KeycloakConfig.PROP_CLIENT_OPTIONAL_CLIENT_SCOPES);
+ if (optionalClientScopeNameStrings != null) {
+ List optionalClientScopeIds = getClientScopeIds(clientScopes, optionalClientScopeNameStrings);
+ if (optionalClientScopeIds != null) {
+ List existingOptionalClientScopes = cr.getOptionalClientScopes();
+ for (ClientScopeRepresentation existingOptionalClientScope : existingOptionalClientScopes) {
+ if (!optionalClientScopeIds.contains(existingOptionalClientScope.getId())) {
+ cr.removeDefaultClientScope(existingOptionalClientScope.getId());
+ }
+ else {
+ optionalClientScopeIds.remove(existingOptionalClientScope.getId());
+ }
+ }
+ optionalClientScopeIdsToAdd.addAll(optionalClientScopeIds);
+ }
+ }
+
+ // Note: if a scope already exists in either list on the server, the add call will be ignored
+ for (String clientScopeId : defaultClientScopeIdsToAdd) {
+ cr.addDefaultClientScope(clientScopeId);
+ }
+ for (String clientScopeId : optionalClientScopeIdsToAdd) {
+ cr.addOptionalClientScope(clientScopeId);
+ }
+ }
+
+ /**
+ * Client attributes are set a little differently, so this method encapsulates the logic to get the attribute map
+ * and set a given property from a PropertyGroup that contains that attributes value in a property by the same name.
+ * @param attributesPg
+ * @param client
+ * @param propName
+ * @throws Exception
+ */
+ private void setAttribute(PropertyGroup attributesPg, ClientRepresentation client, String propName) throws Exception {
+ client.getAttributes().put(propName, attributesPg.getStringProperty(propName));
+ }
+
+ /**
+ * Initializes the identity provider.
+ * @param identityProviders the identity providers resource
+ * @param identityProviderAlias the identity provider alias
+ * @param identityProviderPg the identity provider property group
+ * @throws Exception an Exception
+ */
+ void initializeIdentityProvider(IdentityProvidersResource identityProviders, String identityProviderAlias, PropertyGroup identityProviderPg) throws Exception {
+ System.out.println("initializing identity provider: " + identityProviderAlias);
+ // Create identity provider if it does not exist
+ IdentityProviderRepresentation identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias);
+ if (identityProvider == null) {
+ identityProvider = new IdentityProviderRepresentation();
+ identityProvider.setAlias(identityProviderAlias);
+ identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID));
+ PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG);
+ if (configPg != null) {
+ Map config = identityProvider.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET);
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ identityProvider.setConfig(config);
+ }
+ identityProviders.create(identityProvider);
+ identityProvider = getIdentityProviderByAlias(identityProviders, identityProviderAlias);
+ if (identityProvider == null) {
+ throw new RuntimeException("Unable to create identity provider");
+ }
+ }
+
+ // Update identity provider settings
+ identityProvider.setProviderId(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_PROVIDER_ID));
+ identityProvider.setDisplayName(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_DISPLAY_NAME));
+ identityProvider.setEnabled(identityProviderPg.getBooleanProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_ENABLED));
+ identityProvider.setFirstBrokerLoginFlowAlias(identityProviderPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS));
+ identityProvider.setPostBrokerLoginFlowAlias(identityProviderPg
+ .getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_POST_BROKER_LOGIN_FLOW_ALIAS));
+ PropertyGroup configPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_CONFIG);
+ if (configPg != null) {
+ Map config = identityProvider.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ config.remove(KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET);
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ identityProvider.setConfig(config);
+ }
+ identityProviders.get(identityProvider.getAlias()).update(identityProvider);
+
+ // Initialize identity provider mappers
+ PropertyGroup mappersPg = identityProviderPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPERS);
+ if (mappersPg != null) {
+ for (PropertyEntry mapperPe: mappersPg.getProperties()) {
+ String mapperName = mapperPe.getName();
+ PropertyGroup mapperPg = mappersPg.getPropertyGroup(mapperName);
+ initializeIdentityProviderMapper(identityProviders.get(identityProvider.getAlias()), identityProviderAlias, mapperName, mapperPg);
+ }
+ }
+ }
+
+ /**
+ * Initializes the mappers of the identity provider.
+ * @param identityProvider the identity provider
+ * @param identityProviderAlias the identity provider alias
+ * @param mapperName the identity provider mapper name
+ * @param mapperPg the identity provider mapper property group
+ * @throws Exception an Exception
+ */
+ void initializeIdentityProviderMapper(IdentityProviderResource identityProvider, String identityProviderAlias, String mapperName, PropertyGroup mapperPg) throws Exception {
+ System.out.println("initializing identity provider mapper: " + mapperName);
+ // Create protocol mapper if it does not exist
+ IdentityProviderMapperRepresentation identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName);
+ if (identityProviderMapper == null) {
+ identityProviderMapper = new IdentityProviderMapperRepresentation();
+ identityProviderMapper.setName(mapperName);
+ identityProviderMapper.setIdentityProviderAlias(identityProviderAlias);
+ identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER));
+ PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
+ if (configPg != null) {
+ Map config = identityProviderMapper.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ identityProviderMapper.setConfig(config);
+ }
+ identityProvider.addMapper(identityProviderMapper);
+ identityProviderMapper = getIdentityProvideMapperByName(identityProvider, mapperName);
+ if (identityProviderMapper == null) {
+ throw new RuntimeException("Unable to create identity provider mapper");
+ }
+ }
+
+ // Update identity provider mapper settings
+ identityProviderMapper.setIdentityProviderAlias(identityProviderAlias);
+ identityProviderMapper.setIdentityProviderMapper(mapperPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER));
+ PropertyGroup configPg = mapperPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
+ if (configPg != null) {
+ Map config = identityProviderMapper.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ identityProviderMapper.setConfig(config);
+ }
+ identityProvider.update(identityProviderMapper.getId(), identityProviderMapper);
+ }
+
+ /**
+ * Initializes the authentication flow.
+ * @param authMgmt the authorization management
+ * @param authenticationFlowAlias the authentication flow alias
+ * @param authenticationFlowPg the authentication flow property group
+ * @throws Exception an Exception
+ */
+ void initializeAuthenticationFlow(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, PropertyGroup authenticationFlowPg) throws Exception {
+ System.out.println("initializing authentication flow: " + authenticationFlowAlias);
+ // Get authentication flow
+ AuthenticationFlowRepresentation authenticationFlow = getAuthenticationFlowByAlias(authMgmt, authenticationFlowAlias);
+ if (authenticationFlow == null) {
+ authenticationFlow = new AuthenticationFlowRepresentation();
+ authenticationFlow.setAlias(authenticationFlowAlias);
+ authenticationFlow.setTopLevel(true);
+ authenticationFlow.setProviderId(authenticationFlowPg.getStringProperty("providerId"));
+ authenticationFlow.setBuiltIn(authenticationFlowPg.getBooleanProperty("builtIn"));
+
+ Response response = authMgmt.createFlow(authenticationFlow);
+
+ if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) {
+ String path = response.getLocation().getPath();
+ String id = path.substring(path.lastIndexOf("/") + 1);
+ System.out.println("Created flow with id '" + id + "'");
+ authenticationFlow.setId(id);
+ updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow);
+ } else {
+ System.err.println("Failed to create flow; status code '" + response.getStatus() + "'");
+ System.err.println(response.readEntity(String.class));
+ }
+ } else {
+ updateFlowWithExecutions(authMgmt, authenticationFlowPg, authenticationFlow);
+ }
+
+
+
+ // Update identity provider redirector
+ for (PropertyEntry authExecutionPe: authenticationFlowPg.getProperties()) {
+ String authExecutionType = authExecutionPe.getName();
+ if (KeycloakConfig.PROP_IDENTITY_REDIRECTOR.equals(authExecutionType)) {
+ PropertyGroup identityProviderRedirectorPg = authenticationFlowPg.getPropertyGroup(authExecutionType);
+ String identityProviderRedirectorAlias = identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS);
+ initializeIdentityProviderRedirector(authMgmt, authenticationFlowAlias, identityProviderRedirectorAlias, identityProviderRedirectorPg);
+ }
+ }
+ }
+
+ private void updateFlowWithExecutions(AuthenticationManagementResource authMgmt, PropertyGroup authenticationFlowPg,
+ AuthenticationFlowRepresentation authenticationFlow) throws Exception {
+ PropertyGroup authenticationExecutionsPg = authenticationFlowPg.getPropertyGroup("authenticationExecutions");
+ JsonObject jsonObject = authenticationFlowPg.getJsonValue("authenticationExecutions").asJsonObject();
+ for (String entry : jsonObject.keySet()) {
+
+ System.out.println("adding auth execution: " + entry);
+
+ PropertyGroup entryProps = authenticationExecutionsPg.getPropertyGroup(entry);
+
+ HashMap executionParams = new HashMap();
+
+ String description = entryProps.getStringProperty("description");
+ executionParams.put("description", description);
+
+ Boolean isFlow = entryProps.getBooleanProperty("authenticatorFlow", false);
+ if (isFlow) {
+ executionParams.put("alias", entry);
+ executionParams.put("type", "basic-flow");
+
+ AuthenticationExecutionInfoRepresentation executionFlow = getOrCreateExecution(authMgmt,
+ authenticationFlow.getAlias(), entry, isFlow, executionParams);
+
+ // the above "alias" actually gets saved as the display name for some reason, but the alias is what we need to add subflow executions
+ executionFlow.setAlias(entry);
+ executionFlow.setRequirement(entryProps.getStringProperty("requirement"));
+ authMgmt.updateExecutions(authenticationFlow.getAlias(), executionFlow);
+
+ PropertyGroup childExecutions = entryProps.getPropertyGroup("authenticationExecutions");
+ for (PropertyEntry childEntry : childExecutions.getProperties()) {
+ // TODO: see if we can get the display name from the authenticator provider_id somehow, instead of requiring it in our config
+ String displayName = childEntry.getName();
+ PropertyGroup childEntryPg = childExecutions.getPropertyGroup(displayName);
+
+ configExecution(childEntryPg, authMgmt, entry, displayName, authenticationFlow);
+ }
+ } else {
+ configExecution(entryProps, authMgmt, authenticationFlow.getAlias(), entry, authenticationFlow);
+ }
+ }
+ }
+
+ private void configExecution(PropertyGroup propGroup, AuthenticationManagementResource authMgmt, String entry,
+ String displayName, AuthenticationFlowRepresentation authenticationFlow) throws Exception {
+ String authenticator = propGroup.getStringProperty("authenticator");
+
+ Boolean childIsFlow = propGroup.getBooleanProperty("authenticatorFlow", false);
+ if (childIsFlow) {
+ System.out.println("Adding nested flow: " + displayName);
+
+ HashMap executionParams = new HashMap<>();
+
+ // String alias = propGroup.getStringProperty("alias");
+ String parentFlowAlias = entry;
+ String flowAlias = displayName;
+ String type = propGroup.getStringProperty("providerId");
+ // String provider = propGroup.getStringProperty("provider");
+ String description = propGroup.getStringProperty("description");
+
+ executionParams.put("alias", flowAlias);
+ executionParams.put("type", type);
+ // executionParams.put("provider", "xx");
+ executionParams.put("description", description);
+
+ authMgmt.addExecutionFlow(parentFlowAlias, executionParams);
+
+ // there doesn't seem to be a way to query for this, but the last added item
+ // should be the correct one
+ AuthenticationExecutionInfoRepresentation lastAdded = null;
+ for (AuthenticationExecutionInfoRepresentation flow : authMgmt.getExecutions(parentFlowAlias)) {
+ lastAdded = flow;
+ }
+
+ // have to update the requirement separately, and also the flowAlias doesn't get
+ // set for some reason
+ lastAdded.setAlias(flowAlias);
+ lastAdded.setRequirement(propGroup.getStringProperty("requirement"));
+ authMgmt.updateExecutions(parentFlowAlias, lastAdded);
+
+ // now fetch the nested flow so we can recursively add the executions to it
+ authenticationFlow = authMgmt.getFlow(lastAdded.getFlowId());
+ updateFlowWithExecutions(authMgmt, propGroup, authenticationFlow);
+ } else {
+ HashMap childExecutionParams = new HashMap<>();
+ childExecutionParams.put("provider", authenticator);
+ AuthenticationExecutionInfoRepresentation childExecution = getOrCreateExecution(authMgmt, entry, displayName,
+ childIsFlow, childExecutionParams);
+
+ String configAlias = propGroup.getStringProperty("configAlias");
+ JsonValue configJson = propGroup.getJsonValue("config");
+ if (configJson != null) {
+ Map config = buildConfigMap(configJson, configAlias);
+
+ AuthenticatorConfigRepresentation authenticatorConfig = getOrCreateAuthenticatorConfig(authMgmt,
+ childExecution, configAlias, config);
+ authenticatorConfig.setConfig(config);
+ authMgmt.updateAuthenticatorConfig(authenticatorConfig.getId(), authenticatorConfig);
+ childExecution.setAuthenticationConfig(configAlias);
+ }
+
+ childExecution.setRequirement(propGroup.getStringProperty("requirement"));
+ authMgmt.updateExecutions(authenticationFlow.getAlias(), childExecution);
+ }
+ }
+
+ private AuthenticatorConfigRepresentation getOrCreateAuthenticatorConfig(AuthenticationManagementResource authMgmt,
+ AuthenticationExecutionInfoRepresentation execution, String configAlias, Map config) {
+
+ AuthenticatorConfigRepresentation authenticatorConfig = null;
+
+ String configId = execution.getAuthenticationConfig();
+ if (configId != null) {
+ authenticatorConfig = authMgmt.getAuthenticatorConfig(configId);
+ } else {
+ authenticatorConfig = new AuthenticatorConfigRepresentation();
+ authenticatorConfig.setAlias(configAlias);
+ Response response = authMgmt.newExecutionConfig(execution.getId(), authenticatorConfig);
+
+ if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) {
+ String path = response.getLocation().getPath();
+ String id = path.substring(path.lastIndexOf("/") + 1);
+ System.out.println("Created authenticator config with id '" + id + "'");
+ authenticatorConfig.setId(id);
+ } else {
+ System.err.println("Failed to create authenticator config; status code '" + response.getStatus() + "'");
+ System.err.println(response.readEntity(String.class));
+ }
+ }
+
+ return authenticatorConfig;
+ }
+
+ private Map buildConfigMap(JsonValue configJson, String configAlias) {
+ Map config = new HashMap();
+ Set> entrySet = configJson.asJsonObject().entrySet();
+ for (Entry configEntry : entrySet) {
+ JsonValue value = configEntry.getValue();
+ if (value instanceof JsonString) {
+ config.put(configEntry.getKey(), ((JsonString) value).getString());
+ } else {
+ System.err.println("Expected config of type String, but found " + value.getValueType());
+ }
+ }
+ return config;
+ }
+
+ private AuthenticationExecutionInfoRepresentation getOrCreateExecution(AuthenticationManagementResource authMgmt,
+ String flowAlias, String displayName, boolean isFlow, HashMap executionParams) {
+ AuthenticationExecutionInfoRepresentation savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName);
+
+ // System.out.println("savedExecution1: " + savedExecution);
+
+ if (savedExecution == null) {
+ if (isFlow) {
+ authMgmt.addExecutionFlow(flowAlias, executionParams);
+ } else {
+ // System.out.println("calling addExecution for flowAlias: " + flowAlias);
+ // for (Map.Entry entry : executionParams.entrySet()) {
+ // System.out.println("addExecution param: " + entry.getKey() + " : " +
+ // entry.getValue());
+ // }
+ authMgmt.addExecution(flowAlias, executionParams);
+ }
+ savedExecution = getExecutionByDisplayName(authMgmt, flowAlias, displayName);
+ // System.out.println("savedExecution2: " + savedExecution);
+ }
+ if (savedExecution == null) {
+ throw new RuntimeException("Unable to create execution '" + displayName + "'");
+ }
+ return savedExecution;
+ }
+
+ /**
+ * Initializes the identity provider redirector.
+ * @param authMgmt the authorization management
+ * @param authenticationFlowAlias the authentication flow alias
+ * @param identityProviderRedirectorAlias the identity provider redirector alias
+ * @param identityProviderRedirectorPg the identity provider redirector property group
+ * @throws Exception an Exception
+ */
+ void initializeIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias, String identityProviderRedirectorAlias, PropertyGroup identityProviderRedirectorPg) throws Exception {
+ System.out.println("initializing identity provider redirector: " + identityProviderRedirectorAlias);
+ // Get identity provider redirector
+ AuthenticationExecutionInfoRepresentation identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias);
+ if (identityProviderRedirector == null) {
+ throw new RuntimeException("Identity provider redirector does not exist");
+ }
+
+ // Update identity provider redirector
+ identityProviderRedirector.setRequirement(identityProviderRedirectorPg.getStringProperty(KeycloakConfig.PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT));
+ authMgmt.updateExecutions(authenticationFlowAlias, identityProviderRedirector);
+ identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias);
+ if (identityProviderRedirector == null) {
+ throw new RuntimeException("Identity provider redirector does not exist");
+ }
+
+ // Create config representation if it does not exist
+ AuthenticatorConfigRepresentation configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null;
+ if (configRepresentation == null) {
+ configRepresentation = new AuthenticatorConfigRepresentation();
+ configRepresentation.setAlias(identityProviderRedirectorAlias);
+ PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
+ if (configPg != null) {
+ Map config = configRepresentation.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ configRepresentation.setConfig(config);
+ }
+ authMgmt.newExecutionConfig(identityProviderRedirector.getId(), configRepresentation);
+ identityProviderRedirector = getIdentityProviderRedirector(authMgmt, authenticationFlowAlias);
+ if (identityProviderRedirector == null) {
+ throw new RuntimeException("Identity provider redirector does not exist");
+ }
+ configRepresentation = identityProviderRedirector.getAuthenticationConfig() != null ? authMgmt.getAuthenticatorConfig(identityProviderRedirector.getAuthenticationConfig()) : null;
+ if (configRepresentation == null) {
+ throw new RuntimeException("Unable to create identity provider redirector");
+ }
+ }
+
+ // Update config representation
+ configRepresentation.setAlias(identityProviderRedirectorAlias);
+ PropertyGroup configPg = identityProviderRedirectorPg.getPropertyGroup(KeycloakConfig.PROP_IDENTITY_PROVIDER_MAPPER_CONFIG);
+ if (configPg != null) {
+ Map config = configRepresentation.getConfig();
+ if (config == null) {
+ config = new HashMap<>();
+ }
+ for (PropertyEntry configPe: configPg.getProperties()) {
+ String configKey = configPe.getName();
+ config.put(configKey, configPe.getValue() != null ? configPe.getValue().toString() : null);
+ }
+ configRepresentation.setConfig(config);
+ }
+ authMgmt.updateAuthenticatorConfig(configRepresentation.getId(), configRepresentation);
+ }
+
+ /**
+ * Initializes the group.
+ * @param groups the groups resource
+ * @param groupName the group name
+ * @param groupPg the group property group
+ * @throws Exception an Exception
+ */
+ void initializeGroup(GroupsResource groups, String groupName, PropertyGroup groupPg) throws Exception {
+ System.out.println("initializing group: " + groupName);
+ // Create group if it does not exist
+ GroupRepresentation group = getGroupByName(groups, groupName);
+ if (group == null) {
+ group = new GroupRepresentation();
+ group.setName(groupName);
+ groups.add(group);
+ group = getGroupByName(groups, groupName);
+ if (group == null) {
+ throw new RuntimeException("Unable to create group");
+ }
+ }
+
+ // Update group settings
+ PropertyGroup attributesPg = groupPg.getPropertyGroup(KeycloakConfig.PROP_GROUP_ATTRIBUTES);
+ if (attributesPg != null) {
+ Map> attributes = group.getAttributes();
+ if (attributes == null) {
+ attributes = new HashMap<>();
+ }
+ for (PropertyEntry attributePe: attributesPg.getProperties()) {
+ String attributeKey = attributePe.getName();
+ List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue());
+ attributes.put(attributeKey, attributeValue);
+ }
+ group.setAttributes(attributes);
+ }
+ groups.group(group.getId()).update(group);
+ }
+
+ /**
+ * Initializes the user.
+ * @param users the users resource
+ * @param groups the groups resource
+ * @param userName the user name
+ * @param userPg the user property group
+ * @throws Exception an Exception
+ */
+ void initializeUser(UsersResource users, GroupsResource groups, String userName, PropertyGroup userPg) throws Exception {
+ System.out.println("initializing user: " + userName);
+ // Create user if it does not exist
+ UserRepresentation user = getUserByName(users, userName);
+ if (user == null) {
+ user = new UserRepresentation();
+ user.setUsername(userName);
+ users.create(user);
+ user = getUserByName(users, userName);
+ if (user == null) {
+ throw new RuntimeException("Unable to create user");
+ }
+ }
+
+ // Update user settings
+ user.setEnabled(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_ENABLED));
+ PropertyGroup attributesPg = userPg.getPropertyGroup(KeycloakConfig.PROP_USER_ATTRIBUTES);
+ if (attributesPg != null) {
+ Map> attributes = user.getAttributes();
+ if (attributes == null) {
+ attributes = new HashMap<>();
+ }
+ for (PropertyEntry attributePe: attributesPg.getProperties()) {
+ String attributeKey = attributePe.getName();
+ List attributeValue = PropertyGroup.convertToStringList(attributePe.getValue());
+ attributes.put(attributeKey, attributeValue);
+ }
+ user.setAttributes(attributes);
+ }
+ CredentialRepresentation credential = new CredentialRepresentation();
+ credential.setType(KeycloakConfig.KEYCLOAK_USER_PASSWORD_TYPE);
+ credential.setTemporary(userPg.getBooleanProperty(KeycloakConfig.PROP_USER_PASSWORD_TEMPORARY));
+ credential.setValue(userPg.getStringProperty(KeycloakConfig.PROP_USER_PASSWORD));
+ user.setCredentials(Arrays.asList(credential));
+ users.get(user.getId()).update(user);
+
+ // Update user group memberships
+ List groupIds = getGroupIds(groups, userPg.getStringListProperty(KeycloakConfig.PROP_USER_GROUPS));
+ if (groupIds != null) {
+ List existingGroupIds = getGroupIds(groups, user.getGroups());
+ for (String existingGroupId : existingGroupIds) {
+ if (!groupIds.contains(existingGroupId)) {
+ users.get(user.getId()).leaveGroup(existingGroupId);
+ }
+ else {
+ groupIds.remove(existingGroupId);
+ }
+ }
+ for (String groupId : groupIds) {
+ users.get(user.getId()).joinGroup(groupId);
+ }
+ }
+ }
+
+ /**
+ * Gets the realm by name.
+ * @param realmsResource the realms resource
+ * @param realmName the realm name
+ * @return the realm, or null if not found
+ */
+ private RealmRepresentation getRealmByName(RealmsResource realmsResource, String realmName) {
+ for (RealmRepresentation realm : realmsResource.findAll()) {
+ if (realmName.equals(realm.getRealm())) {
+ return realm;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the client scope by name.
+ * @param clientScopes the client scopes
+ * @param clientScopeName the client scope name
+ * @return the client scope, or null if not found
+ */
+ private ClientScopeRepresentation getClientScopeByName(ClientScopesResource clientScopes, String clientScopeName) {
+ for (ClientScopeRepresentation clientScope : clientScopes.findAll()) {
+ if (clientScopeName.equals(clientScope.getName())) {
+ return clientScope;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the client scope IDs by name.
+ * @param clientScopes the client scopes
+ * @param clientScopeNames the client scope names
+ * @return the client scope IDs
+ */
+ private List getClientScopeIds(ClientScopesResource clientScopes, List clientScopeNames) {
+ List clientScopeIds = new ArrayList<>();
+ Map nameToIdMap = clientScopes.findAll().stream().collect(Collectors.toMap(c -> c.getName(), c -> c.getId()));
+
+ for (String clientScopeName : clientScopeNames) {
+ if (nameToIdMap.containsKey(clientScopeName)) {
+ clientScopeIds.add(nameToIdMap.get(clientScopeName));
+ } else {
+ System.err.println("Skipping client scope '" + clientScopeName + "'; unable to find id for client scope with this name");
+ }
+ }
+ return clientScopeIds;
+ }
+
+ /**
+ * Gets the client by client ID.
+ * @param adminClient the clients
+ * @param clientName the client name
+ * @return the client, or null if not found
+ */
+ private ClientRepresentation getClientByClientId(ClientsResource clients, String clientId) {
+ for (ClientRepresentation client : clients.findAll()) {
+ if (clientId.equals(client.getClientId())) {
+ return client;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the protocol mapper by name.
+ * @param protocolMappers the protocol mappers
+ * @param mapperName the mapper name
+ * @return the protocol mapper, or null if not found
+ */
+ private ProtocolMapperRepresentation getProtocolMapperByName(ProtocolMappersResource protocolMappers, String mapperName) {
+ for (ProtocolMapperRepresentation protocolMapper : protocolMappers.getMappers()) {
+ if (mapperName.equals(protocolMapper.getName())) {
+ return protocolMapper;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the identity provider by provider alias.
+ * @param identityProviders the identity providers
+ * @param identityProviderAlias the identity provider alias
+ * @return the identity provider, or null if not found
+ */
+ private IdentityProviderRepresentation getIdentityProviderByAlias(IdentityProvidersResource identityProviders, String identityProviderAlias) {
+ for (IdentityProviderRepresentation identityProvider : identityProviders.findAll()) {
+ if (identityProviderAlias.equals(identityProvider.getAlias())) {
+ return identityProvider;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the identity provider mapper by name.
+ * @param identity provider the identity provider
+ * @param mapperName the mapper name
+ * @return the identity provider mapper, or null if not found
+ */
+ private IdentityProviderMapperRepresentation getIdentityProvideMapperByName(IdentityProviderResource identityProvider, String mapperName) {
+ for (IdentityProviderMapperRepresentation identityProviderMapper : identityProvider.getMappers()) {
+ if (mapperName.equals(identityProviderMapper.getName())) {
+ return identityProviderMapper;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Gets the authentication flow by alias.
+ * @param authMgmt the authorization management
+ * @param authenticationFlowAlias the authentication flow alias
+ * @return the authorization flow, or null if not found
+ */
+ private AuthenticationFlowRepresentation getAuthenticationFlowByAlias(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) {
+ for (AuthenticationFlowRepresentation flow : authMgmt.getFlows()) {
+ if (authenticationFlowAlias.equals(flow.getAlias())) {
+ return flow;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the authentication execution by alias.
+ * @param authMgmt the authorization management
+ * @param authenticationFlowAlias the authentication flow alias
+ * @return the execution info, or null if not found
+ */
+ private AuthenticationExecutionInfoRepresentation getExecutionByDisplayName(AuthenticationManagementResource authMgmt, String authenticationFlowAlias,
+ String executionDisplayName) {
+ for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) {
+ if (executionDisplayName.equals(execution.getDisplayName())) {
+ return execution;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the identity provider redirector by alias.
+ * @param authMgmt the authorization management
+ * @param authenticationFlowAlias the authentication flow alias
+ * @return the authorization flow, or null if not found
+ */
+ private AuthenticationExecutionInfoRepresentation getIdentityProviderRedirector(AuthenticationManagementResource authMgmt, String authenticationFlowAlias) {
+ for (AuthenticationExecutionInfoRepresentation execution : authMgmt.getExecutions(authenticationFlowAlias)) {
+ if (KeycloakConfig.KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR.equals(execution.getProviderId())) {
+ return execution;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Gets the group by name.
+ * @param groups the groups
+ * @param groupName the group name
+ * @return the group, or null if not found
+ */
+ private GroupRepresentation getGroupByName(GroupsResource groups, String groupName) {
+ for (GroupRepresentation group : groups.groups()) {
+ if (groupName.equals(group.getName())) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the group IDs by name.
+ * @param groups the groups
+ * @param groupNames the group names
+ * @return the group IDs
+ */
+ private List getGroupIds(GroupsResource groups, List groupNames) {
+ List groupIds = new ArrayList<>();
+ for (GroupRepresentation group : groups.groups()) {
+ if (groupNames != null && groupNames.contains(group.getName())) {
+ groupIds.add(group.getId());
+ }
+ }
+ return groupIds;
+ }
+
+ /**
+ * Gets the user by name.
+ * @param users the users
+ * @param userName the user name
+ * @return the user, or null if not found
+ */
+ private UserRepresentation getUserByName(UsersResource users, String userName) {
+ for (UserRepresentation user : users.list()) {
+ if (userName.equals(user.getUsername())) {
+ return user;
+ }
+ }
+ return null;
+ }
}
diff --git a/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java b/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java
index d6cdfba..69f31d5 100644
--- a/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java
+++ b/keycloak-config/src/main/java/org/alvearie/keycloak/config/util/KeycloakConfig.java
@@ -2,7 +2,7 @@
(C) Copyright IBM Corp. 2021
SPDX-License-Identifier: Apache-2.0
-*/
+ */
package org.alvearie.keycloak.config.util;
import java.io.File;
@@ -29,302 +29,303 @@
*/
public class KeycloakConfig {
- // Keycloak configuration property names (top-level)
- public static final String PROP_KEYCLOAK_SERVER_URL = "keycloak|serverUrl";
- public static final String PROP_KEYCLOAK_ADMIN_USER = "keycloak|adminUser";
- public static final String PROP_KEYCLOAK_ADMIN_PW = "keycloak|adminPassword";
- public static final String PROP_KEYCLOAK_ADMIN_CLIENT_ID = "keycloak|adminClientId";
- public static final String PROP_KEYCLOAK_REALMS = "keycloak|realms";
+ // Keycloak configuration property names (top-level)
+ public static final String PROP_KEYCLOAK_SERVER_URL = "keycloak|serverUrl";
+ public static final String PROP_KEYCLOAK_ADMIN_USER = "keycloak|adminUser";
+ public static final String PROP_KEYCLOAK_ADMIN_PW = "keycloak|adminPassword";
+ public static final String PROP_KEYCLOAK_ADMIN_CLIENT_ID = "keycloak|adminClientId";
+ public static final String PROP_KEYCLOAK_REALMS = "keycloak|realms";
- // Keycloak configuration property names (relative)
- public static final String PROP_REALM_ENABLED = "enabled";
- public static final String PROP_CLIENT_SCOPES = "clientScopes";
- public static final String PROP_CLIENT_SCOPE_DESCRIPTION = "description";
- public static final String PROP_CLIENT_SCOPE_PROTOCOL = "protocol";
- public static final String PROP_CLIENT_SCOPE_ATTRIBUTES = "attributes";
- public static final String PROP_CLIENT_SCOPE_MAPPERS = "mappers";
- public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL = "protocol";
- public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER = "protocolmapper";
- public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG = "config";
- public static final String PROP_DEFAULT_DEFAULT_CLIENT_SCOPES = "defaultDefaultClientScopes";
- public static final String PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES = "defaultOptionalClientScopes";
- public static final String PROP_IDENTITY_PROVIDERS = "identityProviders";
- public static final String PROP_IDENTITY_PROVIDER_DISPLAY_NAME = "displayName";
- public static final String PROP_IDENTITY_PROVIDER_ENABLED = "enabled";
- public static final String PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS = "firstBrokerLoginFlowAlias";
- public static final String PROP_IDENTITY_PROVIDER_CONFIG = "config";
- public static final String PROP_IDENTITY_PROVIDER_PROVIDER_ID = "providerId";
- public static final String PROP_IDENTITY_PROVIDER_MAPPERS = "mappers";
- public static final String PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER = "identityProviderMapper";
- public static final String PROP_IDENTITY_PROVIDER_MAPPER_CONFIG = "config";
- public static final String PROP_CLIENTS = "clients";
- public static final String PROP_CLIENT_NAME = "name";
- public static final String PROP_CLIENT_DESCRIPTION = "description";
- public static final String PROP_CLIENT_CONSENT_REQUIRED = "consentRequired";
- public static final String PROP_CLIENT_PUBLIC_CLIENT = "publicClient";
- public static final String PROP_CLIENT_AUTHENTICATOR_TYPE = "clientAuthenticatorType";
- public static final String PROP_CLIENT_BEARER_ONLY = "bearerOnly";
- public static final String PROP_CLIENT_DIRECT_ACCESS_ENABLED = "enableDirectAccess";
- public static final String PROP_CLIENT_DEFAULT_CLIENT_SCOPES = "defaultClientScopes";
- public static final String PROP_CLIENT_OPTIONAL_CLIENT_SCOPES = "optionalClientScopes";
- public static final String PROP_CLIENT_ROOT_URL = "rootURL";
- public static final String PROP_CLIENT_REDIRECT_URIS = "redirectURIs";
- public static final String PROP_CLIENT_ADMIN_URL = "adminURL";
- public static final String PROP_CLIENT_WEB_ORIGINS = "webOrigins";
- public static final String PROP_CLIENT_STANDARD_FLOW_ENABLED = "standardFlowEnabled";
- public static final String PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED = "serviceAccountsEnabled";
- public static final String PROP_CLIENT_ATTRIBUTES = "attributes";
- public static final String PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled";
- public static final String PROP_CLIENT_ATTR_USE_JWKS_URL = "use.jwks.url";
- public static final String PROP_CLIENT_ATTR_JWKS_URL = "jwks.url";
- public static final String PROP_AUTHENTICATION_FLOWS = "authenticationFlows";
- public static final String PROP_BROWSER_FLOW = "browserFlow";
- public static final String PROP_IDENTITY_REDIRECTOR = "identityProviderRedirector";
- public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS = "alias";
- public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT = "requirement";
- public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_DEFAULT_PROVIDER = "defaultProvider";
- public static final String PROP_GROUPS = "groups";
- public static final String PROP_GROUP_ATTRIBUTES = "attributes";
- public static final String PROP_DEFAULT_GROUPS = "defaultGroups";
- public static final String PROP_USERS = "users";
- public static final String PROP_USER_ENABLED = "enabled";
- public static final String PROP_USER_PASSWORD = "password";
- public static final String PROP_USER_PASSWORD_TEMPORARY = "passwordTemporary";
- public static final String PROP_USER_ATTRIBUTES = "attributes";
- public static final String PROP_USER_GROUPS = "groups";
- public static final String KEYCLOAK_USER_PASSWORD_TYPE = "password";
- public static final String KEYCLOAK_FIRST_BROKER_LOGIN = "first broker login";
- public static final String KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET = "clientSecret";
- public static final String KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR = "identity-provider-redirector";
- public static final String PROP_EVENTS_CONFIG = "eventsConfig";
- public static final String PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS = "saveLoginEvents";
- public static final String PROP_EVENTS_CONFIG_EXPIRATION = "expiration";
- public static final String PROP_EVENTS_CONFIG_SAVE_TYPES = "types";
- public static final String PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS = "saveAdminEvents";
+ // Keycloak configuration property names (relative)
+ public static final String PROP_REALM_ENABLED = "enabled";
+ public static final String PROP_CLIENT_SCOPES = "clientScopes";
+ public static final String PROP_CLIENT_SCOPE_DESCRIPTION = "description";
+ public static final String PROP_CLIENT_SCOPE_PROTOCOL = "protocol";
+ public static final String PROP_CLIENT_SCOPE_ATTRIBUTES = "attributes";
+ public static final String PROP_CLIENT_SCOPE_MAPPERS = "mappers";
+ public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL = "protocol";
+ public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER = "protocolmapper";
+ public static final String PROP_CLIENT_SCOPE_MAPPER_PROTOCOL_MAPPER_CONFIG = "config";
+ public static final String PROP_DEFAULT_DEFAULT_CLIENT_SCOPES = "defaultDefaultClientScopes";
+ public static final String PROP_DEFAULT_OPTIONAL_CLIENT_SCOPES = "defaultOptionalClientScopes";
+ public static final String PROP_IDENTITY_PROVIDERS = "identityProviders";
+ public static final String PROP_IDENTITY_PROVIDER_DISPLAY_NAME = "displayName";
+ public static final String PROP_IDENTITY_PROVIDER_ENABLED = "enabled";
+ public static final String PROP_IDENTITY_PROVIDER_FIRST_BROKER_LOGIN_FLOW_ALIAS = "firstBrokerLoginFlowAlias";
+ public static final String PROP_IDENTITY_PROVIDER_POST_BROKER_LOGIN_FLOW_ALIAS = "postBrokerLoginFlowAlias";
+ public static final String PROP_IDENTITY_PROVIDER_CONFIG = "config";
+ public static final String PROP_IDENTITY_PROVIDER_PROVIDER_ID = "providerId";
+ public static final String PROP_IDENTITY_PROVIDER_MAPPERS = "mappers";
+ public static final String PROP_IDENTITY_PROVIDER_MAPPER_IDENTITY_PROVIDER_MAPPER = "identityProviderMapper";
+ public static final String PROP_IDENTITY_PROVIDER_MAPPER_CONFIG = "config";
+ public static final String PROP_CLIENTS = "clients";
+ public static final String PROP_CLIENT_NAME = "name";
+ public static final String PROP_CLIENT_DESCRIPTION = "description";
+ public static final String PROP_CLIENT_CONSENT_REQUIRED = "consentRequired";
+ public static final String PROP_CLIENT_PUBLIC_CLIENT = "publicClient";
+ public static final String PROP_CLIENT_AUTHENTICATOR_TYPE = "clientAuthenticatorType";
+ public static final String PROP_CLIENT_BEARER_ONLY = "bearerOnly";
+ public static final String PROP_CLIENT_DIRECT_ACCESS_ENABLED = "enableDirectAccess";
+ public static final String PROP_CLIENT_DEFAULT_CLIENT_SCOPES = "defaultClientScopes";
+ public static final String PROP_CLIENT_OPTIONAL_CLIENT_SCOPES = "optionalClientScopes";
+ public static final String PROP_CLIENT_ROOT_URL = "rootURL";
+ public static final String PROP_CLIENT_REDIRECT_URIS = "redirectURIs";
+ public static final String PROP_CLIENT_ADMIN_URL = "adminURL";
+ public static final String PROP_CLIENT_WEB_ORIGINS = "webOrigins";
+ public static final String PROP_CLIENT_STANDARD_FLOW_ENABLED = "standardFlowEnabled";
+ public static final String PROP_CLIENT_SERVICE_ACCOUNTS_ENABLED = "serviceAccountsEnabled";
+ public static final String PROP_CLIENT_ATTRIBUTES = "attributes";
+ public static final String PROP_CLIENT_ATTR_DEVICE_AUTH_GRANT_ENABLED = "oauth2.device.authorization.grant.enabled";
+ public static final String PROP_CLIENT_ATTR_USE_JWKS_URL = "use.jwks.url";
+ public static final String PROP_CLIENT_ATTR_JWKS_URL = "jwks.url";
+ public static final String PROP_AUTHENTICATION_FLOWS = "authenticationFlows";
+ public static final String PROP_BROWSER_FLOW = "browserFlow";
+ public static final String PROP_IDENTITY_REDIRECTOR = "identityProviderRedirector";
+ public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_ALIAS = "alias";
+ public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_REQUIREMENT = "requirement";
+ public static final String PROP_IDENTITY_PROVIDER_REDIRECTOR_DEFAULT_PROVIDER = "defaultProvider";
+ public static final String PROP_GROUPS = "groups";
+ public static final String PROP_GROUP_ATTRIBUTES = "attributes";
+ public static final String PROP_DEFAULT_GROUPS = "defaultGroups";
+ public static final String PROP_USERS = "users";
+ public static final String PROP_USER_ENABLED = "enabled";
+ public static final String PROP_USER_PASSWORD = "password";
+ public static final String PROP_USER_PASSWORD_TEMPORARY = "passwordTemporary";
+ public static final String PROP_USER_ATTRIBUTES = "attributes";
+ public static final String PROP_USER_GROUPS = "groups";
+ public static final String KEYCLOAK_USER_PASSWORD_TYPE = "password";
+ public static final String KEYCLOAK_FIRST_BROKER_LOGIN = "first broker login";
+ public static final String KEYCLOAK_IDENTITY_PROVIDER_CLIENT_SECRET = "clientSecret";
+ public static final String KEYCLOAK_IDENTITY_PROVIDER_REDIRECTOR = "identity-provider-redirector";
+ public static final String PROP_EVENTS_CONFIG = "eventsConfig";
+ public static final String PROP_EVENTS_CONFIG_SAVE_LOGIN_EVENTS = "saveLoginEvents";
+ public static final String PROP_EVENTS_CONFIG_EXPIRATION = "expiration";
+ public static final String PROP_EVENTS_CONFIG_SAVE_TYPES = "types";
+ public static final String PROP_EVENTS_CONFIG_SAVE_ADMIN_EVENTS = "saveAdminEvents";
- private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(null);
+ private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(null);
- private String fileName;
- private PropertyGroup config;
+ private String fileName;
+ private PropertyGroup config;
- /**
- * Instantiates configuration using the specified file name.
- *
- * @param fileName
- * the JSON file containing keycloak configuration
- */
- public KeycloakConfig(String fileName) {
- this.fileName = fileName;
- }
+ /**
+ * Instantiates configuration using the specified file name.
+ *
+ * @param fileName
+ * the JSON file containing keycloak configuration
+ */
+ public KeycloakConfig(String fileName) {
+ this.fileName = fileName;
+ }
- /**
- * Gets the property group.
- *
- * @param propertyName
- * the property name
- * @return the property group
- */
- public PropertyGroup getPropertyGroup(String propertyName) {
- return getTypedProperty(PropertyGroup.class, propertyName, null);
- }
+ /**
+ * Gets the property group.
+ *
+ * @param propertyName
+ * the property name
+ * @return the property group
+ */
+ public PropertyGroup getPropertyGroup(String propertyName) {
+ return getTypedProperty(PropertyGroup.class, propertyName, null);
+ }
- /**
- * Gets the property value as a string.
- *
- * @param propertyName
- * the property name
- * @return the property value
- */
- public String getStringProperty(String propertyName) {
- return getTypedProperty(String.class, propertyName, null);
- }
+ /**
+ * Gets the property value as a string.
+ *
+ * @param propertyName
+ * the property name
+ * @return the property value
+ */
+ public String getStringProperty(String propertyName) {
+ return getTypedProperty(String.class, propertyName, null);
+ }
- /**
- * Gets the property value as a boolean.
- *
- * @param propertyName
- * the property name
- * @return the property value
- */
- public Boolean getBooleanProperty(String propertyName) {
- return getTypedProperty(Boolean.class, propertyName, null);
- }
+ /**
+ * Gets the property value as a boolean.
+ *
+ * @param propertyName
+ * the property name
+ * @return the property value
+ */
+ public Boolean getBooleanProperty(String propertyName) {
+ return getTypedProperty(Boolean.class, propertyName, null);
+ }
- /**
- * Gets the property value as an integer.
- *
- * @param propertyName
- * the property name
- * @return the property value
- */
- public Integer getIntProperty(String propertyName) {
- return getTypedProperty(Integer.class, propertyName, null);
- }
+ /**
+ * Gets the property value as an integer.
+ *
+ * @param propertyName
+ * the property name
+ * @return the property value
+ */
+ public Integer getIntProperty(String propertyName) {
+ return getTypedProperty(Integer.class, propertyName, null);
+ }
- /**
- * Gets the property value as a double.
- *
- * @param propertyName
- * the property name
- * @return the property value
- */
- public Double getDoubleProperty(String propertyName) {
- return getTypedProperty(Double.class, propertyName, null);
- }
+ /**
+ * Gets the property value as a double.
+ *
+ * @param propertyName
+ * the property name
+ * @return the property value
+ */
+ public Double getDoubleProperty(String propertyName) {
+ return getTypedProperty(Double.class, propertyName, null);
+ }
- /**
- * Gets the property value as a list.
- *
- * @param propertyName
- * the property name
- * @return the property value
- */
- @SuppressWarnings("unchecked")
- public List getStringListProperty(String propertyName) {
- return getTypedProperty(List.class, propertyName, null);
- }
+ /**
+ * Gets the property value as a list.
+ *
+ * @param propertyName
+ * the property name
+ * @return the property value
+ */
+ @SuppressWarnings("unchecked")
+ public List getStringListProperty(String propertyName) {
+ return getTypedProperty(List.class, propertyName, null);
+ }
- /**
- * This function retrieves the specified property as a generic JsonValue.
- *
- * @param propertyName
- * the hierarchical name of the property to be retrieved (e.g. "level1/level2/prop1")
- * @return a JsonValue representing the property's value or null if it wasn't found
- */
- private JsonValue getProperty(String propertyName) {
- JsonValue result = null;
- PropertyGroup pg = loadConfiguration();
- if (pg != null) {
- result = pg.getJsonValue(propertyName);
- }
- return result;
- }
+ /**
+ * This function retrieves the specified property as a generic JsonValue.
+ *
+ * @param propertyName
+ * the hierarchical name of the property to be retrieved (e.g. "level1/level2/prop1")
+ * @return a JsonValue representing the property's value or null if it wasn't found
+ */
+ private JsonValue getProperty(String propertyName) {
+ JsonValue result = null;
+ PropertyGroup pg = loadConfiguration();
+ if (pg != null) {
+ result = pg.getJsonValue(propertyName);
+ }
+ return result;
+ }
- /**
- * Loads the specified file as a JSON file and returns a PropertyGroup containing the contents of the JSON file as
- * the root property group.
- *
- * @param filename
- * the name of the JSON file to be loaded
- */
- private PropertyGroup loadConfiguration() {
- if (config == null) {
- try (InputStream is = resolveFile(fileName)) {
- String templatedJson = IOUtils.toString(is, StandardCharsets.UTF_8);
- String resolvedJson = StringSubstitutor.replace(templatedJson, EnvironmentVariables.get());
- try (JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(resolvedJson))) {
- JsonObject jsonObj = reader.readObject();
- reader.close();
- config = new PropertyGroup(jsonObj);
- }
- } catch (IOException e) {
- throw new RuntimeException("Unable to load configuration", e);
- }
- }
- return config;
- }
+ /**
+ * Loads the specified file as a JSON file and returns a PropertyGroup containing the contents of the JSON file as
+ * the root property group.
+ *
+ * @param filename
+ * the name of the JSON file to be loaded
+ */
+ private PropertyGroup loadConfiguration() {
+ if (config == null) {
+ try (InputStream is = resolveFile(fileName)) {
+ String templatedJson = IOUtils.toString(is, StandardCharsets.UTF_8);
+ String resolvedJson = StringSubstitutor.replace(templatedJson, EnvironmentVariables.get());
+ try (JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(resolvedJson))) {
+ JsonObject jsonObj = reader.readObject();
+ reader.close();
+ config = new PropertyGroup(jsonObj);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to load configuration", e);
+ }
+ }
+ return config;
+ }
- /**
- * Returns an InputStream for the specified filename. This function will first try to open the file using the
- * filename as a relative or absolute filename. If that fails, then we'll try to find the file on the classpath.
- *
- * @param filename
- * the name of the file to search for
- * @return an InputStream to the file or throws a FileNotFoundException if not found
- * @throws FileNotFoundException
- */
- private static InputStream resolveFile(String filename) throws FileNotFoundException {
- // First, try to use the filename as-is.
- File f = new File(filename);
- if (f.exists()) {
- return new FileInputStream(f);
- }
+ /**
+ * Returns an InputStream for the specified filename. This function will first try to open the file using the
+ * filename as a relative or absolute filename. If that fails, then we'll try to find the file on the classpath.
+ *
+ * @param filename
+ * the name of the file to search for
+ * @return an InputStream to the file or throws a FileNotFoundException if not found
+ * @throws FileNotFoundException
+ */
+ private static InputStream resolveFile(String filename) throws FileNotFoundException {
+ // First, try to use the filename as-is.
+ File f = new File(filename);
+ if (f.exists()) {
+ return new FileInputStream(f);
+ }
- // Next, look on the classpath.
- InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
- if (is != null) {
- return is;
- }
+ // Next, look on the classpath.
+ InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
+ if (is != null) {
+ return is;
+ }
- throw new FileNotFoundException("File '" + filename + "' was not found.");
- }
+ throw new FileNotFoundException("File '" + filename + "' was not found.");
+ }
- /**
- * This generic function will perform the work of retrieving a property and then converting the resulting value to
- * the appropriate type.
- *
- * @param propertyName
- * the name of the property to retrieve
- * @param defaultValue
- * the default value to return in the event that the property is not found
- * @return the typed property
- */
- @SuppressWarnings("unchecked")
- private T getTypedProperty(Class expectedDataType, String propertyName, T defaultValue) {
- T result = null;
+ /**
+ * This generic function will perform the work of retrieving a property and then converting the resulting value to
+ * the appropriate type.
+ *
+ * @param propertyName
+ * the name of the property to retrieve
+ * @param defaultValue
+ * the default value to return in the event that the property is not found
+ * @return the typed property
+ */
+ @SuppressWarnings("unchecked")
+ private T getTypedProperty(Class expectedDataType, String propertyName, T defaultValue) {
+ T result = null;
- // Find the property as a generic JsonValue from either the current tenant's config or the default config.
- JsonValue jsonValue = getProperty(propertyName);
+ // Find the property as a generic JsonValue from either the current tenant's config or the default config.
+ JsonValue jsonValue = getProperty(propertyName);
- // If found, then convert the value to the expected type.
- if (jsonValue != null) {
- Object obj = null;
- try {
- obj = PropertyGroup.convertJsonValue(jsonValue);
- if (obj != null) {
- // If the property was of the expected type, then just do the assignment.
- // Otherwise, we'll try to do some simple conversions (e.g. String --> Boolean).
- if (expectedDataType.isAssignableFrom(obj.getClass())) {
- result = (T) obj;
- } else {
- if (obj instanceof String) {
- if (Boolean.class.equals(expectedDataType)) {
- result = (T) Boolean.valueOf((String) obj);
- } else if (Integer.class.equals(expectedDataType)) {
- result = (T) Integer.valueOf((String) obj);
- } else if (Double.class.equals(expectedDataType)) {
- result = (T) Double.valueOf((String) obj);
- } else {
- throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName()
- + ", but was of type "
- + obj.getClass().getName());
- }
- } else if (obj instanceof Boolean) {
- if (String.class.equals(expectedDataType)) {
- result = (T) ((Boolean) obj).toString();
- } else {
- throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName()
- + ", but was of type "
- + obj.getClass().getName());
- }
- } else {
- throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName()
- + ", but was of type "
- + obj.getClass().getName());
- }
- }
- }
- } catch (Exception e) {
- throw new RuntimeException("Unexpected error converting property '" + propertyName + "' to native type.", e);
- }
- }
+ // If found, then convert the value to the expected type.
+ if (jsonValue != null) {
+ Object obj = null;
+ try {
+ obj = PropertyGroup.convertJsonValue(jsonValue);
+ if (obj != null) {
+ // If the property was of the expected type, then just do the assignment.
+ // Otherwise, we'll try to do some simple conversions (e.g. String --> Boolean).
+ if (expectedDataType.isAssignableFrom(obj.getClass())) {
+ result = (T) obj;
+ } else {
+ if (obj instanceof String) {
+ if (Boolean.class.equals(expectedDataType)) {
+ result = (T) Boolean.valueOf((String) obj);
+ } else if (Integer.class.equals(expectedDataType)) {
+ result = (T) Integer.valueOf((String) obj);
+ } else if (Double.class.equals(expectedDataType)) {
+ result = (T) Double.valueOf((String) obj);
+ } else {
+ throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName()
+ + ", but was of type "
+ + obj.getClass().getName());
+ }
+ } else if (obj instanceof Boolean) {
+ if (String.class.equals(expectedDataType)) {
+ result = (T) ((Boolean) obj).toString();
+ } else {
+ throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName()
+ + ", but was of type "
+ + obj.getClass().getName());
+ }
+ } else {
+ throw new RuntimeException("Expected property " + propertyName + " to be of type " + expectedDataType.getName()
+ + ", but was of type "
+ + obj.getClass().getName());
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected error converting property '" + propertyName + "' to native type.", e);
+ }
+ }
- return (result != null ? result : defaultValue);
- }
+ return (result != null ? result : defaultValue);
+ }
- /**
- * Utility class that allows mocking system environment variables retrieval in test classes (as Mockito disallows
- * mocking static methods of {@link System}).
- */
- public static class EnvironmentVariables {
- /**
- * Simple proxy method for {@link System#getenv()} that returns an unmodifiable string map view of the current
- * system environment.
- *
- * @return the environment as a map of variable names to values
- */
- public static Map get() {
- return System.getenv();
- }
- }
+ /**
+ * Utility class that allows mocking system environment variables retrieval in test classes (as Mockito disallows
+ * mocking static methods of {@link System}).
+ */
+ public static class EnvironmentVariables {
+ /**
+ * Simple proxy method for {@link System#getenv()} that returns an unmodifiable string map view of the current
+ * system environment.
+ *
+ * @return the environment as a map of variable names to values
+ */
+ public static Map get() {
+ return System.getenv();
+ }
+ }
}
diff --git a/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json b/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json
index 156c5e9..ee28240 100644
--- a/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json
+++ b/keycloak-config/src/main/resources/config/keycloak-config-with-idp.json
@@ -1,6 +1,6 @@
{
"keycloak": {
- "serverUrl": "http://keycloak:8080/auth",
+ "serverUrl": "http://keycloak:8080",
"adminUser": "${KEYCLOAK_USER}",
"adminPassword": "${KEYCLOAK_PASSWORD}",
"adminClientId": "admin-cli",
diff --git a/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java b/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java
index 999a2ac..5f11ba7 100644
--- a/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java
+++ b/keycloak-config/src/test/java/org/alvearie/keycloak/config/util/PropertyGroupTest.java
@@ -5,19 +5,19 @@
*/
package org.alvearie.keycloak.config.util;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import org.alvearie.keycloak.config.util.PropertyGroup.PropertyEntry;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import jakarta.json.Json;
import jakarta.json.JsonBuilderFactory;
@@ -27,206 +27,215 @@
import jakarta.json.stream.JsonGeneratorFactory;
public class PropertyGroupTest {
- private static final JsonBuilderFactory BUILDER_FACTORY = Json.createBuilderFactory(null);
- private static JsonObject obj = null;
-
- private static boolean DEBUG = true;
-
- @BeforeClass
- public static void setup() {
- // Build a JSON object for testing.
- obj = BUILDER_FACTORY.createObjectBuilder()
- .add("level1", BUILDER_FACTORY.createObjectBuilder()
- .add("level2", BUILDER_FACTORY.createObjectBuilder()
- .add("scalars", BUILDER_FACTORY.createObjectBuilder()
- .add("stringProp", "stringValue")
- .add("intProp", 123)
- .add("booleanProp", true)
- .add("booleanProp-2", "true"))
- .add("arrays", BUILDER_FACTORY.createObjectBuilder()
- .add("int-array", BUILDER_FACTORY.createArrayBuilder()
- .add(1)
- .add(2)
- .add(3))
- .add("string-array", BUILDER_FACTORY.createArrayBuilder()
- .add("one")
- .add("two"))
- .add("object-array", BUILDER_FACTORY.createArrayBuilder()
- .add(BUILDER_FACTORY.createObjectBuilder()
- .add("attr1", "val1"))
- .add(BUILDER_FACTORY.createObjectBuilder()
- .add("attr2", "val2"))))
- .add("nulls", BUILDER_FACTORY.createObjectBuilder()
- .add("nullProp", JsonValue.NULL)
- .add("nullArrayProp", BUILDER_FACTORY.createArrayBuilder()
- .add(JsonValue.NULL)))))
- .build();
-
- if (DEBUG) {
- Map config = Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true);
- JsonGeneratorFactory factory = Json.createGeneratorFactory(config);
- JsonGenerator generator = factory.createGenerator(System.out);
- generator.write(obj);
- generator.flush();
- System.out.println();
- }
- }
-
- @Test
- public void testGetPropertyGroup() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
-
- PropertyGroup scalars = pg.getPropertyGroup("level1|level2|scalars");
- assertNotNull(scalars);
- PropertyGroup result = scalars.getPropertyGroup("scalars");
- assertNull(result);
- String value = scalars.getStringProperty("stringProp");
- assertNotNull(value);
- assertEquals("stringValue", value);
- }
-
- @Test
- public void testStringProperty() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- String value = pg.getStringProperty("level1|level2|scalars|stringProp");
- assertNotNull(value);
- assertEquals("stringValue", value);
- }
-
- @Test
- public void testIntProperty() {
- PropertyGroup pg = new PropertyGroup(obj);
- Integer value = pg.getIntProperty("level1|level2|scalars|intProp");
- assertNotNull(value);
- assertEquals(123, value.intValue());
- }
-
- @Test
- public void testBooleanProperty() {
- PropertyGroup pg = new PropertyGroup(obj);
- Boolean value = pg.getBooleanProperty("level1|level2|scalars|booleanProp");
- assertNotNull(value);
- assertEquals(Boolean.TRUE, value);
-
- value = pg.getBooleanProperty("level1|level2|scalars|booleanProp-2");
- assertNotNull(value);
- assertEquals(Boolean.TRUE, value);
- }
-
- @Test
- public void testArrayProperty() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- Object[] array = pg.getArrayProperty("level1|level2|arrays|string-array");
- assertNotNull(array);
- assertEquals(2, array.length);
- assertEquals("one", array[0]);
- assertEquals("two", array[1]);
- }
-
- @Test
- public void testStringListProperty() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- List strings = pg.getStringListProperty("level1|level2|arrays|string-array");
- assertNotNull(strings);
- assertEquals(2, strings.size());
- assertEquals("one", strings.get(0));
- assertEquals("two", strings.get(1));
-
- strings = pg.getStringListProperty("level1|level2|arrays|int-array");
- assertNotNull(strings);
- assertEquals(3, strings.size());
- assertEquals("1", strings.get(0));
- assertEquals("2", strings.get(1));
- assertEquals("3", strings.get(2));
- }
-
- @Test
- public void testObjectArrayProperty() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- Object[] array = pg.getArrayProperty("level1|level2|arrays|object-array");
- assertNotNull(array);
- assertEquals(2, array.length);
- if (!(array[0] instanceof PropertyGroup)) {
- fail("array element 0 not a PropertyGroup!");
- }
- if (!(array[1] instanceof PropertyGroup)) {
- fail("array element 1 not a PropertyGroup!");
- }
-
- // Check the first element.
- PropertyGroup pg0 = (PropertyGroup) array[0];
- String val1 = pg0.getStringProperty("attr1");
- assertNotNull(val1);
- assertEquals("val1", val1);
-
- // Check the second element.
- PropertyGroup pg1 = (PropertyGroup) array[1];
- String val2 = pg1.getStringProperty("attr2");
- assertNotNull(val2);
- assertEquals("val2", val2);
- }
-
- @Test
- public void testNullProperty() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- assertNull(pg.getJsonValue("level1|level2|nulls|nullProp"));
- assertNull(pg.getStringProperty("level1|level2|nulls|nullProp"));
- assertNull(pg.getBooleanProperty("level1|level2|nulls|nullProp"));
- assertNull(pg.getIntProperty("level1|level2|nulls|nullProp"));
- assertNull(pg.getDoubleProperty("level1|level2|nulls|nullProp"));
- assertNull(pg.getStringListProperty("level1|level2|nulls|nullProp"));
- }
-
- @Test
- public void testNonExistentProperty() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- assertNull(pg.getJsonValue("bogus"));
- assertNull(pg.getStringProperty("bogus"));
- assertNull(pg.getBooleanProperty("bogus"));
- assertNull(pg.getIntProperty("bogus"));
- assertNull(pg.getDoubleProperty("bogus"));
- assertNull(pg.getStringListProperty("bogus"));
-
- assertNull(pg.getJsonValue("level1|bogus"));
- assertNull(pg.getStringProperty("level1|bogus"));
- assertNull(pg.getBooleanProperty("level1|bogus"));
- assertNull(pg.getIntProperty("level1|bogus"));
- assertNull(pg.getDoubleProperty("level1|bogus"));
- assertNull(pg.getStringListProperty("level1|bogus"));
-
- assertNull(pg.getJsonValue("bogus|bogus"));
- assertNull(pg.getStringProperty("bogus|bogus"));
- assertNull(pg.getBooleanProperty("bogus|bogus"));
- assertNull(pg.getIntProperty("bogus|bogus"));
- assertNull(pg.getDoubleProperty("bogus|bogus"));
- assertNull(pg.getStringListProperty("bogus|bogus"));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testStringPropertyException() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- Object result = pg.getStringProperty("level1|level2|scalars|intProp");
- System.err.println("Unexpected result: " + result);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testIntPropertyException() {
- PropertyGroup pg = new PropertyGroup(obj);
- Object result = pg.getIntProperty("level1|level2|scalars|stringProp");
- System.err.println("Unexpected result: " + result);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testBooleanPropertyException() {
- PropertyGroup pg = new PropertyGroup(obj);
- Object result = pg.getBooleanProperty("level1|level2|scalars|intProp");
- System.err.println("Unexpected result: " + result);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void testArrayPropertyException() throws Exception {
- PropertyGroup pg = new PropertyGroup(obj);
- Object result = pg.getArrayProperty("level1|level2");
- System.err.println("Unexpected result: " + result);
- }
+ private static final JsonBuilderFactory BUILDER_FACTORY = Json.createBuilderFactory(null);
+ private static JsonObject obj = null;
+
+ private static boolean DEBUG = true;
+
+ @BeforeAll
+ public static void setup() {
+ // Build a JSON object for testing.
+ obj = BUILDER_FACTORY.createObjectBuilder()
+ .add("level1", BUILDER_FACTORY.createObjectBuilder()
+ .add("level2", BUILDER_FACTORY.createObjectBuilder()
+ .add("scalars", BUILDER_FACTORY.createObjectBuilder()
+ .add("stringProp", "stringValue")
+ .add("intProp", 123)
+ .add("booleanProp", true)
+ .add("booleanProp-2", "true"))
+ .add("arrays", BUILDER_FACTORY.createObjectBuilder()
+ .add("int-array", BUILDER_FACTORY.createArrayBuilder()
+ .add(1)
+ .add(2)
+ .add(3))
+ .add("string-array", BUILDER_FACTORY.createArrayBuilder()
+ .add("one")
+ .add("two"))
+ .add("object-array", BUILDER_FACTORY.createArrayBuilder()
+ .add(BUILDER_FACTORY.createObjectBuilder()
+ .add("attr1", "val1"))
+ .add(BUILDER_FACTORY.createObjectBuilder()
+ .add("attr2", "val2"))))
+ .add("nulls", BUILDER_FACTORY.createObjectBuilder()
+ .add("nullProp", JsonValue.NULL)
+ .add("nullArrayProp", BUILDER_FACTORY.createArrayBuilder()
+ .add(JsonValue.NULL)))))
+ .build();
+
+ if (DEBUG) {
+ Map config = Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true);
+ JsonGeneratorFactory factory = Json.createGeneratorFactory(config);
+ JsonGenerator generator = factory.createGenerator(System.out);
+ generator.write(obj);
+ generator.flush();
+ System.out.println();
+ }
+ }
+
+ @Test
+ public void testGetPropertyGroup() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+
+ PropertyGroup scalars = pg.getPropertyGroup("level1|level2|scalars");
+ assertNotNull(scalars);
+ PropertyGroup result = scalars.getPropertyGroup("scalars");
+ assertNull(result);
+ String value = scalars.getStringProperty("stringProp");
+ assertNotNull(value);
+ assertEquals("stringValue", value);
+ }
+
+ @Test
+ public void testStringProperty() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+ String value = pg.getStringProperty("level1|level2|scalars|stringProp");
+ assertNotNull(value);
+ assertEquals("stringValue", value);
+ }
+
+ @Test
+ public void testIntProperty() {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Integer value = pg.getIntProperty("level1|level2|scalars|intProp");
+ assertNotNull(value);
+ assertEquals(123, value.intValue());
+ }
+
+ @Test
+ public void testBooleanProperty() {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Boolean value = pg.getBooleanProperty("level1|level2|scalars|booleanProp");
+ assertNotNull(value);
+ assertEquals(Boolean.TRUE, value);
+
+ value = pg.getBooleanProperty("level1|level2|scalars|booleanProp-2");
+ assertNotNull(value);
+ assertEquals(Boolean.TRUE, value);
+ }
+
+ @Test
+ public void testArrayProperty() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Object[] array = pg.getArrayProperty("level1|level2|arrays|string-array");
+ assertNotNull(array);
+ assertEquals(2, array.length);
+ assertEquals("one", array[0]);
+ assertEquals("two", array[1]);
+ }
+
+ @Test
+ public void testStringListProperty() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+ List strings = pg.getStringListProperty("level1|level2|arrays|string-array");
+ assertNotNull(strings);
+ assertEquals(2, strings.size());
+ assertEquals("one", strings.get(0));
+ assertEquals("two", strings.get(1));
+
+ strings = pg.getStringListProperty("level1|level2|arrays|int-array");
+ assertNotNull(strings);
+ assertEquals(3, strings.size());
+ assertEquals("1", strings.get(0));
+ assertEquals("2", strings.get(1));
+ assertEquals("3", strings.get(2));
+ }
+
+ @Test
+ public void testObjectArrayProperty() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Object[] array = pg.getArrayProperty("level1|level2|arrays|object-array");
+ assertNotNull(array);
+ assertEquals(2, array.length);
+ if (!(array[0] instanceof PropertyGroup)) {
+ fail("array element 0 not a PropertyGroup!");
+ }
+ if (!(array[1] instanceof PropertyGroup)) {
+ fail("array element 1 not a PropertyGroup!");
+ }
+
+ // Check the first element.
+ PropertyGroup pg0 = (PropertyGroup) array[0];
+ String val1 = pg0.getStringProperty("attr1");
+ assertNotNull(val1);
+ assertEquals("val1", val1);
+
+ // Check the second element.
+ PropertyGroup pg1 = (PropertyGroup) array[1];
+ String val2 = pg1.getStringProperty("attr2");
+ assertNotNull(val2);
+ assertEquals("val2", val2);
+ }
+
+ @Test
+ public void testNullProperty() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+ assertNull(pg.getJsonValue("level1|level2|nulls|nullProp"));
+ assertNull(pg.getStringProperty("level1|level2|nulls|nullProp"));
+ assertNull(pg.getBooleanProperty("level1|level2|nulls|nullProp"));
+ assertNull(pg.getIntProperty("level1|level2|nulls|nullProp"));
+ assertNull(pg.getDoubleProperty("level1|level2|nulls|nullProp"));
+ assertNull(pg.getStringListProperty("level1|level2|nulls|nullProp"));
+ }
+
+ @Test
+ public void testNonExistentProperty() throws Exception {
+ PropertyGroup pg = new PropertyGroup(obj);
+ assertNull(pg.getJsonValue("bogus"));
+ assertNull(pg.getStringProperty("bogus"));
+ assertNull(pg.getBooleanProperty("bogus"));
+ assertNull(pg.getIntProperty("bogus"));
+ assertNull(pg.getDoubleProperty("bogus"));
+ assertNull(pg.getStringListProperty("bogus"));
+
+ assertNull(pg.getJsonValue("level1|bogus"));
+ assertNull(pg.getStringProperty("level1|bogus"));
+ assertNull(pg.getBooleanProperty("level1|bogus"));
+ assertNull(pg.getIntProperty("level1|bogus"));
+ assertNull(pg.getDoubleProperty("level1|bogus"));
+ assertNull(pg.getStringListProperty("level1|bogus"));
+
+ assertNull(pg.getJsonValue("bogus|bogus"));
+ assertNull(pg.getStringProperty("bogus|bogus"));
+ assertNull(pg.getBooleanProperty("bogus|bogus"));
+ assertNull(pg.getIntProperty("bogus|bogus"));
+ assertNull(pg.getDoubleProperty("bogus|bogus"));
+ assertNull(pg.getStringListProperty("bogus|bogus"));
+ }
+
+ @Test
+ public void testStringPropertyException() throws Exception {
+ assertThrows(IllegalArgumentException.class, () -> {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Object result = pg.getStringProperty("level1|level2|scalars|intProp");
+ System.err.println("Unexpected result: " + result);
+ });
+ }
+
+ @Test
+ public void testIntPropertyException() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Object result = pg.getIntProperty("level1|level2|scalars|stringProp");
+ System.err.println("Unexpected result: " + result);
+ });
+ }
+
+ @Test
+ public void testBooleanPropertyException() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ PropertyGroup pg = new PropertyGroup(obj);
+ Object result = pg.getBooleanProperty("level1|level2|scalars|intProp");
+ System.err.println("Unexpected result: " + result);
+ });
+ }
+
+ @Test
+ public void testArrayPropertyException() {
+ assertThrows(IllegalArgumentException.class, () -> {
+
+ PropertyGroup pg = new PropertyGroup(obj);
+ Object result = pg.getArrayProperty("level1|level2");
+ System.err.println("Unexpected result: " + result);
+ });
+ }
}
diff --git a/keycloak-config/src/test/resources/keycloak-config.json b/keycloak-config/src/test/resources/keycloak-config.json
index a60b8ee..fff7b3c 100644
--- a/keycloak-config/src/test/resources/keycloak-config.json
+++ b/keycloak-config/src/test/resources/keycloak-config.json
@@ -1,6 +1,6 @@
{
"keycloak": {
- "serverUrl": "http://localhost:55095/auth",
+ "serverUrl": "http://localhost:55095",
"adminUser": "admin",
"adminPassword": "admin",
"adminClientId": "admin-cli",
diff --git a/keycloak-extensions/pom.xml b/keycloak-extensions/pom.xml
index c024d4c..e40a931 100644
--- a/keycloak-extensions/pom.xml
+++ b/keycloak-extensions/pom.xml
@@ -11,18 +11,6 @@
jar
-
-
- com.ibm.fhir
- fhir-provider
- provided
-
-
- com.ibm.fhir
- fhir-model
- provided
-
org.keycloak
keycloak-server-spi
@@ -44,6 +32,7 @@
com.github.dasniko
testcontainers-keycloak
+ test
io.rest-assured
@@ -73,10 +62,41 @@
com.squareup.okhttp3
mockwebserver
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-structures-r4
+ ${hapi.fhir.version}
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-client
+ ${hapi.fhir.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.3.0
+
+
+ package
+
+ shade
+
+
+
+
+
maven-dependency-plugin
3.3.0
@@ -88,15 +108,18 @@
copy-dependencies
+
+ org.keycloak
+
- fhir-provider,fhir-config,fhir-model,fhir-core,antlr4-runtime,commons-io,commons-lang3,commons-text,encoder,jakarta.annotation-api,jakarta.json,jcip-annotations
+ hapi-fhir-base, hapi-fhir-client, hapi-fhir-structures-r4, org.hl7.fhir.r4, org.hl7.fhir.utilities, commons-text
compile
-
-
+
+
diff --git a/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java b/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java
index e42aef0..a891075 100644
--- a/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java
+++ b/keycloak-extensions/src/main/java/org/alvearie/keycloak/PatientSelectionForm.java
@@ -2,26 +2,32 @@
(C) Copyright IBM Corp. 2021
SPDX-License-Identifier: Apache-2.0
-*/
+ */
package org.alvearie.keycloak;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import javax.ws.rs.RuntimeType;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.alvearie.keycloak.freemarker.PatientStruct;
+import org.hl7.fhir.r4.model.Bundle;
+import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent;
+import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent;
+import org.hl7.fhir.r4.model.Bundle.BundleType;
+import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
+import org.hl7.fhir.r4.model.HumanName;
+import org.hl7.fhir.r4.model.Patient;
+import org.hl7.fhir.r4.model.Resource;
import org.jboss.logging.Logger;
-import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.util.HttpHeaderNames;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
@@ -43,17 +49,9 @@
import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.sessions.AuthenticationSessionModel;
-import com.ibm.fhir.core.FHIRMediaType;
-import com.ibm.fhir.model.config.FHIRModelConfig;
-import com.ibm.fhir.model.resource.Bundle;
-import com.ibm.fhir.model.resource.Bundle.Entry;
-import com.ibm.fhir.model.resource.Patient;
-import com.ibm.fhir.model.type.Date;
-import com.ibm.fhir.model.type.HumanName;
-import com.ibm.fhir.model.type.Url;
-import com.ibm.fhir.model.type.code.BundleType;
-import com.ibm.fhir.model.type.code.HTTPVerb;
-import com.ibm.fhir.provider.FHIRProvider;
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.rest.client.api.IGenericClient;
+import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
/**
* Present a patient context picker when the client requests the launch/patient scope and the
@@ -62,258 +60,267 @@
*/
public class PatientSelectionForm implements Authenticator {
- private static final Logger LOG = Logger.getLogger(PatientSelectionForm.class);
-
- private static final String SMART_AUDIENCE_PARAM = "client_request_param_aud";
- private static final String SMART_SCOPE_PATIENT_READ = "patient/Patient.read";
- private static final String SMART_SCOPE_LAUNCH_PATIENT = "launch/patient";
-
- private static final String ATTRIBUTE_RESOURCE_ID = "resourceId";
-
- private Client fhirClient;
-
- public PatientSelectionForm() {
- FHIRModelConfig.setExtendedCodeableConceptValidation(false);
- fhirClient = ResteasyClientBuilder.newClient()
- .register(new FHIRProvider(RuntimeType.CLIENT));
- }
-
- @Override
- public void authenticate(AuthenticationFlowContext context) {
- AuthenticationSessionModel authSession = context.getAuthenticationSession();
- ClientModel client = authSession.getClient();
-
- String requestedScopesString = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
- Stream clientScopes = TokenManager.getRequestedClientScopes(requestedScopesString, client);
-
- if (clientScopes.noneMatch(s -> SMART_SCOPE_LAUNCH_PATIENT.equals(s.getName()))) {
- // no launch/patient scope == no-op
- context.success();
- return;
- }
-
- if (context.getUser() == null) {
- fail(context, "Expected a user but found null");
- return;
- }
-
- List resourceIds = getResourceIdsForUser(context);
- if (resourceIds.size() == 0) {
- fail(context, "Expected user to have one or more resourceId attributes, but found none");
- return;
- }
- if (resourceIds.size() == 1) {
- succeed(context, resourceIds.get(0));
- return;
- }
-
- AuthenticatorConfigModel config = context.getAuthenticatorConfig();
- if (config == null || !config.getConfig().containsKey(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME)) {
- fail(context, "The Patient Selection Authenticator must be configured with a valid FHIR base URL");
- return;
- }
-
- String accessToken = buildInternalAccessToken(context, resourceIds);
-
- Bundle requestBundle = buildRequestBundle(resourceIds);
- try (Response fhirResponse = fhirClient
- .target(config.getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME))
- .request(MediaType.APPLICATION_JSON)
- .header(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken)
- .post(Entity.entity(requestBundle, FHIRMediaType.APPLICATION_FHIR_JSON_TYPE))) {
-
- if (fhirResponse.getStatus() != 200) {
- String msg = "Error while retrieving Patient resources for the selection form";
- LOG.warnf(msg);
- LOG.warnf("Response with code " + fhirResponse.getStatus() + "%n%s", fhirResponse.readEntity(String.class));
- context.failure(AuthenticationFlowError.INTERNAL_ERROR,
- Response.status(302)
- .header("Location", context.getAuthenticationSession().getRedirectUri() +
- "?error=server_error" +
- "&error_description=" + msg)
- .build());
- return;
- }
-
- List patients = gatherPatientInfo(fhirResponse.readEntity(Bundle.class));
- if (patients.isEmpty()) {
- succeed(context, resourceIds.get(0));
- return;
- }
-
- if (patients.size() == 1) {
- succeed(context, patients.get(0).getId());
- } else {
- Response response = context.form()
- .setAttribute("patients", patients)
- .createForm("patient-select-form.ftl");
-
- context.challenge(response);
- }
- }
- }
-
- private List getResourceIdsForUser(AuthenticationFlowContext context) {
- return context.getUser().getAttributeStream(ATTRIBUTE_RESOURCE_ID)
- .flatMap(a -> Arrays.stream(a.split(" ")))
- .map(String::trim)
- .filter(s -> !s.isEmpty())
- .collect(Collectors.toList());
- }
-
- private String buildInternalAccessToken(AuthenticationFlowContext context, List resourceIds) {
- KeycloakSession session = context.getSession();
- AuthenticationSessionModel authSession = context.getAuthenticationSession();
- UserModel user = context.getUser();
- ClientModel client = authSession.getClient();
-
- UserSessionModel userSession = session.sessions().createUserSession(context.getRealm(), user, user.getUsername(),
- context.getConnection().getRemoteAddr(), null, false, null, null);
-
- AuthenticatedClientSessionModel authedClientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
- if (authedClientSession == null) {
- authedClientSession = session.sessions().createClientSession(context.getRealm(), client, userSession);
- }
- authedClientSession.setNote(OIDCLoginProtocol.ISSUER,
- Urls.realmIssuer(session.getContext().getUri().getBaseUri(), context.getRealm().getName()));
-
- // Note: this depends on the corresponding string being registered as a valid scope for this client
- ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(authedClientSession,
- SMART_SCOPE_PATIENT_READ, session);
-
- String requestedAudience = authSession.getClientNote(SMART_AUDIENCE_PARAM);
- if (requestedAudience == null) {
- String internalFhirUrl = context.getAuthenticatorConfig().getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME);
- LOG.info("Client request is missing the 'aud' parameter, using '" + internalFhirUrl + "' from config.");
- requestedAudience = internalFhirUrl;
- }
-
- // Explicit decision not to check the requested audience against the configured internal FHIR URL
- // Checking of the requested audience should be performed in a previous step by the AudienceValidator
- TokenManager tokenManager = new TokenManager();
- AccessToken accessToken = tokenManager.createClientAccessToken(session, context.getRealm(), authSession.getClient(),
- context.getUser(), userSession, clientSessionCtx);
-
- // Explicitly override the scope string with what we need (less brittle than depending on this to exist as a client scope)
- accessToken.setScope(SMART_SCOPE_PATIENT_READ);
-
- JsonWebToken jwt = accessToken.audience(requestedAudience);
- jwt.setOtherClaims("patient_id", resourceIds);
- return session.tokens().encode(jwt);
- }
-
- private Bundle buildRequestBundle(List resourceIds) {
- Bundle.Builder requestBuilder = Bundle.builder()
- .type(BundleType.BATCH);
- resourceIds.stream()
- .map(id -> Entry.Request.builder()
- .method(HTTPVerb.GET)
- .url(Url.of("Patient/" + id))
- .build())
- .map(request -> Entry.builder()
- .request(request)
- .build())
- .forEach(entry -> requestBuilder.entry(entry));
- return requestBuilder.build();
- }
-
- private void fail(AuthenticationFlowContext context, String msg) {
- LOG.warn(msg);
- context.failure(AuthenticationFlowError.INTERNAL_ERROR,
- Response.status(302)
- .header("Location", context.getAuthenticationSession().getRedirectUri() +
- "?error=server_error" +
- "&error_description=" + msg)
- .build());
- }
-
- private void succeed(AuthenticationFlowContext context, String patient) {
- // Add selected information to authentication session
- context.getAuthenticationSession().setUserSessionNote("patient_id", patient);
- context.success();
- }
-
- private List gatherPatientInfo(Bundle fhirResponse) {
- List patients = new ArrayList<>();
-
- for (Entry entry : fhirResponse.getEntry()) {
- if (entry.getResponse() == null || !entry.getResponse().getStatus().hasValue() ||
- !entry.getResponse().getStatus().getValue().startsWith("200")) {
- continue;
- }
-
- Patient patient = entry.getResource().as(Patient.class);
-
- String patientId = patient.getId();
-
- String patientName = "Missing Name";
- if (patient.getName().isEmpty()) {
- LOG.warn("Patient[id=" + patient.getId() + "] has no name; using placeholder");
- } else {
- if (patient.getName().size() > 1) {
- LOG.warn("Patient[id=" + patient.getId() + "] has multiple names; using the first one");
- }
- patientName = constructSimpleName(patient.getName().get(0));
- }
-
- String patientDOB = patient.getBirthDate() == null ? "missing"
- : Date.PARSER_FORMATTER.format(patient.getBirthDate().getValue());
-
- patients.add(new PatientStruct(patientId, patientName, patientDOB));
- }
-
- return patients;
- }
-
- private String constructSimpleName(HumanName name) {
- if (name.getText() != null && name.getText().hasValue()) {
- return name.getText().getValue();
- }
-
- return Stream.concat(name.getGiven().stream(), Stream.of(name.getFamily()))
- .map(n -> n.getValue())
- .filter(Objects::nonNull)
- .collect(Collectors.joining(" "));
- }
-
- @Override
- public boolean requiresUser() {
- return true;
- }
-
- @Override
- public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- return true;
- }
-
- @Override
- public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
- }
-
- @Override
- public void action(AuthenticationFlowContext context) {
-
- MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
- String patient = formData.getFirst("patient");
-
- LOG.debugf("The user selected patient '%s'", patient);
-
- if (patient == null || patient.trim().isEmpty() || !getResourceIdsForUser(context).contains(patient.trim())) {
- LOG.warnf("The patient selection '%s' is not valid for the authenticated user.", patient.trim());
- context.cancelLogin();
-
- // reauthenticate...
- authenticate(context);
- return;
- }
-
- succeed(context, patient.trim());
- }
-
- @Override
- public void close() {
- if (fhirClient != null) {
- fhirClient.close();
- }
- }
+ private static final Logger LOG = Logger.getLogger(PatientSelectionForm.class);
+
+ private static final String SMART_AUDIENCE_PARAM = "client_request_param_aud";
+
+ private static final String SMART_SCOPE_PATIENT_READ = "patient/Patient.read";
+ private static final String SMART_SCOPE_LAUNCH_PATIENT = "launch/patient";
+
+ private static final String ATTRIBUTE_RESOURCE_ID = "resourceId";
+
+
+ // creating the fhirContext is expensive, you only want to create it once
+ private static final FhirContext fhirCtx = FhirContext.forR4();
+ static {
+ // turn off server validation (capability statement pre-checks)
+ fhirCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
+ }
+
+ public PatientSelectionForm() {
+
+ }
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ ClientModel client = authSession.getClient();
+
+ String requestedScopesString = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
+ Stream clientScopes = TokenManager.getRequestedClientScopes(requestedScopesString, client);
+
+ if (clientScopes.noneMatch(s -> SMART_SCOPE_LAUNCH_PATIENT.equals(s.getName()))) {
+ // no launch/patient scope == no-op
+ context.success();
+ return;
+ }
+
+ if (context.getUser() == null) {
+ fail(context, "Expected a user but found null");
+ return;
+ }
+
+ List resourceIds = getResourceIdsForUser(context);
+ if (resourceIds.size() == 0) {
+ fail(context, "Expected user to have one or more resourceId attributes, but found none");
+ return;
+ }
+ if (resourceIds.size() == 1) {
+ succeed(context, resourceIds.get(0));
+ return;
+ }
+
+ AuthenticatorConfigModel config = context.getAuthenticatorConfig();
+ if (config == null || !config.getConfig().containsKey(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME)) {
+ fail(context, "The Patient Selection Authenticator must be configured with a valid FHIR base URL");
+ return;
+ }
+
+ String accessToken = buildInternalAccessToken(context, resourceIds);
+
+ Bundle requestBundle = buildRequestBundle(resourceIds);
+
+ String fhirBaseUrl = config.getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME);
+ IGenericClient hapiClient = fhirCtx.newRestfulGenericClient(fhirBaseUrl);
+
+ try {
+ Bundle returnBundle = hapiClient.transaction().withBundle(requestBundle)
+ .withAdditionalHeader(HttpHeaderNames.AUTHORIZATION, "Bearer " + accessToken)
+ .execute();
+
+ List patients = gatherPatientInfo(returnBundle);
+ if (patients.isEmpty()) {
+ succeed(context, resourceIds.get(0));
+ return;
+ }
+
+ if (patients.size() == 1) {
+ succeed(context, patients.get(0).getId());
+ } else {
+ Response response = context.form()
+ .setAttribute("patients", patients)
+ .createForm("patient-select-form.ftl");
+
+ context.challenge(response);
+ }
+ } catch (Exception ex) {
+ String msg = "Error while retrieving Patient resources for the selection form";
+ LOG.warn(msg);
+ LOG.warn("Exception caught: " + ex);
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR,
+ Response.status(302).header("Location", context.getAuthenticationSession().getRedirectUri()
+ + "?error=server_error" + "&error_description=" + msg).build());
+ return;
+ }
+ }
+
+ private List getResourceIdsForUser(AuthenticationFlowContext context) {
+ return context.getUser().getAttributeStream(ATTRIBUTE_RESOURCE_ID)
+ .flatMap(a -> Arrays.stream(a.split(" ")))
+ .map(String::trim)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toList());
+ }
+
+ private String buildInternalAccessToken(AuthenticationFlowContext context, List resourceIds) {
+ KeycloakSession session = context.getSession();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+ UserModel user = context.getUser();
+ ClientModel client = authSession.getClient();
+
+ UserSessionModel userSession = session.sessions().createUserSession(context.getRealm(), user, user.getUsername(),
+ context.getConnection().getRemoteAddr(), null, false, null, null);
+
+ AuthenticatedClientSessionModel authedClientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
+ if (authedClientSession == null) {
+ authedClientSession = session.sessions().createClientSession(context.getRealm(), client, userSession);
+ }
+ authedClientSession.setNote(OIDCLoginProtocol.ISSUER,
+ Urls.realmIssuer(session.getContext().getUri().getBaseUri(), context.getRealm().getName()));
+
+ // Note: this depends on the corresponding string being registered as a valid scope for this client
+ ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(authedClientSession,
+ SMART_SCOPE_PATIENT_READ, session);
+
+ String requestedAudience = authSession.getClientNote(SMART_AUDIENCE_PARAM);
+ if (requestedAudience == null) {
+ String internalFhirUrl = context.getAuthenticatorConfig().getConfig().get(PatientSelectionFormFactory.INTERNAL_FHIR_URL_PROP_NAME);
+ LOG.info("Client request is missing the 'aud' parameter, using '" + internalFhirUrl + "' from config.");
+ requestedAudience = internalFhirUrl;
+ }
+
+ // Explicit decision not to check the requested audience against the configured internal FHIR URL
+ // Checking of the requested audience should be performed in a previous step by the AudienceValidator
+ TokenManager tokenManager = new TokenManager();
+ AccessToken accessToken = tokenManager.createClientAccessToken(session, context.getRealm(), authSession.getClient(),
+ context.getUser(), userSession, clientSessionCtx);
+
+ // Explicitly override the scope string with what we need (less brittle than depending on this to exist as a client scope)
+ accessToken.setScope(SMART_SCOPE_PATIENT_READ);
+
+ JsonWebToken jwt = accessToken.audience(requestedAudience);
+ jwt.setOtherClaims("patient_id", resourceIds);
+ return session.tokens().encode(jwt);
+ }
+
+ private Bundle buildRequestBundle(List resourceIds) {
+
+ Bundle searchBundle = new Bundle();
+ searchBundle.setType(BundleType.BATCH);
+
+ for (String id : resourceIds) {
+ BundleEntryComponent bec = searchBundle.addEntry();
+ BundleEntryRequestComponent request = new BundleEntryRequestComponent();
+ request.setMethod(HTTPVerb.GET);
+ request.setUrl("Patient/" + id);
+ bec.setRequest(request);
+ }
+ return searchBundle;
+ }
+
+ private void fail(AuthenticationFlowContext context, String msg) {
+ LOG.warn(msg);
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR,
+ Response.status(302)
+ .header("Location", context.getAuthenticationSession().getRedirectUri() +
+ "?error=server_error" +
+ "&error_description=" + msg)
+ .build());
+ }
+
+ private void succeed(AuthenticationFlowContext context, String patient) {
+ // Add selected information to authentication session
+ context.getAuthenticationSession().setUserSessionNote("patient_id", patient);
+ context.success();
+ }
+
+ private List gatherPatientInfo(Bundle fhirResponse) {
+ List patients = new ArrayList<>();
+
+ for (BundleEntryComponent entry : fhirResponse.getEntry()) {
+ String status = entry.getResponse() == null ? null : entry.getResponse().getStatus();
+ if (status == null || status.isBlank() || !status.startsWith("200")) {
+ continue;
+ }
+
+ Resource resource = entry.getResource();
+ if (!(resource instanceof Patient)) {
+ continue;
+ }
+
+ Patient patient = (Patient) resource;
+ String patientId = patient.getIdElement().getIdPart();
+
+ String patientName = "Missing Name";
+ if (patient.getName().isEmpty()) {
+ LOG.warn("Patient[id=" + patient.getId() + "] has no name; using placeholder");
+ } else {
+ if (patient.getName().size() > 1) {
+ LOG.warn("Patient[id=" + patient.getId() + "] has multiple names; using the first one");
+ }
+ patientName = constructSimpleName(patient.getName().get(0));
+ }
+
+ String patientDOB = "missing";
+ if (patient.getBirthDate() != null) {
+ LocalDate ld = patient.getBirthDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+ patientDOB = ld.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM));
+ }
+
+ LOG.debugf("Adding patient to return struct %s, %s", patientId, patientName);
+ patients.add(new PatientStruct(patientId, patientName, patientDOB));
+ }
+
+ return patients;
+ }
+
+ private String constructSimpleName(HumanName name) {
+ if (name == null) {
+ return null;
+ }
+ String firstNames = name.getGivenAsSingleString();
+ String lastName = name.getFamily();
+ firstNames = firstNames == null ? "" : firstNames;
+ lastName = lastName == null ? "" : lastName;
+ return (firstNames + lastName).trim();
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return true;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+
+ MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
+ String patient = formData.getFirst("patient");
+
+ LOG.debugf("The user selected patient '%s'", patient);
+
+ if (patient == null || patient.trim().isEmpty() || !getResourceIdsForUser(context).contains(patient.trim())) {
+ LOG.warnf("The patient selection '%s' is not valid for the authenticated user.", patient);
+ context.cancelLogin();
+
+ // reauthenticate...
+ authenticate(context);
+ return;
+ }
+
+ succeed(context, patient.trim());
+ }
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
}
diff --git a/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java b/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java
index 745f9fa..6fb7571 100644
--- a/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java
+++ b/keycloak-extensions/src/test/java/org/alvearie/keycloak/KeycloakContainerTest.java
@@ -2,11 +2,12 @@
(C) Copyright IBM Corp. 2021
SPDX-License-Identifier: Apache-2.0
-*/
+ */
package org.alvearie.keycloak;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -16,17 +17,16 @@
import org.alvearie.keycloak.config.KeycloakConfigurator;
import org.alvearie.keycloak.config.util.KeycloakConfig;
+
import org.alvearie.utils.SeleniumOauthInteraction;
import org.apache.commons.io.IOUtils;
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.mockito.Mockito;
import org.testcontainers.Testcontainers;
-import org.testcontainers.containers.BindMode;
-
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -37,121 +37,121 @@
import okhttp3.mockwebserver.RecordedRequest;
public class KeycloakContainerTest {
- private static final String MASTER_REALM = "master";
- private static final String ADMIN_CLIENT_ID = "admin-cli";
- private static final String USERNAME = "a";
- private static final String PASSWORD = "a";
- private static final String KC_CLIENT = "test";
- private static final String REDIRECT_URI = "http://localhost";
- private static final String AUTH_ENDPOINT = "/auth/realms/test/protocol/openid-connect/auth";
- private static final String TOKEN_ENDPOINT = "/auth/realms/test/protocol/openid-connect/token";
- private static final String AUDIENCE = "https://localhost:9443/fhir-server/api/v4";
-
- // per https://www.testcontainers.org/features/networking/#exposing-host-ports-to-the-container
- // the host's port should be known prior to starting the container
- private static MockWebServer mockFhirServer;
- static {
- mockFhirServer = new MockWebServer();
- setupFHIRPortEnvVar(mockFhirServer.getPort());
- Testcontainers.exposeHostPorts(mockFhirServer.getPort());
- }
-
- // per the testcontainers doc, the contain should be started in a static block before JUnit starts up
- private static KeycloakContainer keycloak;
- static {
- keycloak = new KeycloakContainer().withExtensionClassesFrom("target/classes");
- keycloak.addFileSystemBind("target/dependency", "/opt/jboss/keycloak/modules/system/layers/base/com/ibm/fhir/main", BindMode.READ_ONLY);
- // Shouldn't be needed, but sometimes is: https://github.com/dasniko/testcontainers-keycloak/issues/15
- keycloak.withEnv("DB_VENDOR", "H2");
- // Uncomment this to keep the container running after the tests complete
-// keycloak.withReuse(true);
- keycloak.start();
- }
-
- private static Keycloak adminClient;
-
- private static void setupFHIRPortEnvVar(int port) {
- Map env = new HashMap<>(KeycloakConfig.EnvironmentVariables.get());
- env.put("FHIR_PORT", Integer.toString(port));
- Mockito.mockStatic(KeycloakConfig.EnvironmentVariables.class);
- Mockito.when(KeycloakConfig.EnvironmentVariables.get()).thenReturn(env);
- }
-
- @BeforeClass
- public static void setUp() throws Exception {
- mockFhirServer.enqueue(new MockResponse()
- .setHeader("Content-Type", "application/fhir+json")
- .setBody(IOUtils.resourceToString("/mock_fhir_response.json", Charset.forName("UTF-8")))
- );
-
- adminClient = KeycloakBuilder.builder()
- .serverUrl(keycloak.getAuthServerUrl())
- .realm(MASTER_REALM)
- .username(keycloak.getAdminUsername())
- .password(keycloak.getAdminPassword())
- .clientId(ADMIN_CLIENT_ID)
- .build();
- KeycloakConfigurator configurator = new KeycloakConfigurator(adminClient);
-
- KeycloakConfig config = new KeycloakConfig("keycloak-config.json");
- configurator.initializeRealm("test", config.getPropertyGroup("test"));
- }
-
- @AfterClass
- public static void tearDown() throws IOException {
- mockFhirServer.shutdown();
-
- System.out.println("\n\n" + "Dumping container logs:" + "\n");
- System.out.println(keycloak.getLogs());
-
- // no explicit container cleanup to enable re-use
- //keycloak.close();
- }
-
- @Test
- public void testLogin() throws Exception {
- String host = "http://" + keycloak.getHost();
- Integer port = keycloak.getHttpPort();
- SeleniumOauthInteraction s = new SeleniumOauthInteraction(KC_CLIENT, REDIRECT_URI,
- host + ":" + port + AUTH_ENDPOINT, host + ":" + port + TOKEN_ENDPOINT);
-
- Map authResponse = s.fetchCode(USERNAME, PASSWORD, AUDIENCE, "fhirUser", "launch/patient");
-
- System.out.println("Auth response: " + authResponse);
- assertTrue(authResponse.containsKey("code"));
-
- // verify that the underlying request from Keycloak to the mock FHIR server looks like we want
- RecordedRequest recordedRequest = mockFhirServer.takeRequest();
- assertEquals("POST", recordedRequest.getMethod());
- assertEquals("/fhir-server/api/v4", recordedRequest.getPath());
- String auth = recordedRequest.getHeader("Authorization");
- assertTrue(auth.startsWith("Bearer "));
- verifyToken(auth.substring("Bearer ".length()));
- String request = recordedRequest.getBody().readUtf8();
- Map,?> json = new ObjectMapper().readValue(request, HashMap.class);
- assertEquals("Bundle", json.get("resourceType"));
-
-
- Map tokenResponse = s.fetchToken(authResponse.get("code"));
-
- System.out.println("Token response: " + tokenResponse);
- assertTrue(tokenResponse.containsKey("access_token"));
- verifyToken(tokenResponse.get("access_token"));
-
- assertTrue(tokenResponse.containsKey("refresh_token"));
- String[] refreshTokenParts = tokenResponse.get("access_token").split("\\.");
- assertEquals(3, refreshTokenParts.length);
-
- assertTrue(tokenResponse.containsKey("patient"));
- assertEquals("PatientA", tokenResponse.get("patient"));
- }
-
- private void verifyToken(String accessToken)
- throws IOException, JsonParseException, JsonMappingException {
- String[] accessTokenParts = accessToken.split("\\.");
- assertEquals(3, accessTokenParts.length);
- Map,?> claims = new ObjectMapper().readValue(Base64.getDecoder().decode(accessTokenParts[1]), HashMap.class);
- assertTrue(claims.containsKey("patient_id"));
- System.out.println("patient_id claim: " + claims.get("patient_id"));
- }
+ private static final String MASTER_REALM = "master";
+ private static final String ADMIN_CLIENT_ID = "admin-cli";
+ private static final String USERNAME = "a";
+ private static final String PASSWORD = "a";
+ private static final String KC_CLIENT = "test";
+ private static final String REDIRECT_URI = "http://localhost";
+ private static final String AUTH_ENDPOINT = "/realms/test/protocol/openid-connect/auth";
+ private static final String TOKEN_ENDPOINT = "/realms/test/protocol/openid-connect/token";
+ private static final String AUDIENCE = "https://localhost:9443/fhir-server/api/v4";
+
+
+ // per https://www.testcontainers.org/features/networking/#exposing-host-ports-to-the-container
+ // the host's port should be known prior to starting the container
+ private static MockWebServer mockFhirServer;
+ static {
+ mockFhirServer = new MockWebServer();
+ setupFHIRPortEnvVar(mockFhirServer.getPort());
+ Testcontainers.exposeHostPorts(mockFhirServer.getPort());
+ }
+
+ // per the testcontainers doc, the contain should be started in a static block before JUnit starts up
+ private static KeycloakContainer keycloak;
+ static {
+ keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:18.0.0").withProviderClassesFrom("target/classes")
+ .withFileSystemBind("target/dependency", "/opt/keycloak/providers/");
+ keycloak.withEnv("DB_VENDOR", "H2");
+ // Uncomment this to keep the container running after the tests complete
+ // keycloak.withReuse(true);
+ keycloak.start();
+ }
+
+ private static Keycloak adminClient;
+
+ private static void setupFHIRPortEnvVar(int port) {
+ Map env = new HashMap<>(KeycloakConfig.EnvironmentVariables.get());
+ env.put("FHIR_PORT", Integer.toString(port));
+ Mockito.mockStatic(KeycloakConfig.EnvironmentVariables.class);
+ Mockito.when(KeycloakConfig.EnvironmentVariables.get()).thenReturn(env);
+ }
+
+ @BeforeAll
+ public static void setUp() throws Exception {
+ mockFhirServer.enqueue(new MockResponse()
+ .setHeader("Content-Type", "application/fhir+json")
+ .setBody(IOUtils.resourceToString("/mock_fhir_response.json", Charset.forName("UTF-8")))
+ );
+
+ adminClient = KeycloakBuilder.builder()
+ .serverUrl(keycloak.getAuthServerUrl())
+ .realm(MASTER_REALM)
+ .username(keycloak.getAdminUsername())
+ .password(keycloak.getAdminPassword())
+ .clientId(ADMIN_CLIENT_ID)
+ .build();
+ KeycloakConfigurator configurator = new KeycloakConfigurator(adminClient);
+
+ KeycloakConfig config = new KeycloakConfig("keycloak-config.json");
+ configurator.initializeRealm("test", config.getPropertyGroup("test"));
+ }
+
+ @AfterAll
+ public static void tearDown() throws IOException {
+ mockFhirServer.shutdown();
+
+ System.out.println("\n\n" + "Dumping container logs:" + "\n");
+ System.out.println(keycloak.getLogs());
+
+ // no explicit container cleanup to enable re-use
+ //keycloak.close();
+ }
+
+ @Test
+ public void testLogin() throws Exception {
+ String host = "http://" + keycloak.getHost();
+ Integer port = keycloak.getHttpPort();
+ SeleniumOauthInteraction s = new SeleniumOauthInteraction(KC_CLIENT, REDIRECT_URI,
+ host + ":" + port + AUTH_ENDPOINT, host + ":" + port + TOKEN_ENDPOINT);
+
+ Map authResponse = s.fetchCode(USERNAME, PASSWORD, AUDIENCE, "fhirUser", "launch/patient");
+
+ System.out.println("Auth response: " + authResponse);
+ assertTrue(authResponse.containsKey("code"));
+
+ // verify that the underlying request from Keycloak to the mock FHIR server looks like we want
+ RecordedRequest recordedRequest = mockFhirServer.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals("/fhir-server/api/v4", recordedRequest.getPath());
+ String auth = recordedRequest.getHeader("Authorization");
+ assertTrue(auth.startsWith("Bearer "));
+ verifyToken(auth.substring("Bearer ".length()));
+ String request = recordedRequest.getBody().readUtf8();
+ Map,?> json = new ObjectMapper().readValue(request, HashMap.class);
+ assertEquals("Bundle", json.get("resourceType"));
+
+
+ Map tokenResponse = s.fetchToken(authResponse.get("code"));
+
+ System.out.println("Token response: " + tokenResponse);
+ assertTrue(tokenResponse.containsKey("access_token"));
+ verifyToken(tokenResponse.get("access_token"));
+
+ assertTrue(tokenResponse.containsKey("refresh_token"));
+ String[] refreshTokenParts = tokenResponse.get("access_token").split("\\.");
+ assertEquals(3, refreshTokenParts.length);
+
+ assertTrue(tokenResponse.containsKey("patient"));
+ assertEquals("PatientA", tokenResponse.get("patient"));
+ }
+
+ private void verifyToken(String accessToken)
+ throws IOException, JsonParseException, JsonMappingException {
+ String[] accessTokenParts = accessToken.split("\\.");
+ assertEquals(3, accessTokenParts.length);
+ Map,?> claims = new ObjectMapper().readValue(Base64.getDecoder().decode(accessTokenParts[1]), HashMap.class);
+ assertTrue(claims.containsKey("patient_id"));
+ System.out.println("patient_id claim: " + claims.get("patient_id"));
+ }
}
diff --git a/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java b/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java
index b689118..fb41094 100644
--- a/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java
+++ b/keycloak-extensions/src/test/java/org/alvearie/utils/SeleniumOauthInteraction.java
@@ -39,223 +39,223 @@
public class SeleniumOauthInteraction {
- private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumOauthInteraction.class);
-
- private String appClientId;
- private String appRedirectUri;
- private String oauthAuthUrl;
- private String oauthTokenUrl;
-
- private WebDriver driver;
-
- public SeleniumOauthInteraction(String appclient_id, String appredirect_uri,
- String oauth_auth_url, String oauth_token_url){
- appClientId = appclient_id;
- appRedirectUri = appredirect_uri;
- oauthAuthUrl = oauth_auth_url;
- oauthTokenUrl = oauth_token_url;
-
- WebDriverManager.chromedriver().setup();
- ChromeOptions options = new ChromeOptions();
- options.setHeadless(true);
- driver = new ChromeDriver(options);
- }
-
- /**
- * Hits the configured auth url with the configured client_id and redirect_uri, as well as
- * the passed audience and scopes, then tests the login forms with the passed username and password via
- * Selenium WebDriver.
- *
- * @param user
- * @param pass
- * @param aud
- * @param scope one or more requested scopes
- * @return a map of key-value pairs from the query string of the redirect location on the auth response
- * @throws Exception
- */
- public Map fetchCode(String user, String pass, String aud, String... scope) throws Exception {
- Map response = new HashMap();
-
- try {
- BasicNameValuePair[] params = new BasicNameValuePair[] {
- new BasicNameValuePair("response_type", "code"),
- new BasicNameValuePair("state", UUID.randomUUID().toString()),
- new BasicNameValuePair("client_id", appClientId),
- new BasicNameValuePair("redirect_uri", appRedirectUri),
- new BasicNameValuePair("aud", aud),
- new BasicNameValuePair("scope", String.join(" ", scope)),
- };
- String queryString = URLEncodedUtils.format(Arrays.asList(params), UTF_8);
-
- // launch Firefox and direct it to the Base URL
- driver.get(oauthAuthUrl + "?" + queryString);
-
- WebElement dynamicElement = (new WebDriverWait(driver, 10))
- .until(ExpectedConditions.presenceOfElementLocated(By.id("username")));
- dynamicElement.sendKeys(user);
-
- driver.findElement(By.id("password")).sendKeys(pass);
- driver.findElement(By.id("kc-login")).click();
-
- Boolean loginButtonDisappeared = (new WebDriverWait(driver, 5, 200))
- .until(ExpectedConditions.invisibilityOfElementLocated(By.id("kc-login")));
- LOGGER.debug("Login button is visible?? " + !loginButtonDisappeared);
-
- // At this point we'll either find:
- // A) patient selection form
- // B) consent grant form
- // C) URL with 'code=' in the query string
- // D) 'Update Account Information' - user created with sign_up API.
-
- // Wait up to 3 seconds for screen showing 'Select patient'
- // Page contents:
- // div class=login-pf-page
- // div class=card-pf
- // header.div kc-username
- // div kc-content
- // form id=patient-selection
- // input id= (one per patient that the user has access to)
- // input id=submit
- try {
- // wait up to 3 seconds - poll for element every 200 ms
- new WebDriverWait(driver, 3, 200)
- .until(ExpectedConditions.presenceOfElementLocated(By.id("patient-selection")));
-
- // simulate choosing the patient that has an id of "PatientA"
- driver.findElement(By.id("PatientA")).click();
- driver.findElement(By.id("submit")).click();
-
- } catch ( TimeoutException e ) {
- LOGGER.error("Expected the patient selection form but didn't find it", e);
- fail("Expected the patient selection form but didn't find it");
- }
-
- // wait up to 2 seconds for screen showing YES/NO page for first-time users.
- // Page contents:
- // kc-page-title=Grant Access to inferno
- // li - permissions granted
- // input id=kc-cancel NO
- // input kd=kc-login YES
- try {
- // either the button is found or a TimeoutException is generated
- WebElement grantAccessButton = (new WebDriverWait(driver, 1,200))
- .until(ExpectedConditions.presenceOfElementLocated(By.id("kc-login")));
-
- grantAccessButton.click();
- } catch ( TimeoutException e ) {
- // Didn't find YES button with id='kc-login'.
- // Ignore exception; probably not the first sign-on for this user.
- LOGGER.error("Didn't find YES button with id='kc-login' - ignore exception" + e.getMessage());
- }
-
- // poll at 500 ms interval until 'code' is present in URL query parameter list.
- // Usually this loop completes in under 2.5 seconds.
- for ( int i =0; i< 100; i++) {
- Thread.sleep(500);
- response = getQueryMap( driver.getCurrentUrl() );
- if ( response.keySet().contains("code") )
- {
- break;
- }
- }
- } catch (Exception e) {
- if (driver == null || driver.getCurrentUrl() == null) {
- throw e;
- }
- // adding the while loop to avoid the timing issues and reliably
- // extract the grant code from next page after clicking the login
- // button
- // Note: the handling in exception is required because on some
- // operating systems the selenium webdriver exits browser when
- // the recipient host is unable to connect.
- int cnt = 0;
- while (!driver.getCurrentUrl().contains("code=")) {
- try {
- Thread.sleep(500);
- cnt++;
- if (cnt > 10) {
- LOGGER.debug("Waiting for page to retrieve grant code, Round ... " + cnt);
- break;
- }
- } catch (InterruptedException e1) {
- // do nothing
- }
- }
- if (!driver.getCurrentUrl().contains("code=")) {
- LOGGER.error("Something went wrong during the oauth code retreival process", e);
- } else {
- response = getQueryMap(driver.getCurrentUrl());
- }
- } finally {
- if (driver != null) {
- // close Firefox
- driver.close();
- }
- }
- return response;
- }
-
- /**
- * Exchange the code for a token (with default params)
- *
- * @param code
- * @return
- * @throws Exception
- */
- public Map fetchToken(String code) throws Exception {
- Map headers = new HashMap<>();
- headers.put("Content-Type", "application/x-www-form-urlencoded");
-
- Map params = new HashMap<>();
- params.put("grant_type", "authorization_code");
- params.put("code", code);
- params.put("client_id", appClientId);
- params.put("redirect_uri", appRedirectUri);
-
- return fetchTokenWith(params);
- }
-
- /**
- * Invoke the token endpoint with the passed params.
- *
- * @param requestParams
- * @return
- * @throws Exception
- */
- public Map fetchTokenWith(Map requestParams) throws Exception {
- Client client = ClientBuilder.newClient();
- WebTarget target = client.target(oauthTokenUrl);
-
- Response response = target.request().post(Entity.form(new MultivaluedHashMap(requestParams)));
-
- @SuppressWarnings("unchecked")
- Map result = response.readEntity(HashMap.class);
- return result;
- }
-
- private Map getQueryMap(String url) throws Exception {
- Map response = new HashMap();
- String[] pairs = new URI(url).getQuery().split("&");
- for (String pair : pairs) {
- int idx = pair.indexOf("=");
- response.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
- URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
- }
- return response;
- }
-
- // main method allows for easy standalone testing. This could be moved to a separate file.
- public static void main(String[] args) throws Exception {
- String realm = "test";
- String baseUrl = "http://localhost:8080/auth/realms/" + realm + "/protocol/openid-connect/";
-
- SeleniumOauthInteraction s = new SeleniumOauthInteraction("test", "https://localhost",
- baseUrl + "auth", baseUrl + "token");
-
- Map authResponse = s.fetchCode("a", "a", "https://localhost:9443/fhir-server/api/v4",
- "openid", "launch/patient");
- Map tokenResponse = s.fetchToken(authResponse.get("code"));
-
- System.out.println(tokenResponse);
- }
+ private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumOauthInteraction.class);
+
+ private String appClientId;
+ private String appRedirectUri;
+ private String oauthAuthUrl;
+ private String oauthTokenUrl;
+
+ private WebDriver driver;
+
+ public SeleniumOauthInteraction(String appclient_id, String appredirect_uri,
+ String oauth_auth_url, String oauth_token_url){
+ appClientId = appclient_id;
+ appRedirectUri = appredirect_uri;
+ oauthAuthUrl = oauth_auth_url;
+ oauthTokenUrl = oauth_token_url;
+
+ WebDriverManager.chromedriver().setup();
+ ChromeOptions options = new ChromeOptions();
+ options.setHeadless(true);
+ driver = new ChromeDriver(options);
+ }
+
+ /**
+ * Hits the configured auth url with the configured client_id and redirect_uri, as well as
+ * the passed audience and scopes, then tests the login forms with the passed username and password via
+ * Selenium WebDriver.
+ *
+ * @param user
+ * @param pass
+ * @param aud
+ * @param scope one or more requested scopes
+ * @return a map of key-value pairs from the query string of the redirect location on the auth response
+ * @throws Exception
+ */
+ public Map fetchCode(String user, String pass, String aud, String... scope) throws Exception {
+ Map response = new HashMap();
+
+ try {
+ BasicNameValuePair[] params = new BasicNameValuePair[] {
+ new BasicNameValuePair("response_type", "code"),
+ new BasicNameValuePair("state", UUID.randomUUID().toString()),
+ new BasicNameValuePair("client_id", appClientId),
+ new BasicNameValuePair("redirect_uri", appRedirectUri),
+ new BasicNameValuePair("aud", aud),
+ new BasicNameValuePair("scope", String.join(" ", scope)),
+ };
+ String queryString = URLEncodedUtils.format(Arrays.asList(params), UTF_8);
+
+ // launch Firefox and direct it to the Base URL
+ driver.get(oauthAuthUrl + "?" + queryString);
+
+ WebElement dynamicElement = (new WebDriverWait(driver, 30))
+ .until(ExpectedConditions.presenceOfElementLocated(By.id("username")));
+ dynamicElement.sendKeys(user);
+
+ driver.findElement(By.id("password")).sendKeys(pass);
+ driver.findElement(By.id("kc-login")).click();
+
+ Boolean loginButtonDisappeared = (new WebDriverWait(driver, 5, 200))
+ .until(ExpectedConditions.invisibilityOfElementLocated(By.id("kc-login")));
+ LOGGER.debug("Login button is visible?? " + !loginButtonDisappeared);
+
+ // At this point we'll either find:
+ // A) patient selection form
+ // B) consent grant form
+ // C) URL with 'code=' in the query string
+ // D) 'Update Account Information' - user created with sign_up API.
+
+ // Wait up to 3 seconds for screen showing 'Select patient'
+ // Page contents:
+ // div class=login-pf-page
+ // div class=card-pf
+ // header.div kc-username
+ // div kc-content
+ // form id=patient-selection
+ // input id= (one per patient that the user has access to)
+ // input id=submit
+ try {
+ // wait up to 3 seconds - poll for element every 200 ms
+ new WebDriverWait(driver, 5, 200)
+ .until(ExpectedConditions.presenceOfElementLocated(By.id("patient-selection")));
+
+ // simulate choosing the patient that has an id of "PatientA"
+ driver.findElement(By.id("PatientA")).click();
+ driver.findElement(By.id("submit")).click();
+
+ } catch ( TimeoutException e ) {
+ LOGGER.error("Expected the patient selection form but didn't find it", e);
+ fail("Expected the patient selection form but didn't find it");
+ }
+
+ // wait up to 2 seconds for screen showing YES/NO page for first-time users.
+ // Page contents:
+ // kc-page-title=Grant Access to inferno
+ // li - permissions granted
+ // input id=kc-cancel NO
+ // input kd=kc-login YES
+ try {
+ // either the button is found or a TimeoutException is generated
+ WebElement grantAccessButton = (new WebDriverWait(driver, 1,200))
+ .until(ExpectedConditions.presenceOfElementLocated(By.id("kc-login")));
+
+ grantAccessButton.click();
+ } catch ( TimeoutException e ) {
+ // Didn't find YES button with id='kc-login'.
+ // Ignore exception; probably not the first sign-on for this user.
+ LOGGER.error("Didn't find YES button with id='kc-login' - ignore exception" + e.getMessage());
+ }
+
+ // poll at 500 ms interval until 'code' is present in URL query parameter list.
+ // Usually this loop completes in under 2.5 seconds.
+ for ( int i =0; i< 100; i++) {
+ Thread.sleep(500);
+ response = getQueryMap( driver.getCurrentUrl() );
+ if ( response.keySet().contains("code") )
+ {
+ break;
+ }
+ }
+ } catch (Exception e) {
+ if (driver == null || driver.getCurrentUrl() == null) {
+ throw e;
+ }
+ // adding the while loop to avoid the timing issues and reliably
+ // extract the grant code from next page after clicking the login
+ // button
+ // Note: the handling in exception is required because on some
+ // operating systems the selenium webdriver exits browser when
+ // the recipient host is unable to connect.
+ int cnt = 0;
+ while (!driver.getCurrentUrl().contains("code=")) {
+ try {
+ Thread.sleep(500);
+ cnt++;
+ if (cnt > 10) {
+ LOGGER.debug("Waiting for page to retrieve grant code, Round ... " + cnt);
+ break;
+ }
+ } catch (InterruptedException e1) {
+ // do nothing
+ }
+ }
+ if (!driver.getCurrentUrl().contains("code=")) {
+ LOGGER.error("Something went wrong during the oauth code retreival process", e);
+ } else {
+ response = getQueryMap(driver.getCurrentUrl());
+ }
+ } finally {
+ if (driver != null) {
+ // close Firefox
+ driver.close();
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Exchange the code for a token (with default params)
+ *
+ * @param code
+ * @return
+ * @throws Exception
+ */
+ public Map fetchToken(String code) throws Exception {
+ Map headers = new HashMap<>();
+ headers.put("Content-Type", "application/x-www-form-urlencoded");
+
+ Map params = new HashMap<>();
+ params.put("grant_type", "authorization_code");
+ params.put("code", code);
+ params.put("client_id", appClientId);
+ params.put("redirect_uri", appRedirectUri);
+
+ return fetchTokenWith(params);
+ }
+
+ /**
+ * Invoke the token endpoint with the passed params.
+ *
+ * @param requestParams
+ * @return
+ * @throws Exception
+ */
+ public Map fetchTokenWith(Map requestParams) throws Exception {
+ Client client = ClientBuilder.newClient();
+ WebTarget target = client.target(oauthTokenUrl);
+
+ Response response = target.request().post(Entity.form(new MultivaluedHashMap(requestParams)));
+
+ @SuppressWarnings("unchecked")
+ Map result = response.readEntity(HashMap.class);
+ return result;
+ }
+
+ private Map getQueryMap(String url) throws Exception {
+ Map response = new HashMap();
+ String[] pairs = new URI(url).getQuery().split("&");
+ for (String pair : pairs) {
+ int idx = pair.indexOf("=");
+ response.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"),
+ URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
+ }
+ return response;
+ }
+
+ // main method allows for easy standalone testing. This could be moved to a separate file.
+ public static void main(String[] args) throws Exception {
+ String realm = "test";
+ String baseUrl = "http://localhost:8080/realms/" + realm + "/protocol/openid-connect/";
+
+ SeleniumOauthInteraction s = new SeleniumOauthInteraction("test", "https://localhost",
+ baseUrl + "auth", baseUrl + "token");
+
+ Map authResponse = s.fetchCode("a", "a", "https://localhost:9443/fhir-server/api/v4",
+ "openid", "launch/patient");
+ Map tokenResponse = s.fetchToken(authResponse.get("code"));
+
+ System.out.println(tokenResponse);
+ }
}
diff --git a/keycloak-extensions/src/test/resources/jboss/module.xml b/keycloak-extensions/src/test/resources/jboss/module.xml
deleted file mode 100644
index 7e0a2b2..0000000
--- a/keycloak-extensions/src/test/resources/jboss/module.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/keycloak-extensions/src/test/resources/keycloak-config.json b/keycloak-extensions/src/test/resources/keycloak-config.json
index ae859b8..923e6e0 100644
--- a/keycloak-extensions/src/test/resources/keycloak-config.json
+++ b/keycloak-extensions/src/test/resources/keycloak-config.json
@@ -64,7 +64,7 @@
"builtIn": false,
"authenticationExecutions": {
"SMART Login": {
- "requirement": "ALTERNATIVE",
+ "requirement": "REQUIRED",
"userSetupAllowed": false,
"authenticatorFlow": true,
"description": "Username, password, otp and other auth forms.",
@@ -80,24 +80,34 @@
"audiences": "https://localhost:9443/fhir-server/api/v4##http://host.testcontainers.internal:9080/fhir-server/api/v4"
}
},
- "Username Password Form": {
- "authenticator": "auth-username-password-form",
- "requirement": "REQUIRED",
- "priority": 20,
- "authenticatorFlow": false
- },
- "Patient Selection Authenticator": {
- "authenticator": "auth-select-patient",
- "requirement": "REQUIRED",
- "priority": 30,
- "authenticatorFlow": false,
- "configAlias": "host.docker",
- "config": {
- "internalFhirUrl": "http://host.testcontainers.internal:${FHIR_PORT}/fhir-server/api/v4"
- }
- }
+ "Forms" : {
+ "description": "Forms",
+ "priority": 20,
+ "providerId": "basic-flow",
+ "builtIn": false,
+ "requirement": "REQUIRED",
+ "authenticatorFlow": true,
+ "authenticationExecutions": {
+ "Username Password Form": {
+ "authenticator": "auth-username-password-form",
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "authenticatorFlow": false
+ }
+ }
+ }
}
- }
+ },
+ "Patient Selection Authenticator": {
+ "authenticator": "auth-select-patient",
+ "requirement": "REQUIRED",
+ "priority": 30,
+ "authenticatorFlow": false,
+ "configAlias": "host.docker",
+ "config": {
+ "internalFhirUrl": "http://host.testcontainers.internal:${FHIR_PORT}/fhir-server/api/v4"
+ }
+ }
}
}
},
diff --git a/pom.xml b/pom.xml
index d945bf7..d1b21f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,17 +10,17 @@
pom
- 18.0.0
- 1.10.0
- 4.11.1
+ 18.0.2
+
+ 2.2.2
11
11
+ 5.7.2
keycloak-config
keycloak-extensions
- jboss-fhir-provider
@@ -75,19 +75,9 @@
1.9
- com.ibm.fhir
- fhir-model
- ${ibm-fhir-server.version}
-
-
- com.ibm.fhir
- fhir-provider
- ${ibm-fhir-server.version}
-
-
- junit
- junit
- 4.13.2
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.9.1
test
@@ -144,6 +134,12 @@
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M7
+
org.apache.maven.plugins
maven-jar-plugin