From ffe3dbe0f3fcf6af16fcddc50a5198496c886af5 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Wed, 3 Apr 2024 12:04:19 +0100 Subject: [PATCH 01/33] fix(jans-linux-setup): improper scim configuration for jans kc #8210 * updated the keycloak configuration file to reflect the configuration for the storage-spi Signed-off-by: Rolain Djeumen --- .../templates/jans-saml/keycloak.conf | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/jans-linux-setup/jans_setup/templates/jans-saml/keycloak.conf b/jans-linux-setup/jans_setup/templates/jans-saml/keycloak.conf index 1f6b35812c3..7ebe10d3dff 100644 --- a/jans-linux-setup/jans_setup/templates/jans-saml/keycloak.conf +++ b/jans-linux-setup/jans_setup/templates/jans-saml/keycloak.conf @@ -41,22 +41,23 @@ # Janssen configuration parameters -# Storage spi configuration +# Storage SPI Configuration (SCIM) -# token endpoint -#jans-storage-auth-token-endpoint= +# janssen-auth token endpoint +spi-storage-kc-jans-storage-auth-token-endpoint= -# scim user endpoint -#jans-storage-scim-user-endpoint +# janssen scim user fetch endpoint +# usually of the format https:///jans-scim/restv1/v2/Users +spi-storage-kc-jans-storage-scim-user-endpoint= # scim user search endpoint -#jans-storage-scim-user-search-endpoint +spi-storage-kc-jans-storage-scim-user-search-endpoint= # scim oauth scopes -#jans-storage-scim-oauth-scope +spi-storage-kc-jans-storage-scim-oauth-scopes=https://jans.io/scim/users.read https://jans.io/scim/users.write #scim client id -#jans-storage-client-id +spi-storage-kc-jans-storage-scim-client-id= #scim client secret -#jans-storage-client-secret +spi-storage-kc-jans-storage-scim-client-secret= From fadf1f27ce112b58abf9a76adf1276ea0e3ab248 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 16 Apr 2024 09:25:07 +0100 Subject: [PATCH 02/33] chore(jans-keycloak-integration): bump kc version to 24.0.0 #8315 Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index ecba52be525..5f866ec9448 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -19,7 +19,7 @@ 3.3.9 17 17 - 23.0.3 + 24.0.0 10.11 10.11 1.8 From 8bde8d5e7854adaa7213bc92a75496eea51d05f3 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 30 Apr 2024 07:01:35 +0100 Subject: [PATCH 03/33] feat(jans-keycloak-integration): keycloak protocol mapper Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 6 ++ .../protocol-mapper/pom.xml | 80 +++++++++++++++++ .../src/assembly/dependencies.xml | 24 ++++++ .../protocol/mapper/SamlProtocolMapper.java | 85 +++++++++++++++++++ .../config/ProtocolMapperConfiguration.java | 7 ++ .../ProtocolMapperConfigurationEntry.java | 6 ++ .../ProtocolMapperConfigurationFactory.java | 25 ++++++ .../org.keycloak.protocol.ProtocolMapper | 1 + .../src/main/resources/assembly/.DONOTDELETE | 1 + 9 files changed, 235 insertions(+) create mode 100644 jans-keycloak-integration/protocol-mapper/pom.xml create mode 100644 jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index 5f866ec9448..483d784e604 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -143,6 +143,12 @@ jans-scim-model ${jans.version} + + + io.jans + jans-orm-standalone + ${jans.version} + diff --git a/jans-keycloak-integration/protocol-mapper/pom.xml b/jans-keycloak-integration/protocol-mapper/pom.xml new file mode 100644 index 00000000000..9f2d7ccb338 --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + io.jans + kc-jans-protocol-mapper + kc-jans-protocol-mapper + jar + + + io.jans + jans-kc-parent + 1.1.1 + + + + ${maven.min-version} + + + + + + + org.keycloak + keycloak-core + + + + org.keycloak + keycloak-server-spi + + + + org.keycloak + keycloak-server-spi-private + + + + org.keycloak + keycloak-services + + + + org.keycloak + keycloak-saml-core-public + + + + + + io.jans + jans-orm-standalone + + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + diff --git a/jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml b/jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml new file mode 100644 index 00000000000..373f6f1d362 --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml @@ -0,0 +1,24 @@ + + deps + + zip + + false + + + target/deps/ + . + + *.jar + + + + src/main/resources/assembly + . + + *.DONOTDELETE + + + + \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java new file mode 100644 index 00000000000..5a5a52a159f --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java @@ -0,0 +1,85 @@ +package io.jans.kc.protocol.mapper; + +import java.util.ArrayList; +import java.util.List; + +import org.keycloak.Config; + +import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; + +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper; +import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; + + + + +public class JansSamlProtocolMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { + + private static final String DISPLAY_TYPE = "User Attribute"; + private static final String DISPLAY_CATEGORY = "User Attribute Mapper"; + private static final String HELP_TEXT = "Janssen User Attributes Protocol Mapper"; + private static final String PROVIDER_ID = "kc-jans-saml-protocol-mapper"; + private static final List configProperties = new ArrayList<>(); + + @Override + public void close() { + + + } + + @Override + public List getConfigProperties() { + + return configProperties; + } + + @Override + public String getId() { + + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + + return DISPLAY_TYPE; + } + + @Override + public String getDisplayCategory() { + + return DISPLAY_CATEGORY; + } + + @Override + public String getHelpText() { + + return HELP_TEXT; + } + + @Override + public void init(Config.Scope scope) { + + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + + } + + @Override + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + + + } +} \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java new file mode 100644 index 00000000000..049b8a9baa8 --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java @@ -0,0 +1,7 @@ +package io.jans.kc.protocol.mapper.config; + +import io.jans.conf.model.AppConfigurationEntry; + +public class ProtocolMapperConfiguration extends AppConfiguration { + +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java new file mode 100644 index 00000000000..544a47172bd --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java @@ -0,0 +1,6 @@ +package io.jans.kc.protocol.mapper.config; + + +public class ProtocolMapperConfigurationEntry extends AppConfigurationEntry { + +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java new file mode 100644 index 00000000000..15f915b2234 --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java @@ -0,0 +1,25 @@ +package io.jans.kc.protocol.mapper.config; + +import io.jans.conf.service.ConfigurationFactory; + +public class ProtocolMapperConfigurationFactory extends ConfigurationFactory { + + + @Override + protected String getDefaultConfigurationFileName() { + + return null; + } + + @Override + protected Class getAppConfigurationType() { + + return ProtocolMapperConfigurationEntry.class; + } + + @Overide + protected String getApplicationConfigurationPropertyName() { + + return null; + } +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper new file mode 100644 index 00000000000..5a84b825cfc --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -0,0 +1 @@ +io.jans.kc.protocol.mapper.JansSamlProtocolMapper \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE b/jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE new file mode 100644 index 00000000000..c7c1c13b13c --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE @@ -0,0 +1 @@ +This file is used to prevent build errors during the run of the maven assembly plugin \ No newline at end of file From 0273af804ffd9c9bf924850be7ee0759cacdadc7 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Wed, 1 May 2024 06:35:16 +0100 Subject: [PATCH 04/33] feat(jans-keycloak-integration): remove references to jans standalone persistence layer Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 6 ----- .../protocol-mapper/pom.xml | 10 ++------ .../protocol/mapper/SamlProtocolMapper.java | 2 +- .../config/ProtocolMapperConfiguration.java | 7 ------ .../ProtocolMapperConfigurationEntry.java | 6 ----- .../ProtocolMapperConfigurationFactory.java | 25 ------------------- .../org.keycloak.protocol.ProtocolMapper | 2 +- 7 files changed, 4 insertions(+), 54 deletions(-) delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index 27cb4f1cfac..0f64c4ab2af 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -151,12 +151,6 @@ jans-scim-model ${jans.version} - - - io.jans - jans-orm-standalone - ${jans.version} - diff --git a/jans-keycloak-integration/protocol-mapper/pom.xml b/jans-keycloak-integration/protocol-mapper/pom.xml index 9f2d7ccb338..485d800a1c2 100644 --- a/jans-keycloak-integration/protocol-mapper/pom.xml +++ b/jans-keycloak-integration/protocol-mapper/pom.xml @@ -9,7 +9,7 @@ io.jans jans-kc-parent - 1.1.1 + 1.1.2-SNAPSHOT @@ -44,13 +44,7 @@ keycloak-saml-core-public - - - - io.jans - jans-orm-standalone - - + diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java index 5a5a52a159f..75a892f88b1 100644 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java @@ -20,7 +20,7 @@ -public class JansSamlProtocolMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { +public class SamlProtocolMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { private static final String DISPLAY_TYPE = "User Attribute"; private static final String DISPLAY_CATEGORY = "User Attribute Mapper"; diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java deleted file mode 100644 index 049b8a9baa8..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfiguration.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jans.kc.protocol.mapper.config; - -import io.jans.conf.model.AppConfigurationEntry; - -public class ProtocolMapperConfiguration extends AppConfiguration { - -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java deleted file mode 100644 index 544a47172bd..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationEntry.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jans.kc.protocol.mapper.config; - - -public class ProtocolMapperConfigurationEntry extends AppConfigurationEntry { - -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java deleted file mode 100644 index 15f915b2234..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/ProtocolMapperConfigurationFactory.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.jans.kc.protocol.mapper.config; - -import io.jans.conf.service.ConfigurationFactory; - -public class ProtocolMapperConfigurationFactory extends ConfigurationFactory { - - - @Override - protected String getDefaultConfigurationFileName() { - - return null; - } - - @Override - protected Class getAppConfigurationType() { - - return ProtocolMapperConfigurationEntry.class; - } - - @Overide - protected String getApplicationConfigurationPropertyName() { - - return null; - } -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 5a84b825cfc..62078d8daf0 100644 --- a/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -1 +1 @@ -io.jans.kc.protocol.mapper.JansSamlProtocolMapper \ No newline at end of file +io.jans.kc.protocol.mapper.SamlProtocolMapper \ No newline at end of file From 10a0162717567ddea4c1756ab182dc7da45fcf97 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Thu, 30 May 2024 10:00:29 +0100 Subject: [PATCH 05/33] feat(jans-keycloak-integration): experimental protocol mapper for kc #8614 * added persistence manager configuration for protocol mapper Signed-off-by: Rolain Djeumen --- .../persistence/PersistenceConfiguration.java | 7 +++++ .../PersistenceConfigurationEntry.java | 7 +++++ .../PersistenceConfigurationFactory.java | 28 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java new file mode 100644 index 00000000000..7fbf45bb7ba --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java @@ -0,0 +1,7 @@ +package io.jans.kc.protocol.mapper.config.persistence; + +import io.jans.conf.model.AppConfiguration; + +public class PersistenceConfiguration extends AppConfiguration { + +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java new file mode 100644 index 00000000000..2c76efa2c94 --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java @@ -0,0 +1,7 @@ +package io.jans.kc.protocol.mapper.config.persistence; + +import io.jans.conf.model.AppConfigurationEntry; + +public class PersistenceConfigurationEntry extends AppConfigurationEntry { + +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java new file mode 100644 index 00000000000..8160fdbcf7d --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java @@ -0,0 +1,28 @@ +package io.jans.kc.protocol.mapper.config.persistence; + +import io.jans.conf.model.AppConfiguration; +import io.jans.conf.model.AppConfigurationEntry; + +import io.jans.conf.service.ConfigurationFactory; + + +public class PersistenceConfigurationFactory extends ConfigurationFactory{ + + @Override + protected String getDefaultConfigurationFileName() { + + return null; + } + + @Override + protected Class getAppConfigurationType() { + + return null; + } + + @Override + protected String getApplicationConfigurationPropertyName() { + + return null; + } +} From 0f1c5a47947679b3dd6b21e35f8485346b31453e Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Thu, 30 May 2024 13:41:27 +0100 Subject: [PATCH 06/33] feat(jans-keycloak-integration): added dependencies for protocol mapper #8614 Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 66 +++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index 6e5c62da134..d4577f5b95d 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -60,6 +60,7 @@ authenticator storage-spi job-scheduler + protocol-mapper @@ -144,14 +145,6 @@ - - - io.jans - jans-scim-model - ${jans.version} - - - jakarta.ws.rs @@ -282,7 +275,55 @@ io.jans jans-core-saml - ${project.version} + ${jans.version} + + + + io.jans + jans-scim-model + ${jans.version} + + + + io.jans + jans-core-standalone + ${jans.version} + + + + io.jans + jans-orm-standalone + ${jans.version} + + + + io.jans + jans-orm-couchbase + ${jans.version} + + + + io.jans + jans-orm-hybrid + ${jans.version} + + + + io.jans + jans-orm-ldap + ${jans.version} + + + + io.jans + jans-orm-sql + ${jans.version} + + + + io.jans + jans-core-service + ${jans.version} @@ -335,14 +376,15 @@ com.fasterxml.jackson.core, commons-codec, commons-lang3, - commons-lang, commons-collections4, commons-io, commons-logging, commons-text, - org.apache.commons, - commons-configuration + jakarta.persistence + + + From a089c83fc4b28c823b88a835e6d219a15c7b70fc Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Thu, 30 May 2024 14:26:53 +0100 Subject: [PATCH 07/33] feat(jans-keycloak-integration): experimental protocol mapper #8614 * added dependencies to protocol mapper * added protocol mapper main class Signed-off-by: Rolain Djeumen --- .../protocol-mapper/pom.xml | 50 ++++++ .../protocol/mapper/SamlProtocolMapper.java | 146 +++++++++++++++++- 2 files changed, 192 insertions(+), 4 deletions(-) diff --git a/jans-keycloak-integration/protocol-mapper/pom.xml b/jans-keycloak-integration/protocol-mapper/pom.xml index 485d800a1c2..6fce624d2ae 100644 --- a/jans-keycloak-integration/protocol-mapper/pom.xml +++ b/jans-keycloak-integration/protocol-mapper/pom.xml @@ -45,6 +45,56 @@ + + + io.jans + jans-core-standalone + + + + io.jans + jans-core-service + + + + io.jans + jans-orm-standalone + + + + io.jans + jans-orm-couchbase + + + + io.jans + jans-orm-hybrid + + + + io.jans + jans-orm-ldap + + + + io.jans + jans-orm-sql + + + + + + org.jboss.slf4j + slf4j-jboss-logmanager + 2.0.1.Final + + + + + org.apache.commons + commons-dbcp2 + 2.12.0 + diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java index 75a892f88b1..fc514247916 100644 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java @@ -3,9 +3,14 @@ import java.util.ArrayList; import java.util.List; +import io.jans.orm.search.filter.Filter; + +import org.jboss.logging.Logger; + import org.keycloak.Config; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; +import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; @@ -13,21 +18,69 @@ import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +import io.jans.orm.model.base.CustomObjectAttribute; +import io.jans.kc.protocol.mapper.config.PersistenceConfigurationException; +import io.jans.kc.protocol.mapper.config.PersistenceConfigurationFactory; +import io.jans.kc.protocol.mapper.model.JansPerson; +import io.jans.model.JansAttribute; +import io.jans.model.GluuStatus; +import io.jans.orm.PersistenceEntryManager; + import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper; import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; public class SamlProtocolMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { - private static final String DISPLAY_TYPE = "User Attribute"; - private static final String DISPLAY_CATEGORY = "User Attribute Mapper"; - private static final String HELP_TEXT = "Janssen User Attributes Protocol Mapper"; + private static final String DISPLAY_TYPE = "Janssen User Attribute"; + private static final String DISPLAY_CATEGORY = AttributeStatementHelper.ATTRIBUTE_STATEMENT_CATEGORY; + private static final String HELP_TEXT = "Maps a Janssen User's Attribute to a SAML Attribute"; private static final String PROVIDER_ID = "kc-jans-saml-protocol-mapper"; - private static final List configProperties = new ArrayList<>(); + //properties + private static final String JANS_ATTR_NAME_PROP_NAME = "jans.attribute.name"; + private static final String JANS_ATTR_NAME_PROP_LABEL = "Jans Attribute"; + private static final String JANS_ATTR_NAME_PROP_HELPTEXT = "Name of the Attribute in Janssen Auth Server"; + private static final List configProperties; + + + private static final Logger log = Logger.getLogger(SamlProtocolMapper.class); + + private final PersistenceConfigurationFactory persistenceConfigurationFactory; + + static { + + + configProperties = ProviderConfigurationBuilder.create() + .property() + .name(JANS_ATTR_NAME_PROP_NAME) + .label(JANS_ATTR_NAME_PROP_LABEL) + .helpText(JANS_ATTR_NAME_PROP_HELPTEXT) + .type(ProviderConfigProperty.STRING_TYPE) + .defaultValue(null) + .required(true) + .add() + .build(); + } + + public SamlProtocolMapper() { + + try { + persistenceConfigurationFactory = PersistenceConfigurationFactory.create(); + PersistenceEntryManager persistenceEntryManager = persistenceConfigurationFactory.getPersistenceEntryManager(); + + }catch(PersistenceConfigurationException e) { + throw new RuntimeException("Could not instantiate protocol mapper",e); + } + } + @Override public void close() { @@ -80,6 +133,91 @@ public void postInit(KeycloakSessionFactory factory) { public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + final String attributename = mappingModel.getConfig().get(JANS_ATTR_NAME_PROP_NAME); + log.infov("Transform attribute statement. Attribute name: {0}",attributename); + JansAttribute attr = findJansAttributeByName(attributename); + if(attr == null) { + log.infov("No attribute found by name : {0}. No transformation to effect",attributename); + return; + } + + if(attr.getStatus() != GluuStatus.ACTIVE) { + log.infov("Attribute {0} disabled. Skipping it for transformAttributeStatement()"); + return; + } + JansPerson person = findJansPersonByUsername(userSession.getLoginUsername(),new String[] {attributename}); + if(person == null) { + log.infov("No jans User associated with this keycloak session's user {0}",userSession.getLoginUsername()); + return; + } + addJansAttributeValueFromPerson(attr,person,attributeStatement,mappingModel,userSession); + } + + private PersistenceEntryManager getPersistenceEntryManager() { + + return persistenceConfigurationFactory.getPersistenceEntryManager(); + } + + private void addJansAttributeValueFromPerson(JansAttribute jansAttribute, JansPerson jansPerson, + AttributeStatementType attributeStatement, ProtocolMapperModel protocolMapper,UserSessionModel userSession) { + + if(!jansPerson.hasCustomAttributes()) { + log.infov("Jans User with keycloak login username {0} returned no custom attributes.",userSession.getLoginUsername()); + return; + } + AttributeType attributeType = createAttributeType(protocolMapper, jansAttribute); + List values = jansPerson.customAttributeValues(jansAttribute); + if(values == null) { + log.infov("Jans user with keycloak login username {0} returned no values for attribute {1}", + userSession.getLoginUsername(),jansAttribute.getName()); + return; + } + + values.forEach(attributeType::addAttributeValue); + attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType)); + } + + private JansAttribute findJansAttributeByName(final String jansAttrName) { + + final String [] attrs = new String [] { + "displayName", + "jansAttrTyp", + "jansClaimName", + "jansSAML1URI", + "jansSAML2URI", + "jansStatus", + "jansAttrName" + }; + + final Filter filter = Filter.createEqualityFilter("jansAttrName",jansAttrName); + return getPersistenceEntryManager().findEntries("ou=attributes,o=jans",JansAttribute.class,filter,attrs).get(0); + } + + private JansPerson findJansPersonByUsername(final String username, final String [] returnattributes) { + + final Filter uidsearchfilter = Filter.createEqualityFilter("uid",username); + final Filter mailsearchfilter = Filter.createEqualityFilter("mail",username); + final Filter usersearchfilter = Filter.createORFilter(uidsearchfilter,mailsearchfilter); + + return getPersistenceEntryManager().findEntries("ou=people,o=jans",JansPerson.class,usersearchfilter,returnattributes).get(0); + } + + private AttributeType createAttributeType(ProtocolMapperModel model, JansAttribute jansAttributeMeta) { + + String attributeName = jansAttributeMeta.getSaml2Uri(); + String attributeNameFormat = JBossSAMLURIConstants.ATTRIBUTE_FORMAT_URI.get(); + if(jansAttributeMeta.getSaml2Uri() == null || jansAttributeMeta.getSaml1Uri().isEmpty()) { + attributeName = jansAttributeMeta.getName(); + attributeNameFormat = JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get(); + } + + AttributeType ret = new AttributeType(attributeName); + ret.setNameFormat(attributeNameFormat); + if(jansAttributeMeta.getDisplayName() != null && !jansAttributeMeta.getDisplayName().trim().isEmpty()) { + ret.setFriendlyName(jansAttributeMeta.getDisplayName()); + } + return ret; } + } \ No newline at end of file From aa6e65d74ccaec1fdbc5a8122dd40607fb2744e8 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Thu, 30 May 2024 14:39:43 +0100 Subject: [PATCH 08/33] feat(jans-keycloak-integration): experimental protocol mapper #8614 * added relevant models to fetch user attributes * refactored the db configuration classes Signed-off-by: Rolain Djeumen --- .../PersistenceConfigurationException.java | 13 ++ .../PersistenceConfigurationFactory.java | 158 ++++++++++++++++++ .../persistence/PersistenceConfiguration.java | 7 - .../PersistenceConfigurationEntry.java | 7 - .../PersistenceConfigurationFactory.java | 28 ---- .../kc/protocol/mapper/model/JansPerson.java | 86 ++++++++++ 6 files changed, 257 insertions(+), 42 deletions(-) create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java create mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java new file mode 100644 index 00000000000..6216727822d --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java @@ -0,0 +1,13 @@ +package io.jans.kc.protocol.mapper.config; + +public class PersistenceConfigurationException extends RuntimeException { + + + public PersistenceConfigurationException(String msg) { + super(msg); + } + + public PersistenceConfigurationException(String msg, Throwable cause) { + super(msg,cause); + } +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java new file mode 100644 index 00000000000..2769907a433 --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java @@ -0,0 +1,158 @@ +package io.jans.kc.protocol.mapper.config; + +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.PersistenceEntryManagerFactory; +import io.jans.orm.model.PersistenceConfiguration; + +import io.jans.orm.service.PersistanceFactoryService; +import io.jans.orm.service.StandalonePersistanceFactoryService; +import io.jans.util.StringHelper; +import io.jans.util.exception.ConfigurationException; +import io.jans.orm.util.properties.FileConfiguration; +import io.jans.util.security.PropertiesDecrypter; +import io.jans.util.security.StringEncrypter; + + +import java.io.File; +import java.util.Properties; + +import org.apache.commons.lang.StringUtils; + +import org.jboss.logging.Logger; + +public class PersistenceConfigurationFactory { + + + private static final Logger log = Logger.getLogger(PersistenceConfigurationFactory.class); + private static final String DEFAULT_CONFIG_FILE_NAME = "jans.properties"; + private static final String SALT_FILE_NAME = "salt"; + + private final PersistanceFactoryService persistenceFactoryService; + private final PersistenceConfiguration persistenceConfig; + private final PersistenceEntryManager persistenceEntryManager; + + public PersistenceConfigurationFactory(final PersistanceFactoryService persistenceFactoryService, + final PersistenceConfiguration persistenceConfig, final PersistenceEntryManager persistenceEntryManager) { + + this.persistenceFactoryService = persistenceFactoryService; + this.persistenceConfig = persistenceConfig; + this.persistenceEntryManager = persistenceEntryManager; + } + + public final PersistenceEntryManager getPersistenceEntryManager() { + + return persistenceEntryManager; + } + + public static PersistenceConfigurationFactory create() { + + PersistanceFactoryService persistenceFactoryService = new StandalonePersistanceFactoryService(); + PersistenceConfiguration config = persistenceFactoryService.loadPersistenceConfiguration(getDefaultConfigurationFileName()); + if(config == null) { + + throw new PersistenceConfigurationException("Failed to load persistence configuration from file. " + + "\n+ Jans configuration base directory: " + getJansConfigurationBaseDir() + + "\n+ Jans configuration default file: " + getDefaultConfigurationFileName()); + } + + FileConfiguration baseConfiguration = loadBaseConfiguration(); + final String jansConfigBaseDir = getJansConfigurationBaseDir(); + String confdir = config.getConfiguration().getString("confDir"); + if(!StringUtils.isNotBlank(confdir)) { + confdir = getJansConfigurationBaseDir() + File.separator + "conf" + File.separator; + } + final String salt = cryptographicSaltFromFile(confdir+SALT_FILE_NAME); + if(!StringUtils.isNotBlank(salt)) { + throw new PersistenceConfigurationException("Failed to load cryptographic material from configuration"); + } + PersistenceEntryManager persistenceEntryManager = createPersistenceEntryManager(config, persistenceFactoryService, salt); + return new PersistenceConfigurationFactory(persistenceFactoryService,config,persistenceEntryManager); + } + + private static final String getDefaultConfigurationFileName() { + + return DEFAULT_CONFIG_FILE_NAME; + } + + private static final FileConfiguration loadBaseConfiguration() { + + return createFileConfiguration(DEFAULT_CONFIG_FILE_NAME,true); + } + + private static final FileConfiguration createFileConfiguration(String fileName, boolean mandatory) { + + try { + return new FileConfiguration(fileName); + }catch(Exception e) { + log.errorv(e,"Failed to load configuration from {0}",fileName); + if(mandatory && fileName != null) { + throw new PersistenceConfigurationException("Failed to load configuration from "+fileName,e); + }else if(mandatory && fileName == null) { + throw new PersistenceConfigurationException("Failed to load configuration because filename was invalid",e); + } + return null; + } + } + + private static final StringEncrypter createStringEncrypterFromSaltFile(final String path) { + + try { + final String salt = cryptographicSaltFromFile(path); + if(StringHelper.isEmpty(salt)) { + throw new PersistenceConfigurationException("Failed to create string encrypter. No cryptographic salt"); + } + return StringEncrypter.instance(salt); + }catch(StringEncrypter.EncryptionException e) { + throw new PersistenceConfigurationException("Failed to create string encrypter",e); + } + } + + private static final String cryptographicSaltFromFile(final String path) { + + try { + FileConfiguration cryptoconfig = new FileConfiguration(path); + return cryptoconfig.getString("encodeSalt"); + }catch(Exception e){ + log.errorv(e,"Failed to load cryptographic salt from {}",path); + throw new PersistenceConfigurationException("Failed to load cryptographic salt from " + path,e); + } + } + + private static final Properties preparePersistenceProperties(final PersistenceConfiguration persistenceConfiguration, final String salt) { + + try { + FileConfiguration config = persistenceConfiguration.getConfiguration(); + Properties connprops = (Properties) config.getProperties(); + return PropertiesDecrypter.decryptAllProperties(StringEncrypter.defaultInstance(),connprops,salt); + }catch(StringEncrypter.EncryptionException e) { + throw new PersistenceConfigurationException("Failed to decrypt persistence connection parameters",e); + } + } + + private static final PersistenceEntryManager createPersistenceEntryManager(final PersistenceConfiguration config, + final PersistanceFactoryService persistenceFactoryService, final String salt) { + + try { + Properties persistenceconnprops = preparePersistenceProperties(config,salt); + PersistenceEntryManagerFactory persistenceEntryManagerFactory = persistenceFactoryService.getPersistenceEntryManagerFactory(config); + return persistenceEntryManagerFactory.createEntryManager(persistenceconnprops); + }catch(Exception e) { + throw new PersistenceConfigurationException("Failed to create persistence entry manager",e); + } + } + + private static final String getJansConfigurationBaseDir() { + + if(System.getProperty("jans.base") != null) { + return System.getProperty("jans.base"); + }else if((System.getProperty("catalina.base") != null) && (System.getProperty("catalina.base.ignore") == null)) { + return System.getProperty("catalina.base"); + }else if(System.getProperty("catalina.home") != null) { + return System.getProperty("catalina.home"); + }else if(System.getProperty("jboss.home.dir") != null) { + return System.getProperty("jboss.home.dir"); + } + + return null; + } +} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java deleted file mode 100644 index 7fbf45bb7ba..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfiguration.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jans.kc.protocol.mapper.config.persistence; - -import io.jans.conf.model.AppConfiguration; - -public class PersistenceConfiguration extends AppConfiguration { - -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java deleted file mode 100644 index 2c76efa2c94..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationEntry.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jans.kc.protocol.mapper.config.persistence; - -import io.jans.conf.model.AppConfigurationEntry; - -public class PersistenceConfigurationEntry extends AppConfigurationEntry { - -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java deleted file mode 100644 index 8160fdbcf7d..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/persistence/PersistenceConfigurationFactory.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.jans.kc.protocol.mapper.config.persistence; - -import io.jans.conf.model.AppConfiguration; -import io.jans.conf.model.AppConfigurationEntry; - -import io.jans.conf.service.ConfigurationFactory; - - -public class PersistenceConfigurationFactory extends ConfigurationFactory{ - - @Override - protected String getDefaultConfigurationFileName() { - - return null; - } - - @Override - protected Class getAppConfigurationType() { - - return null; - } - - @Override - protected String getApplicationConfigurationPropertyName() { - - return null; - } -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java new file mode 100644 index 00000000000..b7b7ab12ccd --- /dev/null +++ b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java @@ -0,0 +1,86 @@ +package io.jans.kc.protocol.mapper.model; + +import java.io.Serializable; + +import java.util.ArrayList; +import java.util.List; + +import io.jans.model.JansAttribute; +import io.jans.orm.annotation.*; +import io.jans.orm.model.base.CustomObjectAttribute; + +@DataEntry +@ObjectClass(value="jansPerson") +public class JansPerson implements Serializable { + + private static final long serialVersionUID = 1L; + + @DN + private String dn; + + @AttributesList(name="name",value="values",multiValued="multiValued") + private List customAttributes = new ArrayList<>(); + + + public JansPerson() { + + } + + public String getDn() { + + return this.dn; + } + + public void setDn(final String dn) { + + this.dn = dn; + } + + public void setCustomAttributes(List customAttributes) { + + this.customAttributes = customAttributes; + } + + public List getCustomAttributes() { + + return this.customAttributes; + } + + public boolean hasCustomAttributes() { + + return (this.customAttributes != null && !this.customAttributes.isEmpty()); + } + + public boolean isMultiValuedCustomAttributes() { + + return hasCustomAttributes() && this.customAttributes.size() > 1; + } + + public List customAttributeValues(final JansAttribute attributeMeta) { + + + for(CustomObjectAttribute customAttribute : customAttributes) { + if(customAttribute.getName().equals(attributeMeta.getName())) { + List values = customAttribute.getValues(); + if(values == null || values.size() == 0) { + return null; + } + return convertToString(values,attributeMeta); + } + } + return null; + } + + private List convertToString(List values, final JansAttribute attributeMeta) { + + List ret = new ArrayList<>(); + for(Object val : values) { + if(val instanceof String) { + ret.add((String) val); + }else { + ret.add(val.toString()); + } + } + return ret; + } +} \ No newline at end of file From 6e6e08578f7a3951f7c61756ef5755afe6d0a3a8 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Fri, 31 May 2024 14:34:12 +0100 Subject: [PATCH 09/33] feat(jans-keycloak-integration): janssen spi bundle #8614 * created maven project for janssen spi bundle Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 1 + jans-keycloak-integration/spi/pom.xml | 124 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 jans-keycloak-integration/spi/pom.xml diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index d4577f5b95d..db1f62697d2 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -61,6 +61,7 @@ storage-spi job-scheduler protocol-mapper + spi diff --git a/jans-keycloak-integration/spi/pom.xml b/jans-keycloak-integration/spi/pom.xml new file mode 100644 index 00000000000..5603b52c745 --- /dev/null +++ b/jans-keycloak-integration/spi/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + io.jans + kc-jans-spi + kc-jans-spi + jar + + + io.jans + jans-kc-parent + 1.1.2-SNAPSHOT + + + + ${maven.min-version} + + + + + + + org.keycloak + keycloak-core + + + + org.keycloak + keycloak-server-spi + + + + org.keycloak + keycloak-server-spi-private + + + + org.keycloak + keycloak-services + + + + org.keycloak + keycloak-saml-core-public + + + + + + io.jans + jans-core-standalone + + + + io.jans + jans-core-service + + + + io.jans + jans-orm-standalone + + + + io.jans + jans-orm-couchbase + + + + io.jans + jans-orm-hybrid + + + + io.jans + jans-orm-ldap + + + + io.jans + jans-orm-sql + + + + + + org.jboss.slf4j + slf4j-jboss-logmanager + 2.0.1.Final + + + + + org.apache.commons + commons-dbcp2 + 2.12.0 + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + From c006ad2cf2e505af5e6f8748f0183b8b40bed53d Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Fri, 31 May 2024 14:58:42 +0100 Subject: [PATCH 10/33] feat(jans-keycloak-integration): janssen spi bundle #8614 * added dependencies xml Signed-off-by: Rolain Djeumen --- .../spi/src/assembly/dependencies.xml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/assembly/dependencies.xml diff --git a/jans-keycloak-integration/spi/src/assembly/dependencies.xml b/jans-keycloak-integration/spi/src/assembly/dependencies.xml new file mode 100644 index 00000000000..373f6f1d362 --- /dev/null +++ b/jans-keycloak-integration/spi/src/assembly/dependencies.xml @@ -0,0 +1,24 @@ + + deps + + zip + + false + + + target/deps/ + . + + *.jar + + + + src/main/resources/assembly + . + + *.DONOTDELETE + + + + \ No newline at end of file From 6e99863f5a8600951e27388fd3628fbf50cdac1d Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 03:02:41 +0100 Subject: [PATCH 11/33] feat(jans-keycloak-integration): enhancements to job-scheduler #8614 * added support for new protocol mapper in job scheduler * fixed typo in application shutdown log message Signed-off-by: Rolain Djeumen --- .../kc/api/admin/client/model/ProtocolMapper.java | 6 ++++++ .../src/main/java/io/jans/kc/scheduler/App.java | 2 +- .../kc/scheduler/TrustRelationshipSyncJob.java | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ProtocolMapper.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ProtocolMapper.java index e42c9e4c16b..f96d4a6213b 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ProtocolMapper.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ProtocolMapper.java @@ -125,6 +125,12 @@ public SamlUserAttributeMapperBuilder attributeNameFormatUnspecified() { return this; } + public SamlUserAttributeMapperBuilder jansAttributeName(final String attributename) { + + config.put("jans.attribute.name",attributename); + return this; + } + public ProtocolMapper build() { return this.mapper; diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index d2d9a207547..883b09a55ec 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -87,7 +87,7 @@ public static void main(String[] args) throws InterruptedException, ParserCreate Thread.sleep(1000); } } - log.info("Application shutthing down"); + log.info("Application shutting down"); }catch(StartupError e) { log.error("Application startup failed",e); if(jobScheduler != null) { diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java index a3890009c67..b93a139e183 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java @@ -212,14 +212,19 @@ private void addReleasedAttributesToManagedSamlClient(ManagedSamlClient client, List protmappers = releasedattributes.stream().map((r)-> { log.debug("Preparing to add released attribute {} to managed saml client with clientId {}",r.getName(),client.clientId()); - return ProtocolMapper + /*return ProtocolMapper .samlUserAttributeMapper(samlUserAttributeMapperId) .name(generateKeycloakUniqueProtocolMapperName(r)) .userAttribute(r.getName()) .friendlyName(r.getDisplayName()!=null?r.getDisplayName():r.getName()) .attributeName(r.getSaml2Uri()) .attributeNameFormatUriReference() - .build(); + .build(); */ + return ProtocolMapper + .samlUserAttributeMapper(samlUserAttributeMapperId) + .name(generateKeycloakUniqueProtocolMapperName(r)) + .jansAttributeName(r.getName()) + .build(); }).toList(); keycloakApi.addProtocolMappersToManagedSamlClient(realm, client, protmappers); @@ -228,12 +233,16 @@ private void addReleasedAttributesToManagedSamlClient(ManagedSamlClient client, private void updateManagedSamlClientProtocolMapper(ManagedSamlClient client, ProtocolMapper mapper, JansAttributeRepresentation releasedattribute) { log.debug("Updating managed client released attribute. Client id: {} / Attribute name: {}",client.clientId(),releasedattribute.getName()); - ProtocolMapper newmapper = ProtocolMapper + /*ProtocolMapper newmapper = ProtocolMapper .samlUserAttributeMapper(mapper) .userAttribute(releasedattribute.getName()) .friendlyName(releasedattribute.getDisplayName()!=null?releasedattribute.getDisplayName():releasedattribute.getName()) .attributeName(releasedattribute.getSaml2Uri()) .attributeNameFormatUriReference() + .build(); */ + ProtocolMapper newmapper = ProtocolMapper + .samlUserAttributeMapper(mapper) + .jansAttributeName(releasedattribute.getName()) .build(); keycloakApi.updateManagedSamlClientProtocolMapper(realm, client,newmapper); } From 2367adb985b0126c090a622d674c0b019148af36 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 03:36:04 +0100 Subject: [PATCH 12/33] feat(jans-keycloak-integration): keycloak integration enhancements #8614 * added support for the protocol-mapper in job-scheduler configuration * fixed issue in job-scheduler logging configuration that caused too many log files to be created Signed-off-by: Rolain Djeumen --- .../job-scheduler/src/main/resources/config.properties.sample | 2 +- .../job-scheduler/src/main/resources/logback.xml.sample | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/resources/config.properties.sample b/jans-keycloak-integration/job-scheduler/src/main/resources/config.properties.sample index 2f8ab122fc7..08bf2b4e404 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/resources/config.properties.sample +++ b/jans-keycloak-integration/job-scheduler/src/main/resources/config.properties.sample @@ -28,4 +28,4 @@ app.job.trustrelationship-sync.schedule-interval=PT10M # keycloak resources configuration app.keycloak.resources.realm=jans app.keycloak.resources.authn.browser.flow-alias=janssen login -app.keycloak.resources.saml.user-attribute-mapper=saml-user-attribute-mapper +app.keycloak.resources.saml.user-attribute-mapper=kc-jans-saml-user-attribute-mapper diff --git a/jans-keycloak-integration/job-scheduler/src/main/resources/logback.xml.sample b/jans-keycloak-integration/job-scheduler/src/main/resources/logback.xml.sample index 6a62328f644..9d0fc7b0cf3 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/resources/logback.xml.sample +++ b/jans-keycloak-integration/job-scheduler/src/main/resources/logback.xml.sample @@ -25,7 +25,7 @@ ${app.logdir}/scheduler.log true - ${app.logdir}/scheduler-%d{yyyy-mm-dd}.log.gz + ${app.logdir}/scheduler-%d{yyyy-MM-dd}.log.gz ${app.logging.loghistory:-180} From a839e127a803b482288f9c70e7254c73bd05c1cd Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 04:12:50 +0100 Subject: [PATCH 13/33] feat(jans-keycloak-integration): spi bundle #8614 * additions to the spi bundle pom file Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/spi/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jans-keycloak-integration/spi/pom.xml b/jans-keycloak-integration/spi/pom.xml index 5603b52c745..5fd6753db0b 100644 --- a/jans-keycloak-integration/spi/pom.xml +++ b/jans-keycloak-integration/spi/pom.xml @@ -95,6 +95,13 @@ commons-dbcp2 2.12.0 + + + + com.nimbusds + oauth2-oidc-sdk + + From 79d3255e719995bad24035a603dd7dc32d8b7a59 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 04:34:46 +0100 Subject: [PATCH 14/33] feat(jans-keycloak-integration): keycloak integration enhancements #8614 * added protocol mapper implementation Signed-off-by: Rolain Djeumen --- .../saml/JansSamlUserAttributeMapper.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java new file mode 100644 index 00000000000..89c636096a2 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java @@ -0,0 +1,139 @@ +package io.jans.kc.spi.protocol.mapper.saml; + +import io.jans.kc.model.JansUserAttributeModel; +import io.jans.kc.spi.ProviderIDs; +import io.jans.kc.spi.custom.JansThinBridgeOperationException; +import io.jans.kc.spi.custom.JansThinBridgeProvider; +import io.jans.model.GluuStatus; + +import java.util.List; + +import org.jboss.logging.Logger; + +import org.keycloak.Config; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; + +import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; +import org.keycloak.dom.saml.v2.assertion.AttributeType; + +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; +import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper; +import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; + +public class JansSamlUserAttributeMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { + + private static final String DISPLAY_TYPE = "Janssen User Attribute"; + private static final String DISPLAY_CATEGORY = AttributeStatementHelper.ATTRIBUTE_STATEMENT_CATEGORY; + private static final String HELP_TEXT = "Maps a Janssen User's Attribute to a SAML Attribute"; + + private static final String PROVIDER_ID = ProviderIDs.JANS_SAML_USER_ATTRIBUTE_MAPPER_PROVIDER; + //properties + private static final String JANS_ATTR_NAME_PROP_NAME = "jans.attribute.name"; + private static final String JANS_ATTR_NAME_PROP_LABEL = "Jans Attribute"; + private static final String JANS_ATTR_NAME_PROP_HELPTEXT = "Name of the Attribute in Janssen Auth Server"; + private static final List configProperties; + + private static final Logger log = Logger.getLogger(JansSamlUserAttributeMapper.class); + + static { + configProperties = ProviderConfigurationBuilder.create() + .property() + .name(JANS_ATTR_NAME_PROP_NAME) + .label(JANS_ATTR_NAME_PROP_LABEL) + .helpText(JANS_ATTR_NAME_PROP_HELPTEXT) + .type(ProviderConfigProperty.STRING_TYPE) + .defaultValue(null) + .required(true) + .add() + .build(); + } + + public JansSamlUserAttributeMapper() { + + + } + + @Override + public void init(Config.Scope scope) { + + } + + @Override + public void close() { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + + try { + final JansThinBridgeProvider jansThinBridge = session.getProvider(JansThinBridgeProvider.class); + final String attributeName = mappingModel.getConfig().get(JANS_ATTR_NAME_PROP_NAME); + final String loginUsername = userSession.getLoginUsername(); + final JansUserAttributeModel userAttribute = jansThinBridge.getUserAttribute(loginUsername,attributeName); + if(userAttribute == null) { + log.info("Could not find jans attribute information for user " + loginUsername); + return; + } + if(!userAttribute.isActive()) { + log.info("Jans attribute " + attributeName + " is not active"); + return; + } + AttributeType keycloakAttribute = userAttribute.asSamlKeycloakAttribute(); + if(keycloakAttribute == null) { + log.info("Could not convert jans attribute " + attributeName + " into a keycloak attribute"); + return; + } + attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(keycloakAttribute)); + }catch(JansThinBridgeOperationException e) { + log.error("Error mapping saml attribute from jans",e); + } + + } + + @Override + public List getConfigProperties() { + + return configProperties; + } + + @Override + public String getId() { + + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + + return DISPLAY_TYPE; + } + + @Override + public String getDisplayCategory() { + + return DISPLAY_CATEGORY; + } + + @Override + public String getHelpText() { + + return HELP_TEXT; + } + + +} From 76e79f3c45e60d1285d4cfb15564c8e6d94bfab8 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 07:37:08 +0100 Subject: [PATCH 15/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * added thin bridge spi provider * added models for thin bridge provider Signed-off-by: Rolain Djeumen --- .../jans/kc/model/JansUserAttributeModel.java | 56 +++ .../java/io/jans/kc/model/JansUserModel.java | 323 ++++++++++++++++++ .../io/jans/kc/model/internal/JansPerson.java | 124 +++++++ .../custom/JansThinBridgeInitException.java | 12 + .../JansThinBridgeOperationException.java | 13 + .../kc/spi/custom/JansThinBridgeProvider.java | 14 + .../custom/JansThinBridgeProviderFactory.java | 8 + .../jans/kc/spi/custom/JansThinBridgeSpi.java | 31 ++ .../impl/DefaultJansThinBridgeProvider.java | 142 ++++++++ .../DefaultJansThinBridgeProviderFactory.java | 161 +++++++++ 10 files changed, 884 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeInitException.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeOperationException.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProviderFactory.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeSpi.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java new file mode 100644 index 00000000000..cad9a2c2545 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java @@ -0,0 +1,56 @@ +package io.jans.kc.model; + +import io.jans.kc.model.internal.JansPerson; +import io.jans.model.GluuStatus; +import io.jans.model.JansAttribute; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; +import org.keycloak.dom.saml.v2.assertion.AttributeType; + +import org.keycloak.saml.common.constants.JBossSAMLURIConstants; + +public class JansUserAttributeModel { + + private final JansAttribute jansAttribute; + private final JansPerson jansPerson; + + public JansUserAttributeModel(final JansAttribute jansAttribute, final JansPerson jansPerson) { + + this.jansAttribute = jansAttribute; + this.jansPerson = jansPerson; + } + + public boolean isActive() { + + return jansAttribute.getStatus() == GluuStatus.ACTIVE; + } + + public AttributeType asSamlKeycloakAttribute() { + + List values = jansPerson.customAttributeValues(jansAttribute.getName()); + if(values == null) { + + return null; + } + String samlAttributeName = jansAttribute.getSaml2Uri(); + String samlAttributeNameFormat = JBossSAMLURIConstants.ATTRIBUTE_FORMAT_URI.get(); + if(StringUtils.isEmpty(samlAttributeName)) { + samlAttributeName = jansAttribute.getName(); + samlAttributeNameFormat = JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get(); + } + AttributeType ret = new AttributeType(samlAttributeName); + ret.setNameFormat(samlAttributeNameFormat); + if(!StringUtils.isEmpty(jansAttribute.getDisplayName())) { + ret.setFriendlyName(jansAttribute.getDisplayName()); + }else { + ret.setFriendlyName(jansAttribute.getName()); + } + + values.forEach(ret::addAttributeValue); + return ret; + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java new file mode 100644 index 00000000000..b455a171a6f --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java @@ -0,0 +1,323 @@ +package io.jans.kc.model; + +import io.jans.kc.model.internal.JansPerson; +import io.jans.orm.model.base.CustomObjectAttribute; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.SubjectCredentialManager; +import org.keycloak.storage.ReadOnlyException; + +public class JansUserModel implements UserModel { + + private static final String INUM_ATTR_NAME = "inum"; + private static final String UID_ATTR_NAME = "uid"; + private static final String JANS_CREATION_TIMESTAMP_ATTR_NAME = "jansCreationTimestamp"; + private static final String JANS_STATUS_ATTR_NAME = "jansStatus"; + private static final String GIVEN_NAME_ATTR_NAME = "givenName"; + private static final String MAIL_ATTR_NAME = "mail"; + private static final String EMAIL_VERIFIED_ATTR_NAME = "emailVerified"; + private final JansPerson jansPerson; + + public JansUserModel(final JansPerson jansPerson) { + + this.jansPerson = jansPerson; + } + + @Override + public String getId() { + + return jansPerson.customAttributeValue(INUM_ATTR_NAME); + } + + @Override + public String getUsername() { + + return jansPerson.customAttributeValue(UID_ATTR_NAME); + } + + @Override + public void setUsername(String username) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public Long getCreatedTimestamp() { + + try { + final String createdStr = jansPerson.customAttributeValue(JANS_CREATION_TIMESTAMP_ATTR_NAME); + if(createdStr == null) { + return null; + } + return Long.parseLong(createdStr); + }catch(NumberFormatException e) { + return null; + } + } + + @Override + public void setCreatedTimestamp(Long timestamp) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public boolean isEnabled() { + + final String enabledStr = jansPerson.customAttributeValue(JANS_STATUS_ATTR_NAME); + if(enabledStr == null) { + return false; + } + if("active".equals(enabledStr)) { + return true; + } + return false; + } + + @Override + public void setEnabled(boolean enabled) { + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public void setSingleAttribute(String name, String value) { + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public void setAttribute(String name, List value) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public void removeAttribute(String name) { + throw new ReadOnlyException("User is read-only for this update"); + } + + + @Override + public String getFirstAttribute(String name) { + + if(USERNAME.equals(name)) { + return getUsername(); + }else if(FIRST_NAME.equals(name)) { + return getFirstName(); + }else if(EMAIL.equals(name)) { + return getEmail(); + }else { + return jansPerson.customAttributeValue(name); + } + } + + @Override + public Stream getAttributeStream(final String name) { + + List ret = new ArrayList<>(); + + if(USERNAME.equals(name)) { + ret.add(getUsername()); + }else if(FIRST_NAME.equals(name)) { + ret.add(getFirstName()); + }else if(EMAIL.equals(name)) { + ret.add(getEmail()); + }else { + return jansPerson.customAttributeValues(name).stream(); + } + return ret.stream(); + } + + @Override + public Map> getAttributes() { + + Map> ret = new HashMap<>(); + for(String attrName : jansPerson.customAttributeNames()) { + ret.put(attrName,jansPerson.customAttributeValues(attrName)); + } + return ret; + } + + @Override + public Stream getRequiredActionsStream() { + + return new ArrayList().stream(); + } + + @Override + public void addRequiredAction(String action) { + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public void removeRequiredAction(String action) { + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public String getFirstName() { + + return jansPerson.customAttributeValue(GIVEN_NAME_ATTR_NAME); + } + + @Override + public void setFirstName(String firstName) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public String getLastName() { + + return null; + } + + @Override + public void setLastName(String lastName) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public String getEmail() { + + return jansPerson.customAttributeValue(MAIL_ATTR_NAME); + } + + @Override + public void setEmail(final String email) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public boolean isEmailVerified() { + + try { + final String attr = jansPerson.customAttributeValue(EMAIL_VERIFIED_ATTR_NAME); + if(attr == null) { + return false; + } + return Boolean.parseBoolean(attr); + }catch(NumberFormatException e) { + return false; + } + } + + @Override + public void setEmailVerified(boolean verified) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public Stream getGroupsStream() { + + return new ArrayList().stream(); + } + + @Override + public long getGroupsCount() { + + return 0; + } + + @Override + public long getGroupsCountByNameContaining(String search) { + + return 0; + } + + @Override + public void joinGroup(GroupModel group) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public void leaveGroup(GroupModel group) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public boolean isMemberOf(GroupModel group) { + + return false; + } + + @Override + public String getFederationLink() { + + return null; + } + + @Override + public void setFederationLink(String link) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public String getServiceAccountClientLink() { + + return null; + } + + @Override + public void setServiceAccountClientLink(String clientInternalId) { + + throw new ReadOnlyException("User is read-only for this update"); + } + + @Override + public SubjectCredentialManager credentialManager() { + + return null; + } + + @Override + public Stream getRealmRoleMappingsStream() { + + return new ArrayList().stream(); + } + + @Override + public Stream getClientRoleMappingsStream(ClientModel app) { + + return new ArrayList().stream(); + } + + @Override + public boolean hasRole(RoleModel role) { + + return false; + } + + @Override + public void grantRole(RoleModel role) { + + throw new ReadOnlyException("User is in read-only for this update"); + } + + @Override + public Stream getRoleMappingsStream() { + + return new ArrayList().stream(); + } + + @Override + public void deleteRoleMapping(RoleModel role) { + + throw new ReadOnlyException("User is in read-only for this update"); + } + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java new file mode 100644 index 00000000000..86df86812c7 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java @@ -0,0 +1,124 @@ +package io.jans.kc.model.internal; + +import java.io.Serializable; + +import java.util.ArrayList; +import java.util.List; + +import io.jans.model.JansAttribute; +import io.jans.orm.annotation.*; +import io.jans.orm.model.base.CustomObjectAttribute; + +@DataEntry +@ObjectClass(value="jansPerson") +public class JansPerson implements Serializable { + + private static final long serialVersionUID = -1L; + + @DN + private String dn; + + @AttributesList(name="name",value="values",multiValued="multiValued") + private List customAttributes = new ArrayList<>(); + + + public JansPerson() { + + } + + public String getDn() { + + return this.dn; + } + + public void setDn(final String dn) { + + this.dn = dn; + } + + public void setCustomAttributes(List customAttributes) { + + this.customAttributes = customAttributes; + } + + public List getCustomAttributes() { + + return this.customAttributes; + } + + public boolean hasCustomAttributes() { + + return (this.customAttributes != null && !this.customAttributes.isEmpty()); + } + + public boolean isMultiValuedCustomAttributes() { + + return hasCustomAttributes() && this.customAttributes.size() > 1; + } + + public CustomObjectAttribute getCustomObjectAttribute(final String name) { + + for(CustomObjectAttribute customAttribute : customAttributes) { + if(customAttribute.getName().equals(name)) { + return customAttribute; + } + } + return null; + } + + public List customAttributeValues(final String name) { + + + for(CustomObjectAttribute customAttribute : customAttributes) { + if(customAttribute.getName().equals(name)) { + List values = customAttribute.getValues(); + if(values == null || values.size() == 0) { + return new ArrayList<>(); + } + return convertToString(values); + } + } + return new ArrayList<>(); + } + + public List customAttributeNames() { + + List ret = new ArrayList<>(); + for(CustomObjectAttribute customAttribute : customAttributes) { + ret.add(customAttribute.getName()); + } + return ret; + } + + public String customAttributeValue(final String attributeName) { + + for(CustomObjectAttribute customAttribute : customAttributes) { + if(customAttribute.getName().equals(attributeName)) { + List values = customAttribute.getValues(); + if(values == null || values.size() == 0) { + return null; + } + List ret = convertToString(values); + if(ret.isEmpty()) { + return null; + } + return ret.get(0); + } + } + + return null; + } + + private List convertToString(List values) { + + List ret = new ArrayList<>(); + for(Object val : values) { + if(val instanceof String) { + ret.add((String) val); + }else { + ret.add(val.toString()); + } + } + return ret; + } +} \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeInitException.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeInitException.java new file mode 100644 index 00000000000..b533077c411 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeInitException.java @@ -0,0 +1,12 @@ +package io.jans.kc.spi.custom; + +public class JansThinBridgeInitException extends RuntimeException { + + public JansThinBridgeInitException(final String msg) { + super(msg); + } + + public JansThinBridgeInitException(final String msg, Throwable cause) { + super(msg,cause); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeOperationException.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeOperationException.java new file mode 100644 index 00000000000..7a550dc68b5 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeOperationException.java @@ -0,0 +1,13 @@ +package io.jans.kc.spi.custom; + + +public class JansThinBridgeOperationException extends RuntimeException { + + public JansThinBridgeOperationException(final String msg) { + super(msg); + } + + public JansThinBridgeOperationException(final String msg, Throwable cause) { + super(msg,cause); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java new file mode 100644 index 00000000000..f95236da2a5 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java @@ -0,0 +1,14 @@ +package io.jans.kc.spi.custom; + +import org.keycloak.provider.*; +import io.jans.kc.model.JansUserModel; +import io.jans.kc.model.JansUserAttributeModel; + + +public interface JansThinBridgeProvider extends Provider { + + JansUserAttributeModel getUserAttribute(final String kcLoginUsername, final String attributeName); + JansUserModel getUserByUsername(final String username); + JansUserModel getUserByEmail(final String email); + JansUserModel getUserByInum(final String inum); +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProviderFactory.java new file mode 100644 index 00000000000..accf3b20de2 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProviderFactory.java @@ -0,0 +1,8 @@ +package io.jans.kc.spi.custom; + +import org.keycloak.provider.*; + +public interface JansThinBridgeProviderFactory extends ProviderFactory { + + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeSpi.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeSpi.java new file mode 100644 index 00000000000..67b3a471ffa --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeSpi.java @@ -0,0 +1,31 @@ +package io.jans.kc.spi.custom; + +import org.keycloak.provider.*; + +public class JansThinBridgeSpi implements Spi { + + private static final String SPI_NAME = "kc-jans-thin-bridge"; + @Override + public boolean isInternal() { + + return false; + } + + @Override + public String getName() { + + return SPI_NAME; + } + + @Override + public Class getProviderClass() { + + return JansThinBridgeProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + + return JansThinBridgeProviderFactory.class; + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java new file mode 100644 index 00000000000..929fb27d4b6 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java @@ -0,0 +1,142 @@ +package io.jans.kc.spi.custom.impl; + +import java.util.List; + +import io.jans.kc.model.JansUserAttributeModel; +import io.jans.kc.model.JansUserModel; +import io.jans.kc.model.internal.JansPerson; +import io.jans.kc.spi.custom.JansThinBridgeProvider; +import io.jans.kc.spi.custom.JansThinBridgeInitException; +import io.jans.kc.spi.custom.JansThinBridgeOperationException; + +import io.jans.model.JansAttribute; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.search.filter.Filter; + +import org.jboss.logging.Logger; + + +public class DefaultJansThinBridgeProvider implements JansThinBridgeProvider { + + private static final String JANS_ATTRIBUTES_ROOT_DN = "ou=attributes,o=jans"; + private static final String JANS_PEOPLE_ROOT_DN = "ou=people,o=jans"; + private static final String UID_ATTR_NAME = "uid"; + private static final String MAIL_ATTR_NAME = "mail"; + private static final String INUM_ATTR_NAME = "inum"; + private static final Logger log = Logger.getLogger(DefaultJansThinBridgeProvider.class); + private static final String [] defaultUserReturnAttributes = new String [] { + "uid","mail","displayName","givenName","inum","sn", "cn", + "jansCreationTimestamp", "jansLastLogonTime","updatedAt", "jansStatus" + }; + + private final PersistenceEntryManager persistenceEntryManager; + + public DefaultJansThinBridgeProvider(final PersistenceEntryManager persistenceEntryManager) { + + this.persistenceEntryManager = persistenceEntryManager; + } + + @Override + public void close() { + + } + + @Override + public JansUserAttributeModel getUserAttribute(final String kcLoginUsername, final String attributeName) { + + try { + + String [] jansAttrReturnAttributes = new String [] { + "displayName","jansAttrTyp","jansClaimName", + "jansSAML1URI","jansSAML2URI","jansStatus", "jansAttrName" + }; + + final JansAttribute jansAttr = findAttributeByName(attributeName,jansAttrReturnAttributes); + if(jansAttr == null) { + return null; + } + + String [] jansPersonReturnAttributes = new String [] { + attributeName + }; + final JansPerson jansPerson = findPersonByKcLoginUsername(kcLoginUsername, jansPersonReturnAttributes); + if(jansPerson == null) { + return null; + } + + return new JansUserAttributeModel(jansAttr,jansPerson); + + }catch(Exception e) { + throw new JansThinBridgeOperationException("Could not get attributes for user " + kcLoginUsername,e); + } + } + + @Override + public JansUserModel getUserByUsername(final String username) { + + try { + final Filter uidSearchFilter = Filter.createEqualityFilter(UID_ATTR_NAME,username); + final JansPerson person = findPerson(uidSearchFilter,defaultUserReturnAttributes); + if(person == null) { + return null; + } + return new JansUserModel(person); + }catch(Exception e) { + throw new JansThinBridgeOperationException("Error fetching jans user with username " + username,e); + } + } + + @Override + public JansUserModel getUserByEmail(final String email) { + + try { + final Filter mailSearchFilter = Filter.createEqualityFilter(MAIL_ATTR_NAME, email); + final JansPerson person = findPerson(mailSearchFilter,defaultUserReturnAttributes); + if(person == null) { + return null; + } + return new JansUserModel(person); + }catch(Exception e) { + throw new JansThinBridgeOperationException("Error fetching jans user with email " + email ,e); + } + } + + @Override + public JansUserModel getUserByInum(final String inum) { + + try { + final Filter inumSearchFilter = Filter.createEqualityFilter(INUM_ATTR_NAME,inum); + final JansPerson person = findPerson(inumSearchFilter,defaultUserReturnAttributes); + if(person == null) { + return null; + } + return new JansUserModel(person); + }catch(Exception e) { + throw new JansThinBridgeOperationException("Error fetching jans user with inum "+inum,e); + } + } + + private JansAttribute findAttributeByName(final String attributeName, final String [] returnAttributes) { + + final Filter searchFilter = Filter.createEqualityFilter("jansAttrName", attributeName); + List searchresult = persistenceEntryManager.findEntries(JANS_ATTRIBUTES_ROOT_DN, + JansAttribute.class,searchFilter,returnAttributes); + + return (searchresult.isEmpty() ? null: searchresult.get(0)); + } + + private JansPerson findPersonByKcLoginUsername(final String kcLoginUsername,final String [] returnAttributes) { + + final Filter uidSearchFilter = Filter.createEqualityFilter(UID_ATTR_NAME,kcLoginUsername); + final Filter mailSearchFilter = Filter.createEqualityFilter(MAIL_ATTR_NAME,kcLoginUsername); + final Filter searchFilter = Filter.createORFilter(uidSearchFilter,mailSearchFilter); + + return findPerson(searchFilter,returnAttributes); + } + + private JansPerson findPerson(final Filter searchFilter, final String [] returnAttributes) { + + List searchresult = persistenceEntryManager.findEntries(JANS_PEOPLE_ROOT_DN,JansPerson.class,searchFilter,returnAttributes); + return (searchresult.isEmpty() ? null: searchresult.get(0)); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java new file mode 100644 index 00000000000..ece1c65ac53 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java @@ -0,0 +1,161 @@ +package io.jans.kc.spi.custom.impl; + + + +import io.jans.kc.spi.custom.JansThinBridgeProvider; +import io.jans.kc.spi.custom.JansThinBridgeProviderFactory; +import io.jans.kc.spi.ProviderIDs; +import io.jans.kc.spi.custom.JansThinBridgeInitException; +import io.jans.kc.spi.custom.JansThinBridgeInitException; +import io.jans.orm.model.PersistenceConfiguration; +import io.jans.orm.PersistenceEntryManager; +import io.jans.orm.PersistenceEntryManagerFactory; +import io.jans.orm.service.PersistanceFactoryService; +import io.jans.orm.service.StandalonePersistanceFactoryService; +import io.jans.orm.util.properties.FileConfiguration; +import io.jans.util.security.PropertiesDecrypter; +import io.jans.util.security.StringEncrypter; + +import java.io.File; +import java.util.Properties; + +import org.apache.commons.lang.StringUtils; + +import org.jboss.logging.Logger; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + + + +public class DefaultJansThinBridgeProviderFactory implements JansThinBridgeProviderFactory { + + + private static final Logger log = Logger.getLogger(DefaultJansThinBridgeProviderFactory.class); + private static final String DEFAULT_CONFIG_FILENAME = "jans.properties"; + private static final String SALT_FILENAME = "salt"; + private static final String PROVIDER_ID = ProviderIDs.JANS_DEFAULT_THIN_BRIDGE_PROVIDER; + + private final PersistanceFactoryService persistenceFactoryService; + private final PersistenceConfiguration persistenceConfiguration; + private final PersistenceEntryManager persistenceEntryManager; + + + + public DefaultJansThinBridgeProviderFactory() { + + log.info("Establishing connection with janssen database"); + persistenceFactoryService = new StandalonePersistanceFactoryService(); + persistenceConfiguration = persistenceFactoryService.loadPersistenceConfiguration(DEFAULT_CONFIG_FILENAME); + if(persistenceConfiguration == null) { + throw new JansThinBridgeInitException("Failed to load persistence configuration from file. " + + "\n+ Jans configuration base directory: " + getJansConfigurationBaseDir() + + "\n+ Jans configuration default file: " + DEFAULT_CONFIG_FILENAME); + } + + String confdir = persistenceConfiguration.getConfiguration().getString("confDir"); + if(!StringUtils.isNotBlank(confdir)) { + confdir = getJansConfigurationBaseDir() + File.separator + "conf" + File.separator; + } + final String salt = cryptographicSaltFromFile(confdir + SALT_FILENAME); + if(!StringUtils.isNotBlank(salt)) { + throw new JansThinBridgeInitException("Failed to load cryptographic material from configuration"); + } + persistenceEntryManager = createPersistenceEntryManager(persistenceConfiguration, persistenceFactoryService, salt); + log.info("Connection established to janssen database"); + } + + @Override + public JansThinBridgeProvider create(KeycloakSession session) { + + return new DefaultJansThinBridgeProvider(persistenceEntryManager); + } + + + @Override + public void init(Config.Scope config) { + + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + + } + + @Override + public String getId() { + + return PROVIDER_ID; + } + + + + private final FileConfiguration loadBaseConfiguration(final String filename) { + + try { + return new FileConfiguration(filename); + }catch(Exception e) { + log.errorv(e,"Failed to load configuration from {0}",filename); + final String errordesc = (filename != null ? " from file " + filename : ". Invalid filename specified"); + throw new JansThinBridgeInitException("Failed to load configuration" + errordesc); + } + } + + private final StringEncrypter createStringEncrypterFromSaltFile(final String path) { + + try { + final String salt = cryptographicSaltFromFile(path); + if(StringUtils.isEmpty(salt)) { + return null; + } + return StringEncrypter.instance(salt); + }catch(StringEncrypter.EncryptionException e) { + throw new JansThinBridgeInitException("Failed to create string encrypted",e); + } + } + + private final String cryptographicSaltFromFile(final String path) { + + FileConfiguration cryptoconfig = new FileConfiguration(path); + return cryptoconfig.getString("encodeSalt"); + } + + private final Properties preparePersistenceProperties(final PersistenceConfiguration persistenceConfiguration, final String salt) { + + try { + FileConfiguration config = persistenceConfiguration.getConfiguration(); + Properties connprops = (Properties) config.getProperties(); + return PropertiesDecrypter.decryptAllProperties(StringEncrypter.defaultInstance(),connprops,salt); + }catch(StringEncrypter.EncryptionException e) { + throw new JansThinBridgeInitException("Failed to decrypt persistence connection parameters",e); + } + } + + private final PersistenceEntryManager createPersistenceEntryManager(final PersistenceConfiguration config, + final PersistanceFactoryService persistenceFactoryService, final String salt) { + + Properties persistconnprops = preparePersistenceProperties(config,salt); + PersistenceEntryManagerFactory peManagerFactory = persistenceFactoryService.getPersistenceEntryManagerFactory(config); + return peManagerFactory.createEntryManager(persistconnprops); + } + + private final String getJansConfigurationBaseDir() { + + if( System.getProperty("jans.base") !=null ) { + return System.getProperty("jans.base"); + }else if( (System.getProperty("catalina.base") != null) && (System.getProperty("catalina.base.ignore") == null) ) { + return System.getProperty("catalina.base"); + }else if( System.getProperty("catalina.home") != null ) { + return System.getProperty("catalina.home"); + } + return null; + } +} From 4f453aba2366746f40a9285d081f8a3de50e6b89 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 08:10:34 +0100 Subject: [PATCH 16/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * moved authenticator spi to spi module * minor refactoring to the authenticator spi Signed-off-by: Rolain Djeumen --- .../java/io/jans/kc/oidc/OIDCAccessToken.java | 5 + .../java/io/jans/kc/oidc/OIDCAuthRequest.java | 86 ++++ .../java/io/jans/kc/oidc/OIDCMetaCache.java | 6 + .../io/jans/kc/oidc/OIDCMetaCacheKeys.java | 7 + .../java/io/jans/kc/oidc/OIDCMetaError.java | 12 + .../io/jans/kc/oidc/OIDCRefreshToken.java | 5 + .../java/io/jans/kc/oidc/OIDCService.java | 13 + .../java/io/jans/kc/oidc/OIDCTokenError.java | 30 ++ .../io/jans/kc/oidc/OIDCTokenRequest.java | 40 ++ .../jans/kc/oidc/OIDCTokenRequestError.java | 12 + .../io/jans/kc/oidc/OIDCTokenResponse.java | 9 + .../io/jans/kc/oidc/OIDCUserInfoError.java | 30 ++ .../kc/oidc/OIDCUserInfoRequestError.java | 13 + .../io/jans/kc/oidc/OIDCUserInfoResponse.java | 9 + .../kc/oidc/impl/HashBasedOIDCMetaCache.java | 139 ++++++ .../kc/oidc/impl/NimbusOIDCAccessToken.java | 20 + .../kc/oidc/impl/NimbusOIDCRefreshToken.java | 15 + .../jans/kc/oidc/impl/NimbusOIDCService.java | 222 +++++++++ .../kc/oidc/impl/NimbusOIDCTokenResponse.java | 56 +++ .../oidc/impl/NimbusOIDCUserInfoResponse.java | 47 ++ .../jans/kc/spi/auth/JansAuthenticator.java | 436 ++++++++++++++++++ .../spi/auth/JansAuthenticatorConfigProp.java | 75 +++ .../kc/spi/auth/JansAuthenticatorFactory.java | 117 +++++ .../jans/kc/spi/auth/SessionAttributes.java | 10 + 24 files changed, 1414 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAccessToken.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCache.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaError.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCRefreshToken.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCService.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenError.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequest.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequestError.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenResponse.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoError.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoRequestError.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoResponse.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCAccessToken.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCUserInfoResponse.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAccessToken.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAccessToken.java new file mode 100644 index 00000000000..4d2646ca476 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAccessToken.java @@ -0,0 +1,5 @@ +package io.jans.kc.oidc; + +public interface OIDCAccessToken { + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java new file mode 100644 index 00000000000..5a5b49b7cbd --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java @@ -0,0 +1,86 @@ +package io.jans.kc.oidc; + +import java.util.List; +import java.util.ArrayList; + +public class OIDCAuthRequest { + + private String clientId; + private String state; + private String nonce; + private List scopes; + private List responseTypes; + private String redirectUri; + + public OIDCAuthRequest() { + + this.clientId = null; + this.state = null; + this.nonce = null; + this.scopes = new ArrayList(); + this.responseTypes = new ArrayList(); + this.redirectUri = null; + } + + public String getClientId() { + + return this.clientId; + } + + public void setClientId(String clientId) { + + this.clientId = clientId; + } + + public void setState(String state) { + + this.state = state; + } + + public final String getState() { + + return this.state; + } + + public void setNonce(String nonce) { + + this.nonce = nonce; + } + + public final String getNonce() { + + return this.nonce; + } + + public void addScope(String scope) { + + this.scopes.add(scope); + } + + public final List getScopes() { + + return this.scopes; + } + + public void addResponseType(String responseType) { + + this.responseTypes.add(responseType); + } + + public final List getResponseTypes() { + + return this.responseTypes; + } + + + public void setRedirectUri(String redirectUri) { + + this.redirectUri = redirectUri; + } + + public String getRedirectUri() { + + return this.redirectUri; + } + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCache.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCache.java new file mode 100644 index 00000000000..01d209f1e0a --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCache.java @@ -0,0 +1,6 @@ +package io.jans.kc.oidc; + +public interface OIDCMetaCache { + public void put(String issuer, String key , Object value); + public Object get(String issuer, String key); +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java new file mode 100644 index 00000000000..3a64b29997d --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java @@ -0,0 +1,7 @@ +package io.jans.kc.oidc; + +public class OIDCMetaCacheKeys { + public static final String AUTHORIZATION_URL = "oidc.authorization.url"; + public static final String TOKEN_URL = "oidc.token.url"; + public static final String USERINFO_URL = "oidc.userinfo.url"; +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaError.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaError.java new file mode 100644 index 00000000000..1c7de99302d --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaError.java @@ -0,0 +1,12 @@ +package io.jans.kc.oidc; + +public class OIDCMetaError extends Exception { + + public OIDCMetaError(String message) { + super(message); + } + + public OIDCMetaError(String message, Throwable cause) { + super(message,cause); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCRefreshToken.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCRefreshToken.java new file mode 100644 index 00000000000..740d2c602b4 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCRefreshToken.java @@ -0,0 +1,5 @@ +package io.jans.kc.oidc; + +public interface OIDCRefreshToken { + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCService.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCService.java new file mode 100644 index 00000000000..1eefabddd03 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCService.java @@ -0,0 +1,13 @@ +package io.jans.kc.oidc; + +import java.net.URI; + +public interface OIDCService { + + public URI getAuthorizationEndpoint(String issuerUrl) throws OIDCMetaError; + public URI getTokenEndpoint(String issuerUrl) throws OIDCMetaError; + public URI getUserInfoEndpoint(String issuerUrl) throws OIDCMetaError; + public URI createAuthorizationUrl(String issuerUrl, OIDCAuthRequest request) throws OIDCMetaError; + public OIDCTokenResponse requestTokens(String issuerUrl, OIDCTokenRequest tokenreq) throws OIDCTokenRequestError; + public OIDCUserInfoResponse requestUserInfo(String issuerUrl, OIDCAccessToken accesstoken) throws OIDCUserInfoRequestError; +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenError.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenError.java new file mode 100644 index 00000000000..491b28918df --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenError.java @@ -0,0 +1,30 @@ +package io.jans.kc.oidc; + +public class OIDCTokenError { + + private String code; + private String description; + private int httpStatusCode; + + public OIDCTokenError(String code, String description, int httpStatusCode) { + + this.code = code; + this.description = description; + this.httpStatusCode = httpStatusCode; + } + + public String code() { + + return this.code; + } + + public String description() { + + return this.description; + } + + public int httpStatusCode() { + + return this.httpStatusCode; + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequest.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequest.java new file mode 100644 index 00000000000..15ee313053c --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequest.java @@ -0,0 +1,40 @@ +package io.jans.kc.oidc; + +import java.net.URI; + +public class OIDCTokenRequest { + + private String code; + //in the future , replace this with a client credentials + //interface to support various authntication credential schemes + private String clientId; + private String clientSecret; + private URI redirecturi; + + public OIDCTokenRequest(String code, String clientId,String clientSecret,URI redirecturi) { + this.code = code; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirecturi = redirecturi; + } + + public String getCode() { + + return this.code; + } + + public String getClientId() { + + return this.clientId; + } + + public String getClientSecret() { + + return this.clientSecret; + } + + public URI getRedirectUri() { + + return this.redirecturi; + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequestError.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequestError.java new file mode 100644 index 00000000000..836ff580eed --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenRequestError.java @@ -0,0 +1,12 @@ +package io.jans.kc.oidc; + +public class OIDCTokenRequestError extends Exception { + + public OIDCTokenRequestError(String msg) { + super(msg); + } + + public OIDCTokenRequestError(String msg, Throwable cause) { + super(msg,cause); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenResponse.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenResponse.java new file mode 100644 index 00000000000..d71d97d86e2 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCTokenResponse.java @@ -0,0 +1,9 @@ +package io.jans.kc.oidc; + +public interface OIDCTokenResponse { + + public OIDCAccessToken accessToken(); + public OIDCRefreshToken refreshToken(); + public OIDCTokenError error(); + public boolean indicatesSuccess(); +} \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoError.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoError.java new file mode 100644 index 00000000000..f6afbc738dd --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoError.java @@ -0,0 +1,30 @@ +package io.jans.kc.oidc; + +public class OIDCUserInfoError { + + private String code; + private String description; + private int httpStatusCode; + + public OIDCUserInfoError(String code, String description, int httpStatusCode) { + + this.code = code; + this.description = description; + this.httpStatusCode = httpStatusCode; + } + + public String code() { + + return this.code; + } + + public String description() { + + return this.description; + } + + public int httpStatusCode() { + + return this.httpStatusCode; + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoRequestError.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoRequestError.java new file mode 100644 index 00000000000..e234cb6723a --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoRequestError.java @@ -0,0 +1,13 @@ +package io.jans.kc.oidc; + +public class OIDCUserInfoRequestError extends Exception { + + public OIDCUserInfoRequestError(String msg) { + super(msg); + } + + + public OIDCUserInfoRequestError(String msg, Throwable cause) { + super(msg,cause); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoResponse.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoResponse.java new file mode 100644 index 00000000000..f8778b8e637 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCUserInfoResponse.java @@ -0,0 +1,9 @@ +package io.jans.kc.oidc; + +public interface OIDCUserInfoResponse { + + public String username(); + public String email(); + public boolean indicatesSuccess(); + public OIDCUserInfoError error(); +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java new file mode 100644 index 00000000000..bf056a3e56e --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java @@ -0,0 +1,139 @@ +package io.jans.kc.oidc.impl; + +import java.util.Map; + +import org.jboss.logging.Logger; + +import io.jans.kc.oidc.OIDCMetaCache; + +import java.util.HashMap; + +public class HashBasedOIDCMetaCache implements OIDCMetaCache{ + + private static final Logger log = Logger.getLogger(HashBasedOIDCMetaCache.class); + private static final long DEFAULT_CACHE_TTL = 20*60; // 20 seconds + + private long cacheEntryTtl; + + private Map> cacheEntries; + + public HashBasedOIDCMetaCache() { + this(DEFAULT_CACHE_TTL); + } + + public HashBasedOIDCMetaCache(long cacheEntryTtl) { + + this.cacheEntryTtl = cacheEntryTtl; + if(this.cacheEntryTtl == 0) { + this.cacheEntryTtl = DEFAULT_CACHE_TTL; + } + this.cacheEntryTtl = this.cacheEntryTtl * 1000; // convert to milliseconds + this.cacheEntries = new HashMap>(); + } + + @Override + public void put(String issuer, String key, Object value) { + + synchronized(cacheEntries) { + createIfNotExistIssuerCacheEntry(issuer); + addIssuerCacheEntry(issuer,key,value); + performHouseCleaning(); + } + } + + @Override + public Object get(String issuer, String key) { + + synchronized(cacheEntries) { + if(issuerCacheEntryIsMissing(issuer)) { + performHouseCleaning(); + return null; + } + + Object ret = getIssuerCacheEntryValue(issuer, key); + performHouseCleaning(); + return ret; + } + } + + private boolean issuerCacheEntryIsMissing(String issuer) { + + return cacheEntries.get(issuer) == null; + } + + private Object getIssuerCacheEntryValue(String issuer, String key) { + + Map issuer_cache = cacheEntries.get(issuer); + return issuer_cache.get(key).getValue(); + } + + private void createIfNotExistIssuerCacheEntry(String issuer) { + + if(!cacheEntries.containsKey(issuer)) { + cacheEntries.put(issuer,new HashMap()); + } + } + + private void addIssuerCacheEntry(String issuer,String key, Object value) { + + Map issuerCache = cacheEntries.get(issuer); + for(String existingkey : issuerCache.keySet()) { + if(existingkey.equalsIgnoreCase(key)) { + //update cache entry + CacheEntry cache_entry = issuerCache.get(existingkey); + cache_entry.updateValue(value); + return; + } + } + + issuerCache.put(key,new CacheEntry(cacheEntryTtl, value)); + } + + private void performHouseCleaning() { + + for(String issuer: cacheEntries.keySet()) { + Map issuer_cache = cacheEntries.get(issuer); + for(String key :issuer_cache.keySet()) { + CacheEntry cache_entry = issuer_cache.get(key); + if(cache_entry.isExpired()) { + issuer_cache.remove(key); + } + } + } + } + + private class CacheEntry { + + private long updateTime; + private long ttl; + private Object value; + + public CacheEntry(long ttl, Object value) { + + this.updateTime = System.currentTimeMillis(); + this.ttl = ttl; + this.value = value; + } + + public Object getValue() { + + return this.value; + } + + public boolean isExpired() { + + return (System.currentTimeMillis() - this.updateTime) > (this.ttl * 1000) ; + } + + public void updateValue(Object value) { + + this.value = value; + this.updateTime = System.currentTimeMillis(); + } + + public void refresh() { + + this.updateTime = System.currentTimeMillis(); + } + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCAccessToken.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCAccessToken.java new file mode 100644 index 00000000000..4192bf4cfb8 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCAccessToken.java @@ -0,0 +1,20 @@ +package io.jans.kc.oidc.impl; + +import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; + +import io.jans.kc.oidc.OIDCAccessToken; + +public class NimbusOIDCAccessToken implements OIDCAccessToken { + + private AccessToken accessToken; + + public NimbusOIDCAccessToken(AccessToken accessToken) { + this.accessToken = accessToken; + } + + public BearerAccessToken asBearerToken() { + + return new BearerAccessToken(accessToken.getValue()); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java new file mode 100644 index 00000000000..70c868dab03 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java @@ -0,0 +1,15 @@ +package io.jans.kc.oidc.impl; + +import com.nimbusds.oauth2.sdk.token.RefreshToken; + +import io.jans.kc.oidc.OIDCRefreshToken; + +public class NimbusOIDCRefreshToken implements OIDCRefreshToken{ + + private RefreshToken refreshToken; + + public NimbusOIDCRefreshToken(RefreshToken refreshToken) { + this.refreshToken = refreshToken; + } + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java new file mode 100644 index 00000000000..318f504c16f --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java @@ -0,0 +1,222 @@ +package io.jans.kc.oidc.impl; + +import java.io.IOException; + +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.GeneralException; +import com.nimbusds.oauth2.sdk.ParseException; +import com.nimbusds.oauth2.sdk.ResponseType; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.ResponseType.Value; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.http.HTTPResponse; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.AuthenticationRequest; +import com.nimbusds.openid.connect.sdk.Nonce; +import com.nimbusds.openid.connect.sdk.UserInfoRequest; +import com.nimbusds.openid.connect.sdk.UserInfoResponse; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; + +import io.jans.kc.oidc.OIDCAccessToken; +import io.jans.kc.oidc.OIDCAuthRequest; +import io.jans.kc.oidc.OIDCMetaCache; +import io.jans.kc.oidc.OIDCMetaCacheKeys; +import io.jans.kc.oidc.OIDCMetaError; +import io.jans.kc.oidc.OIDCService; +import io.jans.kc.oidc.OIDCTokenRequest; +import io.jans.kc.oidc.OIDCTokenRequestError; +import io.jans.kc.oidc.OIDCTokenResponse; +import io.jans.kc.oidc.OIDCUserInfoRequestError; +import io.jans.kc.oidc.OIDCUserInfoResponse; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.jboss.logging.Logger; + +public class NimbusOIDCService implements OIDCService { + + private static final Logger log = Logger.getLogger(NimbusOIDCService.class); + + private OIDCMetaCache metaCache; + + public NimbusOIDCService(OIDCMetaCache metaCache) { + + this.metaCache = metaCache; + } + + @Override + public URI getAuthorizationEndpoint(String issuerUrl) throws OIDCMetaError { + + URI ret = getAuthorizationEndpointFromCache(issuerUrl); + if(ret == null) { + return getAuthorizationEndpointFromServer(issuerUrl); + } + return ret; + } + + @Override + public URI getTokenEndpoint(String issuerUrl) throws OIDCMetaError { + + URI ret = getTokenEndpointFromCache(issuerUrl); + if(ret == null) { + return getTokenEndpointFromServer(issuerUrl); + } + return ret; + } + + @Override + public URI getUserInfoEndpoint(String issuerUrl) throws OIDCMetaError { + + URI ret = getUserInfoEndpointFromCache(issuerUrl); + if(ret == null) { + return getUserInfoEndpointFromServer(issuerUrl); + } + return ret; + } + + @Override + public URI createAuthorizationUrl(String issuerUrl, OIDCAuthRequest request) throws OIDCMetaError { + + try { + + return new AuthenticationRequest.Builder( + extractResponseType(request.getResponseTypes()), + extractScope(request.getScopes()), + new ClientID(request.getClientId()), + new URI(request.getRedirectUri()) + ) + .endpointURI(getAuthorizationEndpoint(issuerUrl)) + .state(new State(request.getState())) + .nonce(new Nonce(request.getNonce())) + .build().toURI(); + }catch(URISyntaxException e) { + throw new OIDCMetaError("Error building the authentication url",e); + } + } + + @Override + public OIDCTokenResponse requestTokens(String issuerUrl,OIDCTokenRequest tokenrequest) throws OIDCTokenRequestError { + + try { + AuthorizationCode code = new AuthorizationCode(tokenrequest.getCode()); + AuthorizationCodeGrant grant = new AuthorizationCodeGrant(code,tokenrequest.getRedirectUri()); + ClientID clientId = new ClientID(tokenrequest.getClientId()); + Secret secret = new Secret(tokenrequest.getClientSecret()); + ClientAuthentication auth = new ClientSecretBasic(clientId,secret); + TokenRequest request = new TokenRequest(getTokenEndpoint(issuerUrl),auth,grant); + TokenResponse tokenresponse = TokenResponse.parse(request.toHTTPRequest().send()); + return new NimbusOIDCTokenResponse(tokenresponse); + }catch(ParseException e) { + throw new OIDCTokenRequestError("Error parsing token response",e); + }catch(IOException e) { + throw new OIDCTokenRequestError("I/O error while retrieving token data",e); + }catch(OIDCMetaError e) { + throw new OIDCTokenRequestError("Error retrieving token endpoint from server", e); + } + } + + @Override + public OIDCUserInfoResponse requestUserInfo(String issuerUrl, OIDCAccessToken accesstoken) throws OIDCUserInfoRequestError { + + if(!(accesstoken instanceof NimbusOIDCAccessToken)){ + throw new OIDCUserInfoRequestError("The specified access token is not supported by the Nimbus Backend"); + } + + BearerAccessToken bearertoken = ((NimbusOIDCAccessToken) accesstoken).asBearerToken(); + try { + HTTPResponse http_response = new UserInfoRequest(getUserInfoEndpoint(issuerUrl),bearertoken).toHTTPRequest().send(); + UserInfoResponse userinforesponse = UserInfoResponse.parse(http_response); + return new NimbusOIDCUserInfoResponse(userinforesponse); + } catch (IOException e) { + throw new OIDCUserInfoRequestError("I/O error trying to obtain user info",e); + }catch(OIDCMetaError e) { + throw new OIDCUserInfoRequestError("Metadata fetch error trying to obtain user info",e); + }catch(ParseException e) { + throw new OIDCUserInfoRequestError("Parse error trying to obtain user info",e); + } + } + + private ResponseType extractResponseType(List rtypes) { + + ResponseType rtype = new ResponseType(); + for(String val : rtypes) { + rtype.add(new Value(val)); + } + return rtype; + } + + private Scope extractScope(List scopes) { + + Scope scope = new Scope(); + for(String val : scopes) { + scope.add(val); + } + return scope; + } + + private URI getAuthorizationEndpointFromCache(String issuerUrl) { + + return (URI) metaCache.get(issuerUrl, OIDCMetaCacheKeys.AUTHORIZATION_URL); + } + + private URI getAuthorizationEndpointFromServer(String issuerUrl) throws OIDCMetaError { + + OIDCProviderMetadata meta = obtainMetadataFromServer(issuerUrl); + cacheMetadataFromServer(issuerUrl,meta); + return getAuthorizationEndpointFromCache(issuerUrl); + } + + private URI getTokenEndpointFromCache(String issuerUrl) { + + return (URI) metaCache.get(issuerUrl,OIDCMetaCacheKeys.TOKEN_URL); + } + + private URI getTokenEndpointFromServer(String issuerUrl) throws OIDCMetaError { + + OIDCProviderMetadata meta = obtainMetadataFromServer(issuerUrl); + cacheMetadataFromServer(issuerUrl, meta); + return getTokenEndpointFromCache(issuerUrl); + } + + private URI getUserInfoEndpointFromServer(String issuerUrl) throws OIDCMetaError { + + OIDCProviderMetadata meta = obtainMetadataFromServer(issuerUrl); + cacheMetadataFromServer(issuerUrl, meta); + return getUserInfoEndpointFromCache(issuerUrl); + } + + private URI getUserInfoEndpointFromCache(String issuerUrl) throws OIDCMetaError { + + return (URI) metaCache.get(issuerUrl,OIDCMetaCacheKeys.USERINFO_URL); + } + + private OIDCProviderMetadata obtainMetadataFromServer(String issuerUrl) throws OIDCMetaError { + + try { + Issuer issuer = new Issuer(issuerUrl); + return OIDCProviderMetadata.resolve(issuer); + }catch(GeneralException e) { + throw new OIDCMetaError("Could not obtain metadata from server",e); + }catch(IOException e) { + throw new OIDCMetaError("Could not obtain metadata from server",e); + } + } + + private void cacheMetadataFromServer(String issuerUrl,OIDCProviderMetadata metadata) { + + metaCache.put(issuerUrl,OIDCMetaCacheKeys.AUTHORIZATION_URL,metadata.getAuthorizationEndpointURI()); + metaCache.put(issuerUrl,OIDCMetaCacheKeys.TOKEN_URL,metadata.getTokenEndpointURI()); + metaCache.put(issuerUrl,OIDCMetaCacheKeys.USERINFO_URL,metadata.getUserInfoEndpointURI()); + } + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java new file mode 100644 index 00000000000..74cb56d1f76 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java @@ -0,0 +1,56 @@ +package io.jans.kc.oidc.impl; + +import com.nimbusds.oauth2.sdk.AccessTokenResponse; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.token.Tokens; + +import io.jans.kc.oidc.OIDCAccessToken; +import io.jans.kc.oidc.OIDCRefreshToken; +import io.jans.kc.oidc.OIDCTokenError; +import io.jans.kc.oidc.OIDCTokenResponse; + + +public class NimbusOIDCTokenResponse implements OIDCTokenResponse { + + private TokenResponse tokenResponse; + private NimbusOIDCAccessToken accessToken; + private NimbusOIDCRefreshToken refreshToken; + private OIDCTokenError tokenError; + + public NimbusOIDCTokenResponse(TokenResponse tokenResponse) { + + this.tokenResponse = tokenResponse; + if(this.tokenResponse.indicatesSuccess()) { + AccessTokenResponse atresponse = this.tokenResponse.toSuccessResponse(); + Tokens tokens = atresponse.getTokens(); + this.accessToken = new NimbusOIDCAccessToken(tokens.getAccessToken()); + this.refreshToken = new NimbusOIDCRefreshToken(tokens.getRefreshToken()); + }else { + + } + } + + @Override + public OIDCAccessToken accessToken() { + + return accessToken; + } + + @Override + public OIDCRefreshToken refreshToken() { + + return refreshToken; + } + + @Override + public OIDCTokenError error() { + + return tokenError; + } + + @Override + public boolean indicatesSuccess() { + + return tokenResponse.indicatesSuccess(); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCUserInfoResponse.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCUserInfoResponse.java new file mode 100644 index 00000000000..ca8ae2b0afa --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCUserInfoResponse.java @@ -0,0 +1,47 @@ +package io.jans.kc.oidc.impl; + +import com.nimbusds.openid.connect.sdk.UserInfoResponse; + +import io.jans.kc.oidc.OIDCUserInfoError; +import io.jans.kc.oidc.OIDCUserInfoResponse; + +public class NimbusOIDCUserInfoResponse implements OIDCUserInfoResponse { + + private static final String USERNAME_CLAIM_NAME = "user_name"; + + private UserInfoResponse userInfoResponse; + + public NimbusOIDCUserInfoResponse(UserInfoResponse userInfoResponse) { + this.userInfoResponse = userInfoResponse; + } + + public String username() { + + return (String) userInfoResponse.toSuccessResponse().getUserInfo().getClaim(USERNAME_CLAIM_NAME); + } + + public String email() { + + return userInfoResponse.toSuccessResponse().getUserInfo().getEmailAddress(); + } + + @Override + public boolean indicatesSuccess() { + + return userInfoResponse.indicatesSuccess(); + } + + @Override + public OIDCUserInfoError error() { + + if(userInfoResponse.indicatesSuccess()) { + return null; + } + + return new OIDCUserInfoError( + userInfoResponse.toErrorResponse().getErrorObject().getCode(), + userInfoResponse.toErrorResponse().getErrorObject().getDescription(), + userInfoResponse.toErrorResponse().getErrorObject().getHTTPStatusCode() + ); + } +} \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java new file mode 100644 index 00000000000..2fa0cea1a7c --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java @@ -0,0 +1,436 @@ +package io.jans.kc.spi.auth; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; + +import java.text.MessageFormat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.ArrayList; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriBuilder; + +import org.jboss.logging.Logger; + +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.KeycloakModelUtils; + +import io.jans.kc.spi.ProviderIDs; +import io.jans.kc.oidc.OIDCAuthRequest; +import io.jans.kc.oidc.OIDCMetaError; +import io.jans.kc.oidc.OIDCService; +import io.jans.kc.oidc.OIDCTokenError; +import io.jans.kc.oidc.OIDCTokenRequest; +import io.jans.kc.oidc.OIDCTokenRequestError; +import io.jans.kc.oidc.OIDCTokenResponse; +import io.jans.kc.oidc.OIDCUserInfoError; +import io.jans.kc.oidc.OIDCUserInfoRequestError; +import io.jans.kc.oidc.OIDCUserInfoResponse; + +public class JansAuthenticator implements Authenticator { + + private static final Logger log = Logger.getLogger(JansAuthenticator.class); + + private static final String JANS_AUTH_REDIRECT_FORM_FTL = "jans-auth-redirect.ftl"; + private static final String JANS_AUTH_ERROR_FTL = "jans-auth-error.ftl"; + + private static final String OPENID_CODE_RESPONSE = "code"; + private static final String OPENID_SCOPE = "openid"; + private static final String USERNAME_SCOPE ="user_name"; + private static final String EMAIL_SCOPE = "email"; + private static final String JANS_LOGIN_URL_ATTRIBUTE = "jansLoginUrl"; + private static final String OPENID_AUTH_PARAMS_ATTRIBUTE = "openIdAuthParams"; + + private static final String URI_PATH_TO_REST_SERVICE = "realms/{realm}/{providerid}/auth-complete"; + + + private OIDCService oidcService; + + public JansAuthenticator(OIDCService oidcService) { + + this.oidcService = oidcService; + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + + Configuration config = extractAndValidateConfiguration(context); + if(config == null) { + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onConfigurationError(context)); + return; + } + + try { + URI redirecturi = createRedirectUri(context); + URI actionuri = createActionUrl(context); + + String state = generateOIDCState(); + String nonce = generateOIDCNonce(); + + OIDCAuthRequest oidcauthrequest = createAuthnRequest(config, state, nonce,redirecturi.toString()); + + URI loginurl = oidcService.createAuthorizationUrl(config.normalizedIssuerUrl(), oidcauthrequest); + URI loginurlnoparams = UriBuilder.fromUri(loginurl.toString()).replaceQuery(null).build(); + + Response response = context + .form() + .setActionUri(actionuri) + .setAttribute(JANS_LOGIN_URL_ATTRIBUTE,loginurlnoparams.toString()) + .setAttribute(OPENID_AUTH_PARAMS_ATTRIBUTE,parseQueryParameters(loginurl.getQuery())) + .createForm(JANS_AUTH_REDIRECT_FORM_FTL); + + saveRealmStringData(context, SessionAttributes.JANS_OIDC_NONCE, nonce); + saveRealmStringData(context, SessionAttributes.KC_ACTION_URI,actionuri.toString()); + saveRealmStringData(context,SessionAttributes.JANS_OIDC_STATE,state); + + context.challenge(response); + }catch(OIDCMetaError e) { + log.errorv(e,"OIDC Error obtaining the authorization url"); + Response response = context.form().createForm(JANS_AUTH_ERROR_FTL); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); + } + } + + @Override + public void action(AuthenticationFlowContext context) { + + Configuration config = extractAndValidateConfiguration(context); + if(config == null) { + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onMissingAuthenticationCode(context)); + return; + } + + String openid_code = getOpenIdCode(context); + if(openid_code == null) { + log.errorv("Missing authentication code during response processing"); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onMissingAuthenticationCode(context)); + return; + } + + OIDCTokenRequest tokenrequest = createTokenRequest(config, openid_code, createRedirectUri(context)); + try { + OIDCTokenResponse tokenresponse = oidcService.requestTokens(config.normalizedIssuerUrl(), tokenrequest); + if(!tokenresponse.indicatesSuccess()) { + OIDCTokenError error = tokenresponse.error(); + log.errorv("Error processing token {0}. ({1}) {2}",error.code(),error.description()); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onTokenRetrievalError(context)); + return; + } + + OIDCUserInfoResponse userinforesponse = oidcService.requestUserInfo(config.normalizedIssuerUrl(),tokenresponse.accessToken()); + if(!userinforesponse.indicatesSuccess()) { + OIDCUserInfoError error = userinforesponse.error(); + log.errorv("Error getting userinfo for authenticated user. ({0}) {1}",error.code(),error.description()); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onUserInfoRetrievalError(context)); + return; + } + + UserModel user = findUserByNameOrEmail(context,userinforesponse.username(),userinforesponse.email()); + if(user == null) { + log.errorv("User with username/email {0} / {1} not found",userinforesponse.username(),userinforesponse.email()); + context.failure(AuthenticationFlowError.UNKNOWN_USER); + return; + } + log.debugv("User {0} authenticated",user.getUsername()); + context.setUser(user); + context.success(); + }catch(OIDCTokenRequestError e) { + log.debugv(e,"Unable to retrieve token information"); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onTokenRetrievalError(context)); + }catch(OIDCUserInfoRequestError e) { + log.debugv(e,"Unable to retrieve user information"); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,onUserInfoRetrievalError(context)); + } + } + + @Override + public boolean requiresUser() { + + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + + return false; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel model, UserModel user) { + + return; + } + + @Override + public List getRequiredActions(KeycloakSession session) { + + return null; + } + + @Override + public void close() { + + return; + } + + private Configuration extractAndValidateConfiguration(AuthenticationFlowContext context) { + + Configuration config = pluginConfigurationFromContext(context); + + if(config == null) { + log.debugv("Plugin probably not configured. Check the Janssen Auth plugin in the authentication flow"); + return null; + } + + ValidationResult validationresult = config.validate(); + if(validationresult.hasErrors()) { + for(String err : validationresult.getErrors()) { + log.errorv("Invalid plugin configuration {0}",err); + } + return null; + } + return config; + } + + private URI createRedirectUri(AuthenticationFlowContext context) { + + try { + String realmname = context.getRealm().getName(); + return UriBuilder.fromUri(context.getSession().getContext().getUri().getBaseUri()) + .path(URI_PATH_TO_REST_SERVICE) + .build(realmname,ProviderIDs.JANS_AUTH_RESPONSE_REST_PROVIDER); + }catch(IllegalArgumentException e) { + log.warnv(e,"Could not create redirect URIs"); + return null; + } + } + + private UserModel findUserByNameOrEmail(AuthenticationFlowContext context, String username,String email) { + + UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(),context.getRealm(),username); + if(user == null) { + user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(),context.getRealm(),email); + } + return user; + } + + + private Map parseQueryParameters(String params) { + + Map ret = new HashMap(); + if(params == null) { + return ret; + } + + String [] parampairs = params.split("&"); + for(String pair : parampairs) { + String [] kv = pair.split("="); + if(kv.length == 1) { + ret.put(kv[0].trim(),""); + }else { + try { + ret.put(kv[0].trim(), + URLDecoder.decode(kv[1].trim(),"UTF-8")); + }catch(UnsupportedEncodingException ue) { + log.debugv(ue,"Failed to decode query parameter data {0}",pair); + } + } + } + + return ret; + } + + private Configuration pluginConfigurationFromContext(AuthenticationFlowContext context) { + + AuthenticatorConfigModel config = context.getAuthenticatorConfig(); + if(config == null || config.getConfig() == null) { + return null; + } + + String server_url = config.getConfig().get(JansAuthenticatorConfigProp.SERVER_URL.getName()); + String client_id = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_ID.getName()); + String client_secret = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_SECRET.getName()); + String issuer = config.getConfig().get(JansAuthenticatorConfigProp.ISSUER.getName()); + String extra_scopes = config.getConfig().get(JansAuthenticatorConfigProp.EXTRA_SCOPES.getName()); + List parsed_extra_scopes = new ArrayList<>(); + if(extra_scopes != null) { + String [] tokens = extra_scopes.split("\\s*,\\s*"); + for(String token : tokens) { + parsed_extra_scopes.add(token); + } + } + + return new Configuration(server_url,client_id,client_secret,issuer,parsed_extra_scopes); + } + + private final String generateOIDCState() { + + return generateRandomString(10); + } + + private final String generateOIDCNonce() { + + return generateRandomString(10); + } + + private final URI createActionUrl(AuthenticationFlowContext context) { + + String accesscode = context.generateAccessCode(); + return context.getActionUrl(accesscode); + } + + + private final void saveRealmStringData(AuthenticationFlowContext context, String key, String value) { + + context.getRealm().setAttribute(key, value); + } + + private String generateRandomString(int length) { + int leftlimit = 48; + int rightlimit = 122; + + return new Random().ints(leftlimit,rightlimit+1) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(length) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + + private OIDCAuthRequest createAuthnRequest(Configuration config, String state, String nonce,String redirecturi) { + // + OIDCAuthRequest request = new OIDCAuthRequest(); + request.setClientId(config.clientId); + request.addScope(OPENID_SCOPE); + request.addScope(USERNAME_SCOPE); + request.addScope(EMAIL_SCOPE); + for(String extrascope : config.scopes) { + request.addScope(extrascope); + } + request.addResponseType(OPENID_CODE_RESPONSE); + request.setNonce(nonce); + request.setState(state); + request.setRedirectUri(redirecturi); + return request; + } + + private OIDCTokenRequest createTokenRequest(Configuration config,String code,URI redirecturi) { + + return new OIDCTokenRequest(code,config.clientId,config.clientSecret,redirecturi); + } + + private final Response onConfigurationError(AuthenticationFlowContext context) { + + return context.form().createForm(JANS_AUTH_ERROR_FTL); + } + + private final Response onMissingAuthenticationCode(AuthenticationFlowContext context) { + + return context.form().createForm(JANS_AUTH_ERROR_FTL); + } + + private final Response onTokenRetrievalError(AuthenticationFlowContext context) { + + return context.form().createForm(JANS_AUTH_ERROR_FTL); + } + + private final Response onUserInfoRetrievalError (AuthenticationFlowContext context) { + + return context.form().createForm(JANS_AUTH_ERROR_FTL); + } + + private final String getOpenIdCode(AuthenticationFlowContext context) { + + return context.getRealm().getAttribute(SessionAttributes.JANS_OIDC_CODE); + } + + + public static class ValidationResult { + + private List errors; + + public void addError(String error) { + + if(errors == null) { + this.errors = new ArrayList(); + } + this.errors.add(error); + } + + public boolean hasErrors() { + + return this.errors != null; + } + + public List getErrors() { + + return this.errors; + } + } + + private class Configuration { + + private String serverUrl; + private String clientId; + private String clientSecret; + private String issuerUrl; + private List scopes; + + public Configuration(String serverUrl,String clientId, String clientSecret, String issuerUrl, List scopes) { + + this.serverUrl = serverUrl; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.issuerUrl = issuerUrl; + this.scopes = scopes; + } + + + public ValidationResult validate() { + + ValidationResult result = new ValidationResult(); + + if(serverUrl == null || serverUrl.isEmpty()) { + result.addError("Missing or empty Server Url"); + } + + if(clientId == null || clientId.isEmpty()) { + result.addError("Missing or empty Client ID"); + } + + if(clientSecret == null || clientSecret.isEmpty()) { + result.addError("Missing or empty client secret"); + } + return result; + } + + public String normalizedIssuerUrl() { + + String effective_url = issuerUrl; + if(effective_url == null) { + effective_url = serverUrl; + } + if(effective_url == null) { + return null; + } + + if(effective_url.charAt(effective_url.length() -1) == '/') { + return effective_url.substring(0, effective_url.length() -1); + } + return effective_url; + } + + } +} \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java new file mode 100644 index 00000000000..22f003a6336 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java @@ -0,0 +1,75 @@ +package io.jans.kc.spi.auth; + +import java.util.List; + +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; + +public enum JansAuthenticatorConfigProp { + + SERVER_URL( + "jans.auth.server.url", + "Janssen Server Url", + "Url of the Janssen Server", + ProviderConfigProperty.STRING_TYPE, + null, + false), + CLIENT_ID( + "jans.auth.client.id", + "Janssen Client ID", + "Client ID of the OpenID Client created in Janssen-Auth", + ProviderConfigProperty.STRING_TYPE, + null, + false), + CLIENT_SECRET( + "jans.auth.client.secret", + "Janssen Client Secret", + "Secret/Password of the OpenID Client created in Janssen-Auth", + ProviderConfigProperty.PASSWORD, + null, + true + ), + ISSUER( + "jans.auth.issuer", + "Janssen OpenID Issuer(Optional)", + "OpenID issuer of the Janssen server (Optional)", + ProviderConfigProperty.STRING_TYPE, + null, + false ), + EXTRA_SCOPES( + "jans.auth.extra_scopes", + "Extra OpenID Scopes", + "Comma delimited list of extra OpenID scopes", + ProviderConfigProperty.STRING_TYPE, + null, + false + ); + + private String name; + private ProviderConfigProperty config; + private JansAuthenticatorConfigProp(String name, String label, String helptext, String type, Object defaultvalue, boolean secret) { + + this.name = name; + this.config = new ProviderConfigProperty(name, label, helptext, type, defaultvalue, secret); + } + + public String getName() { + + return this.name; + } + + public ProviderConfigProperty getConfig() { + + return this.config; + } + + public static final List asList() { + + ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create(); + + for(JansAuthenticatorConfigProp prop : values()) { + builder.property(prop.config); + } + return builder.build(); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java new file mode 100644 index 00000000000..4cd5cefe7c2 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java @@ -0,0 +1,117 @@ +package io.jans.kc.spi.auth; + +import java.util.List; + +import org.jboss.logging.Logger; + +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; + +import org.keycloak.Config; + +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import org.keycloak.provider.ProviderConfigProperty; + +import io.jans.kc.spi.ProviderIDs; +import io.jans.kc.oidc.OIDCMetaCache; +import io.jans.kc.oidc.OIDCService; +import io.jans.kc.oidc.impl.HashBasedOIDCMetaCache; +import io.jans.kc.oidc.impl.NimbusOIDCService; + + +public class JansAuthenticatorFactory implements AuthenticatorFactory { + + private static final String PROVIDER_ID = ProviderIDs.JANS_AUTHENTICATOR_PROVIDER; + + private static final String DISPLAY_TYPE = "Janssen Authenticator"; + private static final String REFERENCE_CATEGORY = "Janssen Authenticator"; + private static final String HELP_TEXT= "Janssen authenticator for Keycloak"; + + private static final Logger log = Logger.getLogger(JansAuthenticatorFactory.class); + + private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + + + private static final OIDCMetaCache META_CACHE = new HashBasedOIDCMetaCache(); + private static final OIDCService OIDC_SERVICE = new NimbusOIDCService(META_CACHE); + private static final JansAuthenticator INSTANCE = new JansAuthenticator(OIDC_SERVICE); + + @Override + public String getId() { + + return PROVIDER_ID; + } + + @Override + public Authenticator create(KeycloakSession session) { + + log.debug("Janssen authenticator create()"); + return INSTANCE; + } + + @Override + public void init(Config.Scope config) { + + return; + } + + @Override + public void close() { + + return; + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + return; + } + + @Override + public String getDisplayType() { + + return DISPLAY_TYPE; + } + + @Override + public String getReferenceCategory() { + + return REFERENCE_CATEGORY; + } + + @Override + public boolean isConfigurable() { + + return true; + } + + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + + return false; + } + + @Override + public String getHelpText() { + + return HELP_TEXT; + } + + @Override + public List getConfigProperties() { + + return JansAuthenticatorConfigProp.asList(); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java new file mode 100644 index 00000000000..e7b0331939f --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java @@ -0,0 +1,10 @@ +package io.jans.kc.spi.auth; + +public class SessionAttributes { + + public static final String JANS_OIDC_STATE = "jans.oidc.state"; + public static final String JANS_OIDC_NONCE = "jans.oidc.nonce"; + public static final String KC_ACTION_URI = "kc.action-uri"; + public static final String JANS_OIDC_CODE = "jans.oidc.code"; + public static final String JANS_SESSION_STATE = "jans.session.state"; +} From abf22edd55b776cd9072593fb634b74d8b7540a9 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 08:23:55 +0100 Subject: [PATCH 17/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * moved authenticator rest service spi to spi module Signed-off-by: Rolain Djeumen --- .../JansAuthResponseResourceProvider.java | 125 ++++++++++++++++++ ...nsAuthResponseResourceProviderFactory.java | 42 ++++++ 2 files changed, 167 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java new file mode 100644 index 00000000000..478a4d59fa6 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java @@ -0,0 +1,125 @@ +package io.jans.kc.spi.rest; + +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.resource.RealmResourceProvider; + +import io.jans.kc.spi.auth.SessionAttributes; + +import java.util.Map; +import java.util.HashMap; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; + +public class JansAuthResponseResourceProvider implements RealmResourceProvider { + + private static final Logger log = Logger.getLogger(JansAuthResponseResourceProvider.class); + + private static final String ACTION_URI_TPL_PARAM = "actionuri"; + private static final String ERR_MSG_TPL_PARAM = "authError"; + + private static final String JANS_AUTH_RESPONSE_ERR_FTL ="jans-auth-response-error.ftl"; + private static final String JANS_AUTH_RESPONSE_COMPLETE_FTL = "jans-auth-response-complete.ftl"; + + private static final String ERR_MSG_INVALID_REALM = "jans.error-invalid-realm"; + private static final String ERR_MSG_MISSING_DATA = "jans.error-missing-data"; + + private KeycloakSession session; + + public JansAuthResponseResourceProvider(KeycloakSession session) { + + this.session = session; + } + + @Override + public Object getResource() { + + return this; + } + + @Override + public void close() { + + } + + @GET + @NoCache + @Produces(MediaType.TEXT_HTML) + @Path("/auth-complete") + public Response completeAuthentication(@QueryParam("code") String code, + @QueryParam("scope") String scope, + @QueryParam("state") String state) { + + RealmModel realm = getAuthenticationRealm(); + if(!stateIsAssociatedToRealm(realm, state)) { + log.infov("Realm {0} is not associated to authz response and state {1}",realm.getName(),state); + return createErrorResponse(ERR_MSG_INVALID_REALM); + } + + if(!realmHasActionUri(realm)) { + log.infov("Realm {0} has no action uri set to complete authentication",realm.getName()); + return createErrorResponse(ERR_MSG_MISSING_DATA); + } + saveAuthResultInRealm(realm, code, state); + return createFinalizeAuthResponse(realm.getAttribute(SessionAttributes.KC_ACTION_URI)); + } + + private final RealmModel getAuthenticationRealm() { + + return session.getContext().getRealm(); + } + + private final boolean stateIsAssociatedToRealm(RealmModel realm , String state) { + + String expectedstate = realm.getAttribute(SessionAttributes.JANS_OIDC_STATE); + + return state.equals(expectedstate); + } + + private final boolean realmHasActionUri(RealmModel realm) { + + String actionuri = realm.getAttribute(SessionAttributes.KC_ACTION_URI); + return (actionuri != null); + } + + private final void saveAuthResultInRealm(RealmModel realm, String code, String session_state) { + + realm.setAttribute(SessionAttributes.JANS_OIDC_CODE,code); + realm.setAttribute(SessionAttributes.JANS_SESSION_STATE,session_state); + } + + private final Response createResponseWithForm(String formtemplate,Map attributes) { + + LoginFormsProvider lfp = session.getProvider(LoginFormsProvider.class); + + if(attributes != null && !attributes.isEmpty()) { + for(String key: attributes.keySet()) { + lfp.setAttribute(key,attributes.get(key)); + } + } + return lfp.createForm(formtemplate); + } + + private final Response createErrorResponse(String errmsgid) { + + Map attributes = new HashMap(); + attributes.put(ERR_MSG_TPL_PARAM,errmsgid); + return createResponseWithForm(JANS_AUTH_RESPONSE_ERR_FTL,attributes); + } + + private final Response createFinalizeAuthResponse(String actionuri) { + + Map attributes = new HashMap(); + attributes.put(ACTION_URI_TPL_PARAM,actionuri); + return createResponseWithForm(JANS_AUTH_RESPONSE_COMPLETE_FTL, attributes); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java new file mode 100644 index 00000000000..8384f1bf782 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java @@ -0,0 +1,42 @@ +package io.jans.kc.spi.rest; + +import org.keycloak.Config.Scope; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.resource.RealmResourceProviderFactory; + +import io.jans.kc.spi.ProviderIDs; + +public class JansAuthResponseResourceProviderFactory implements RealmResourceProviderFactory { + + private static final String ID = ProviderIDs.JANS_AUTH_RESPONSE_REST_PROVIDER; + + @Override + public String getId() { + + return ID; + } + + @Override + public RealmResourceProvider create(KeycloakSession session) { + + return new JansAuthResponseResourceProvider(session); + } + + @Override + public void init(Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } +} \ No newline at end of file From a67f97759bc1c63a9c9ec83bdf473b3017ee3ee3 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 08:39:51 +0100 Subject: [PATCH 18/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * added new storage provider implementation Signed-off-by: Rolain Djeumen --- .../spi/storage/JansUserStorageProvider.java | 68 +++++++++++++++++++ .../JansUserStorageProviderFactory.java | 32 +++++++++ 2 files changed, 100 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java new file mode 100644 index 00000000000..0770f439bff --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java @@ -0,0 +1,68 @@ +package io.jans.kc.spi.storage; + +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.user.UserLookupProvider; + +import io.jans.kc.model.JansUserModel; +import io.jans.kc.spi.custom.JansThinBridgeOperationException; +import io.jans.kc.spi.custom.JansThinBridgeProvider; + +import org.jboss.logging.Logger; + +public class JansUserStorageProvider implements UserStorageProvider, UserLookupProvider { + + private static final Logger log = Logger.getLogger(JansUserStorageProvider.class); + + private final JansThinBridgeProvider jansThinBridge; + + public JansUserStorageProvider(final JansThinBridgeProvider jansThinBridge) { + + this.jansThinBridge = jansThinBridge; + } + + @Override + public void close() { + + } + + @Override + public UserModel getUserByUsername(RealmModel realm, String username) { + + try { + return jansThinBridge.getUserByUsername(username); + }catch(JansThinBridgeOperationException e) { + log.errorv(e,"Error fetching jans user with username " + username); + } + return null; + } + + @Override + public UserModel getUserByEmail(RealmModel realm, String email) { + + try { + return jansThinBridge.getUserByEmail(email); + }catch(JansThinBridgeOperationException e) { + log.errorv(e,"Error fetching jans user with email " + email); + } + return null; + } + + @Override + public UserModel getUserById(RealmModel realm, String id) { + + try { + StorageId storageId = new StorageId(id); + final String inum = storageId.getExternalId(); + return jansThinBridge.getUserByInum(inum); + }catch(JansThinBridgeOperationException e) { + log.errorv(e,"Error fetching jans user with id " + id); + } + return null; + } + + +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java new file mode 100644 index 00000000000..d166fe103bb --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java @@ -0,0 +1,32 @@ +package io.jans.kc.spi.storage; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.UserStorageProviderFactory; + +import io.jans.kc.spi.ProviderIDs; +import io.jans.kc.spi.JansSpiInitException; +import io.jans.kc.spi.custom.JansThinBridgeProvider; + +public class JansUserStorageProviderFactory implements UserStorageProviderFactory { + + + private static final String PROVIDER_ID = ProviderIDs.JANS_USER_STORAGE_PROVIDER; + + @Override + public String getId() { + + //TODO implement this + return PROVIDER_ID; + } + + @Override + public JansUserStorageProvider create(KeycloakSession session, ComponentModel model) { + + JansThinBridgeProvider jansThinBridgeProvider = session.getProvider(JansThinBridgeProvider.class); + if(jansThinBridgeProvider == null) { + throw new JansSpiInitException("Could not obtain reference to thin bridge provider"); + } + return new JansUserStorageProvider(jansThinBridgeProvider); + } +} From 7e4567e20161e3a3a8cec26a8d37327e14065e03 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 08:46:54 +0100 Subject: [PATCH 19/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * added missing files to spi Signed-off-by: Rolain Djeumen --- .../java/io/jans/kc/spi/JansSpiInitException.java | 12 ++++++++++++ .../src/main/java/io/jans/kc/spi/ProviderIDs.java | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/JansSpiInitException.java create mode 100644 jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/JansSpiInitException.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/JansSpiInitException.java new file mode 100644 index 00000000000..36ff90e4489 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/JansSpiInitException.java @@ -0,0 +1,12 @@ +package io.jans.kc.spi; + +public class JansSpiInitException extends RuntimeException { + + public JansSpiInitException(final String msg) { + super(msg); + } + + public JansSpiInitException(final String msg, Throwable cause) { + super(msg,cause); + } +} diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java new file mode 100644 index 00000000000..2a383043442 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java @@ -0,0 +1,9 @@ +package io.jans.kc.spi; + +public class ProviderIDs { + public static final String JANS_AUTHENTICATOR_PROVIDER = "kc-jans-authn"; + public static final String JANS_AUTH_RESPONSE_REST_PROVIDER = "kc-jans-authn-rest-bridge"; + public static final String JANS_SAML_USER_ATTRIBUTE_MAPPER_PROVIDER = "kc-jans-saml-user-attribute-mapper"; + public static final String JANS_DEFAULT_THIN_BRIDGE_PROVIDER = "kc-jans-thin-bridge-default"; + public static final String JANS_USER_STORAGE_PROVIDER = "kc-jans-user-storage"; +} \ No newline at end of file From 7ec45bd2ad9b107bdd906fbaa73398ded1d37666 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 08:51:42 +0100 Subject: [PATCH 20/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * added resource files to spi module Signed-off-by: Rolain Djeumen --- ...c.spi.custom.JansThinBridgeProviderFactory | 1 + ...ycloak.authentication.AuthenticatorFactory | 1 + .../org.keycloak.protocol.ProtocolMapper | 1 + .../services/org.keycloak.provider.Spi | 1 + ...ices.resource.RealmResourceProviderFactory | 1 + ...eycloak.storage.UserStorageProviderFactory | 1 + .../src/main/resources/assembly/.DONOTDELETE | 1 + .../messages/messages_en.properties | 9 +++++ ... project favicon transparent 50px 50px.png | Bin 0 -> 9365 bytes .../resources/img/janssen-project.jpg | Bin 0 -> 88147 bytes .../resources/img/janssen_dove_icon.jpg | Bin 0 -> 36751 bytes .../theme-resources/resources/js/jans-auth.js | 0 .../templates/jans-auth-error.ftl | 16 ++++++++ .../templates/jans-auth-redirect.ftl | 37 ++++++++++++++++++ .../templates/jans-auth-response-complete.ftl | 23 +++++++++++ .../templates/jans-auth-response-error.ftl | 16 ++++++++ 16 files changed, 108 insertions(+) create mode 100644 jans-keycloak-integration/spi/src/main/resources/META-INF/services/io.jans.kc.spi.custom.JansThinBridgeProviderFactory create mode 100644 jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory create mode 100644 jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper create mode 100644 jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi create mode 100644 jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory create mode 100644 jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory create mode 100644 jans-keycloak-integration/spi/src/main/resources/assembly/.DONOTDELETE create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/messages/messages_en.properties create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen project favicon transparent 50px 50px.png create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen-project.jpg create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen_dove_icon.jpg create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/js/jans-auth.js create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-error.ftl create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl create mode 100644 jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl diff --git a/jans-keycloak-integration/spi/src/main/resources/META-INF/services/io.jans.kc.spi.custom.JansThinBridgeProviderFactory b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/io.jans.kc.spi.custom.JansThinBridgeProviderFactory new file mode 100644 index 00000000000..edd9fed9b5e --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/io.jans.kc.spi.custom.JansThinBridgeProviderFactory @@ -0,0 +1 @@ +io.jans.kc.spi.custom.impl.DefaultJansThinBridgeProviderFactory \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100644 index 00000000000..3f98bf660d4 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1 @@ +io.jans.kc.spi.auth.JansAuthenticatorFactory \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper new file mode 100644 index 00000000000..4a8751a217e --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -0,0 +1 @@ +io.jans.kc.spi.protocol.mapper.saml.JansSamlUserAttributeMapper \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 00000000000..86e73a0b055 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +io.jans.kc.spi.custom.JansThinBridgeSpi \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory new file mode 100644 index 00000000000..66bd969afe6 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory @@ -0,0 +1 @@ +io.jans.kc.spi.rest.JansAuthResponseResourceProviderFactory \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 00000000000..df4a036ae57 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1 @@ +io.jans.kc.spi.storage.JansUserStorageProviderFactory \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/assembly/.DONOTDELETE b/jans-keycloak-integration/spi/src/main/resources/assembly/.DONOTDELETE new file mode 100644 index 00000000000..c7c1c13b13c --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/assembly/.DONOTDELETE @@ -0,0 +1 @@ +This file is used to prevent build errors during the run of the maven assembly plugin \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/messages/messages_en.properties b/jans-keycloak-integration/spi/src/main/resources/theme-resources/messages/messages_en.properties new file mode 100644 index 00000000000..c710f1fca8a --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/theme-resources/messages/messages_en.properties @@ -0,0 +1,9 @@ +jans.redirect.to-jans=You''re being redirected to Janssen... +jans.redirect.too-long-click-here=If it''s taking too long, Click Here +jans.error-title=We''re Sorry ... +jans.error-description=Error processing authentication request. Please contact your administrator. +jans.error-invalid-realm=Error processing authentication request. The associated session is probably stale \ + or does not exist. Please contact your administrator. +jans.error-missing-data=Error processing authentication request. Missing or unavailable data required to complete \ + the authentication. Please contact your administrator. +jans.complete-auth-button=Click here if you're not being redirected \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen project favicon transparent 50px 50px.png b/jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen project favicon transparent 50px 50px.png new file mode 100644 index 0000000000000000000000000000000000000000..f3c9c3e58ca60cdefc3a4c6b4bde9196386aaa08 GIT binary patch literal 9365 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 Pamd_w53$tHE&6`|1Y5Vp literal 0 HcmV?d00001 diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen-project.jpg b/jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen-project.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b3296c5caed68b107da584363268d9b794b30161 GIT binary patch literal 88147 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 literal 0 HcmV?d00001 diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen_dove_icon.jpg b/jans-keycloak-integration/spi/src/main/resources/theme-resources/resources/img/janssen_dove_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd254cd3ef2ea8f6cf6288c7f0241ff7072bb9ae GIT binary patch literal 36751 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 +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + ${kcSanitize(msg("jans.error-title"))?no_esc} + <#elseif section = "form"> +
+

${kcSanitize(msg("jans.error-description"))?no_esc}

+ <#if skipLink??> + <#else> + <#if client?? && client.baseUrl?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ + \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl b/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl new file mode 100644 index 00000000000..c54179638ac --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl @@ -0,0 +1,37 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section="header"> + ${msg("jans.redirect.to-jans")} + <#elseif section="form"> +
+ + + <#list openIdAuthParams?keys as paramname> + + + +
+
+
+ +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl b/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl new file mode 100644 index 00000000000..21e82650879 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl @@ -0,0 +1,23 @@ + + + Janssen Bridge :: Completing Authentication + + +
+
+ ${msg('jans.complete-auth-button')} + + + \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl b/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl new file mode 100644 index 00000000000..177ae31a123 --- /dev/null +++ b/jans-keycloak-integration/spi/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl @@ -0,0 +1,16 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "header"> + ${kcSanitize(msg("jans.error-title"))?no_esc} + <#elseif section = "form"> +
+

${kcSanitize(msg(authError))?no_esc}

+ <#if skipLink??> + <#else> + <#if client?? && client.baseUrl?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ + \ No newline at end of file From 086808d168b49f39cadd010784cec8c98eab3769 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 09:00:32 +0100 Subject: [PATCH 21/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * bump spi version to 1.1.3-SNAPSHOT * removed protocol-mapper PoC from build modules Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 1 - jans-keycloak-integration/spi/pom.xml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index d90a6864cd0..ed385c3afd9 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -60,7 +60,6 @@ authenticator storage-spi job-scheduler - protocol-mapper spi diff --git a/jans-keycloak-integration/spi/pom.xml b/jans-keycloak-integration/spi/pom.xml index 5fd6753db0b..a95d5b6f3c4 100644 --- a/jans-keycloak-integration/spi/pom.xml +++ b/jans-keycloak-integration/spi/pom.xml @@ -9,7 +9,7 @@ io.jans jans-kc-parent - 1.1.2-SNAPSHOT + 1.1.3-SNAPSHOT From f444ea113dc6658d64b2b1039d0e96d982ab9705 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 12:24:49 +0100 Subject: [PATCH 22/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * minor bugfix to scheduler. did not show fatal startup errors in log file Signed-off-by: Rolain Djeumen --- .../src/main/java/io/jans/kc/scheduler/App.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index 883b09a55ec..7032cce632a 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -95,6 +95,13 @@ public static void main(String[] args) throws InterruptedException, ParserCreate } System.exit(-1); return; + }catch(Exception e) { + log.error("Fatal error starting application",e); + if(jobScheduler != null ) { + jobScheduler.stop(); + } + System.exit(-1); + return; } } @@ -271,7 +278,7 @@ public static class ShutdownHook extends Thread { public void run() { try { - log.debug("Shutting down application"); + log.info("Shutting down application"); if (jobScheduler != null) { jobScheduler.stop(); } From 92ee6d25b10142b0c4b4cb0b216a30a941deafad Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 12:51:43 +0100 Subject: [PATCH 23/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 *fix for fatal errors which don't still appear in the logs Signed-off-by: Rolain Djeumen --- .../src/main/java/io/jans/kc/scheduler/App.java | 4 ++-- .../io/jans/kc/scheduler/TrustRelationshipSyncJob.java | 7 +------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index 7032cce632a..b115da879c3 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -90,6 +90,7 @@ public static void main(String[] args) throws InterruptedException, ParserCreate log.info("Application shutting down"); }catch(StartupError e) { log.error("Application startup failed",e); + log.info("Application startup failed",e); if(jobScheduler != null) { jobScheduler.stop(); } @@ -97,6 +98,7 @@ public static void main(String[] args) throws InterruptedException, ParserCreate return; }catch(Exception e) { log.error("Fatal error starting application",e); + log.info("Application startup failed",e); if(jobScheduler != null ) { jobScheduler.stop(); } @@ -170,10 +172,8 @@ private static final JobScheduler createQuartzJobSchedulerFromConfiguration(AppC private static final void runCronJobs() { - log.debug("Running trust relationship sync cron job"); TrustRelationshipSyncJob trsyncjob = new TrustRelationshipSyncJob(); trsyncjob.run(null); - log.debug("Trust relationship sync cron job complete"); } private static final void performPostStartupOperations() { diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java index b93a139e183..d23120b7228 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java @@ -39,12 +39,7 @@ public TrustRelationshipSyncJob() { this.keycloakApi = App.keycloakApi(); this.realm = App.configuration().keycloakResourcesRealm(); this.samlUserAttributeMapperId = App.configuration().keycloakResourcesSamlUserAttributeMapper(); - try { - this.authnBrowserFlow = keycloakApi.getAuthenticationFlowFromAlias(realm,App.configuration().keycloakResourcesBrowserFlowAlias()); - }catch(Exception e) { - log.warn("Could not properly initialize sync job",e); - this.authnBrowserFlow = null; - } + this.authnBrowserFlow = keycloakApi.getAuthenticationFlowFromAlias(realm,App.configuration().keycloakResourcesBrowserFlowAlias()); } @Override From f25ff29ae2f77f874e018101d83eae2b17597cff Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Tue, 18 Jun 2024 13:15:21 +0100 Subject: [PATCH 24/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * further housekeeping in job-scheduler Signed-off-by: Rolain Djeumen --- .../job-scheduler/src/main/java/io/jans/kc/scheduler/App.java | 2 -- .../java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index b115da879c3..c4d39157eee 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -90,7 +90,6 @@ public static void main(String[] args) throws InterruptedException, ParserCreate log.info("Application shutting down"); }catch(StartupError e) { log.error("Application startup failed",e); - log.info("Application startup failed",e); if(jobScheduler != null) { jobScheduler.stop(); } @@ -98,7 +97,6 @@ public static void main(String[] args) throws InterruptedException, ParserCreate return; }catch(Exception e) { log.error("Fatal error starting application",e); - log.info("Application startup failed",e); if(jobScheduler != null ) { jobScheduler.stop(); } diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java index 2d14fc4db51..5a7c5cde4a4 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java @@ -39,7 +39,8 @@ public void execute(JobExecutionContext context) throws JobExecutionException { ExecutionContext effectivecontext = new QuartzExecutionContext(context.getMergedJobDataMap()); job.run(effectivecontext); } catch(ReflectiveOperationException e) { - e.printStackTrace(); + throw new JobExecutionException("Failed to run job " + jobname,e); + }catch(Exception e) { throw new JobExecutionException("Failed to run job " + jobname,e); } } From a84d52a67fa6823cb5c76d124a85611c64494a73 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Wed, 19 Jun 2024 16:55:52 +0100 Subject: [PATCH 25/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * fixed bug in user storage spi preventing authentication in new version of keycloak Signed-off-by: Rolain Djeumen --- .../java/io/jans/kc/model/JansUserModel.java | 20 +++++++++- .../kc/spi/custom/JansThinBridgeProvider.java | 9 +++-- .../impl/DefaultJansThinBridgeProvider.java | 19 ++++++---- .../spi/storage/JansUserStorageProvider.java | 37 +++++++++++++++---- .../JansUserStorageProviderFactory.java | 6 ++- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java index b455a171a6f..8eb3664af83 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java @@ -9,12 +9,18 @@ import java.util.Map; import java.util.stream.Stream; +import org.keycloak.component.ComponentModel; + import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.models.RoleModel; import org.keycloak.models.SubjectCredentialManager; import org.keycloak.storage.ReadOnlyException; +import org.keycloak.storage.StorageId; + +import org.jboss.logging.Logger; public class JansUserModel implements UserModel { @@ -25,17 +31,27 @@ public class JansUserModel implements UserModel { private static final String GIVEN_NAME_ATTR_NAME = "givenName"; private static final String MAIL_ATTR_NAME = "mail"; private static final String EMAIL_VERIFIED_ATTR_NAME = "emailVerified"; + + private static final Logger log = Logger.getLogger(JansUserModel.class); + private final JansPerson jansPerson; + private final StorageId storageId; + private final ComponentModel storageProviderModel; + private final KeycloakSession session; - public JansUserModel(final JansPerson jansPerson) { + public JansUserModel(KeycloakSession session, ComponentModel storageProviderModel, JansPerson jansPerson) { + this.session = session; + this.storageProviderModel = storageProviderModel; this.jansPerson = jansPerson; + String userId = jansPerson.customAttributeValue(INUM_ATTR_NAME); + this.storageId = new StorageId(storageProviderModel.getId(),userId); } @Override public String getId() { - return jansPerson.customAttributeValue(INUM_ATTR_NAME); + return storageId.getId(); } @Override diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java index f95236da2a5..c84d11d7c09 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/JansThinBridgeProvider.java @@ -1,14 +1,15 @@ package io.jans.kc.spi.custom; import org.keycloak.provider.*; -import io.jans.kc.model.JansUserModel; + import io.jans.kc.model.JansUserAttributeModel; +import io.jans.kc.model.internal.JansPerson; public interface JansThinBridgeProvider extends Provider { JansUserAttributeModel getUserAttribute(final String kcLoginUsername, final String attributeName); - JansUserModel getUserByUsername(final String username); - JansUserModel getUserByEmail(final String email); - JansUserModel getUserByInum(final String inum); + JansPerson getJansUserByUsername(final String username); + JansPerson getJansUserByEmail(final String email); + JansPerson getJansUserByInum(final String inum); } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java index 929fb27d4b6..169aa5bc7bd 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java @@ -3,7 +3,6 @@ import java.util.List; import io.jans.kc.model.JansUserAttributeModel; -import io.jans.kc.model.JansUserModel; import io.jans.kc.model.internal.JansPerson; import io.jans.kc.spi.custom.JansThinBridgeProvider; import io.jans.kc.spi.custom.JansThinBridgeInitException; @@ -72,45 +71,51 @@ public JansUserAttributeModel getUserAttribute(final String kcLoginUsername, fin } @Override - public JansUserModel getUserByUsername(final String username) { + public JansPerson getJansUserByUsername(final String username) { try { final Filter uidSearchFilter = Filter.createEqualityFilter(UID_ATTR_NAME,username); final JansPerson person = findPerson(uidSearchFilter,defaultUserReturnAttributes); if(person == null) { + log.debugv("User with uid {0} not found in janssen",username); return null; } - return new JansUserModel(person); + log.debugv("User with uid {0} was found in janssen",username); + return person; }catch(Exception e) { throw new JansThinBridgeOperationException("Error fetching jans user with username " + username,e); } } @Override - public JansUserModel getUserByEmail(final String email) { + public JansPerson getJansUserByEmail(final String email) { try { final Filter mailSearchFilter = Filter.createEqualityFilter(MAIL_ATTR_NAME, email); final JansPerson person = findPerson(mailSearchFilter,defaultUserReturnAttributes); if(person == null) { + log.debugv("User with email {0} not found in janssen",email); return null; } - return new JansUserModel(person); + log.debugv("User with email {0} was found in janssen",email); + return person; }catch(Exception e) { throw new JansThinBridgeOperationException("Error fetching jans user with email " + email ,e); } } @Override - public JansUserModel getUserByInum(final String inum) { + public JansPerson getJansUserByInum(final String inum) { try { final Filter inumSearchFilter = Filter.createEqualityFilter(INUM_ATTR_NAME,inum); final JansPerson person = findPerson(inumSearchFilter,defaultUserReturnAttributes); if(person == null) { + log.debugv("User with inum not found in janssen",inum); return null; } - return new JansUserModel(person); + log.debugv("User with inum {0} found in janssen",inum); + return person; }catch(Exception e) { throw new JansThinBridgeOperationException("Error fetching jans user with inum "+inum,e); } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java index 0770f439bff..554f20c9321 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java @@ -1,5 +1,8 @@ package io.jans.kc.spi.storage; +import org.keycloak.component.ComponentModel; + +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -8,6 +11,7 @@ import org.keycloak.storage.user.UserLookupProvider; import io.jans.kc.model.JansUserModel; +import io.jans.kc.model.internal.JansPerson; import io.jans.kc.spi.custom.JansThinBridgeOperationException; import io.jans.kc.spi.custom.JansThinBridgeProvider; @@ -18,9 +22,13 @@ public class JansUserStorageProvider implements UserStorageProvider, UserLookupP private static final Logger log = Logger.getLogger(JansUserStorageProvider.class); private final JansThinBridgeProvider jansThinBridge; + private final ComponentModel model; + private final KeycloakSession session; - public JansUserStorageProvider(final JansThinBridgeProvider jansThinBridge) { + public JansUserStorageProvider(KeycloakSession session,ComponentModel model, JansThinBridgeProvider jansThinBridge) { + this.session = session; + this.model = model; this.jansThinBridge = jansThinBridge; } @@ -33,35 +41,50 @@ public void close() { public UserModel getUserByUsername(RealmModel realm, String username) { try { - return jansThinBridge.getUserByUsername(username); + log.infov("getUserByUsername(). Username: {0}",username); + JansPerson person = jansThinBridge.getJansUserByUsername(username); + if(person != null) { + return new JansUserModel(session,model,person); + } + return null; }catch(JansThinBridgeOperationException e) { log.errorv(e,"Error fetching jans user with username " + username); + return null; } - return null; } @Override public UserModel getUserByEmail(RealmModel realm, String email) { try { - return jansThinBridge.getUserByEmail(email); + log.infov("getUserByEmail(). Email : {0}",email); + JansPerson person = jansThinBridge.getJansUserByEmail(email); + if(person != null) { + return new JansUserModel(session,model,person); + } + return null; }catch(JansThinBridgeOperationException e) { log.errorv(e,"Error fetching jans user with email " + email); + return null; } - return null; } @Override public UserModel getUserById(RealmModel realm, String id) { try { + log.infov("getUserById(). Id: {0}",id); StorageId storageId = new StorageId(id); final String inum = storageId.getExternalId(); - return jansThinBridge.getUserByInum(inum); + JansPerson person = jansThinBridge.getJansUserByInum(inum); + if(person != null) { + return new JansUserModel(session,model,person); + } + return null; }catch(JansThinBridgeOperationException e) { log.errorv(e,"Error fetching jans user with id " + id); + return null; } - return null; } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java index d166fe103bb..76f42c0cdf1 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProviderFactory.java @@ -8,15 +8,17 @@ import io.jans.kc.spi.JansSpiInitException; import io.jans.kc.spi.custom.JansThinBridgeProvider; +import org.jboss.logging.Logger; + public class JansUserStorageProviderFactory implements UserStorageProviderFactory { private static final String PROVIDER_ID = ProviderIDs.JANS_USER_STORAGE_PROVIDER; + private static final Logger log = Logger.getLogger(JansUserStorageProviderFactory.class); @Override public String getId() { - //TODO implement this return PROVIDER_ID; } @@ -27,6 +29,6 @@ public JansUserStorageProvider create(KeycloakSession session, ComponentModel mo if(jansThinBridgeProvider == null) { throw new JansSpiInitException("Could not obtain reference to thin bridge provider"); } - return new JansUserStorageProvider(jansThinBridgeProvider); + return new JansUserStorageProvider(session,model,jansThinBridgeProvider); } } From b4459c16662720ce620b53e6b49d505c447709a0 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Wed, 19 Jun 2024 19:19:33 +0100 Subject: [PATCH 26/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * have scheduler create saml clients with document and assertion signing as default configuration Signed-off-by: Rolain Djeumen --- .../io/jans/kc/api/admin/client/model/ManagedSamlClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ManagedSamlClient.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ManagedSamlClient.java index e1ff0b209b6..0de4826711d 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ManagedSamlClient.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ManagedSamlClient.java @@ -152,8 +152,8 @@ private void initClientRepresentation() { clientRepresentation.setAuthenticationFlowBindingOverrides(authnFlowBindingOverrides); //set default saml attributes - samlShoulDocumentsBeSigned(false); - samlSignAssertions(false); + samlShoulDocumentsBeSigned(true); + samlSignAssertions(true); samlForcePostBinding(false); samlEncryptAssertions(false); samlForceArtifactBinding(false); From db62d0f9ba4bf990cbf3959aa0762a05c738d04d Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Thu, 20 Jun 2024 10:18:18 +0100 Subject: [PATCH 27/33] feat(jans-keycloak-integration): enhancement to jans-keycloak-integration #8614 * removed reference to protocol-mapper poc submodule Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 1 - .../protocol-mapper/pom.xml | 124 ---------- .../src/assembly/dependencies.xml | 24 -- .../protocol/mapper/SamlProtocolMapper.java | 223 ------------------ .../PersistenceConfigurationException.java | 13 - .../PersistenceConfigurationFactory.java | 158 ------------- .../kc/protocol/mapper/model/JansPerson.java | 86 ------- .../org.keycloak.protocol.ProtocolMapper | 1 - .../src/main/resources/assembly/.DONOTDELETE | 1 - 9 files changed, 631 deletions(-) delete mode 100644 jans-keycloak-integration/protocol-mapper/pom.xml delete mode 100644 jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper delete mode 100644 jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index ed385c3afd9..22c49f0ce6c 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -59,7 +59,6 @@ authenticator storage-spi - job-scheduler spi diff --git a/jans-keycloak-integration/protocol-mapper/pom.xml b/jans-keycloak-integration/protocol-mapper/pom.xml deleted file mode 100644 index 6fce624d2ae..00000000000 --- a/jans-keycloak-integration/protocol-mapper/pom.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - 4.0.0 - io.jans - kc-jans-protocol-mapper - kc-jans-protocol-mapper - jar - - - io.jans - jans-kc-parent - 1.1.2-SNAPSHOT - - - - ${maven.min-version} - - - - - - - org.keycloak - keycloak-core - - - - org.keycloak - keycloak-server-spi - - - - org.keycloak - keycloak-server-spi-private - - - - org.keycloak - keycloak-services - - - - org.keycloak - keycloak-saml-core-public - - - - - - io.jans - jans-core-standalone - - - - io.jans - jans-core-service - - - - io.jans - jans-orm-standalone - - - - io.jans - jans-orm-couchbase - - - - io.jans - jans-orm-hybrid - - - - io.jans - jans-orm-ldap - - - - io.jans - jans-orm-sql - - - - - - org.jboss.slf4j - slf4j-jboss-logmanager - 2.0.1.Final - - - - - org.apache.commons - commons-dbcp2 - 2.12.0 - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - - diff --git a/jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml b/jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml deleted file mode 100644 index 373f6f1d362..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/assembly/dependencies.xml +++ /dev/null @@ -1,24 +0,0 @@ - - deps - - zip - - false - - - target/deps/ - . - - *.jar - - - - src/main/resources/assembly - . - - *.DONOTDELETE - - - - \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java deleted file mode 100644 index fc514247916..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/SamlProtocolMapper.java +++ /dev/null @@ -1,223 +0,0 @@ -package io.jans.kc.protocol.mapper; - -import java.util.ArrayList; -import java.util.List; - -import io.jans.orm.search.filter.Filter; - -import org.jboss.logging.Logger; - -import org.keycloak.Config; - -import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.dom.saml.v2.assertion.AttributeType; - -import org.keycloak.models.AuthenticatedClientSessionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.UserSessionModel; - -import org.keycloak.protocol.saml.mappers.AttributeStatementHelper; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; - -import io.jans.orm.model.base.CustomObjectAttribute; -import io.jans.kc.protocol.mapper.config.PersistenceConfigurationException; -import io.jans.kc.protocol.mapper.config.PersistenceConfigurationFactory; -import io.jans.kc.protocol.mapper.model.JansPerson; -import io.jans.model.JansAttribute; -import io.jans.model.GluuStatus; -import io.jans.orm.PersistenceEntryManager; - -import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper; -import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper; - -import org.keycloak.saml.common.constants.JBossSAMLURIConstants; - - - -public class SamlProtocolMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper { - - private static final String DISPLAY_TYPE = "Janssen User Attribute"; - private static final String DISPLAY_CATEGORY = AttributeStatementHelper.ATTRIBUTE_STATEMENT_CATEGORY; - private static final String HELP_TEXT = "Maps a Janssen User's Attribute to a SAML Attribute"; - private static final String PROVIDER_ID = "kc-jans-saml-protocol-mapper"; - - //properties - private static final String JANS_ATTR_NAME_PROP_NAME = "jans.attribute.name"; - private static final String JANS_ATTR_NAME_PROP_LABEL = "Jans Attribute"; - private static final String JANS_ATTR_NAME_PROP_HELPTEXT = "Name of the Attribute in Janssen Auth Server"; - private static final List configProperties; - - - private static final Logger log = Logger.getLogger(SamlProtocolMapper.class); - - private final PersistenceConfigurationFactory persistenceConfigurationFactory; - - static { - - - configProperties = ProviderConfigurationBuilder.create() - .property() - .name(JANS_ATTR_NAME_PROP_NAME) - .label(JANS_ATTR_NAME_PROP_LABEL) - .helpText(JANS_ATTR_NAME_PROP_HELPTEXT) - .type(ProviderConfigProperty.STRING_TYPE) - .defaultValue(null) - .required(true) - .add() - .build(); - } - - public SamlProtocolMapper() { - - try { - persistenceConfigurationFactory = PersistenceConfigurationFactory.create(); - PersistenceEntryManager persistenceEntryManager = persistenceConfigurationFactory.getPersistenceEntryManager(); - - }catch(PersistenceConfigurationException e) { - throw new RuntimeException("Could not instantiate protocol mapper",e); - } - } - - @Override - public void close() { - - - } - - @Override - public List getConfigProperties() { - - return configProperties; - } - - @Override - public String getId() { - - return PROVIDER_ID; - } - - @Override - public String getDisplayType() { - - return DISPLAY_TYPE; - } - - @Override - public String getDisplayCategory() { - - return DISPLAY_CATEGORY; - } - - @Override - public String getHelpText() { - - return HELP_TEXT; - } - - @Override - public void init(Config.Scope scope) { - - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - - } - - @Override - public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { - - final String attributename = mappingModel.getConfig().get(JANS_ATTR_NAME_PROP_NAME); - log.infov("Transform attribute statement. Attribute name: {0}",attributename); - JansAttribute attr = findJansAttributeByName(attributename); - if(attr == null) { - log.infov("No attribute found by name : {0}. No transformation to effect",attributename); - return; - } - - if(attr.getStatus() != GluuStatus.ACTIVE) { - log.infov("Attribute {0} disabled. Skipping it for transformAttributeStatement()"); - return; - } - JansPerson person = findJansPersonByUsername(userSession.getLoginUsername(),new String[] {attributename}); - if(person == null) { - log.infov("No jans User associated with this keycloak session's user {0}",userSession.getLoginUsername()); - return; - } - addJansAttributeValueFromPerson(attr,person,attributeStatement,mappingModel,userSession); - } - - private PersistenceEntryManager getPersistenceEntryManager() { - - return persistenceConfigurationFactory.getPersistenceEntryManager(); - } - - private void addJansAttributeValueFromPerson(JansAttribute jansAttribute, JansPerson jansPerson, - AttributeStatementType attributeStatement, ProtocolMapperModel protocolMapper,UserSessionModel userSession) { - - if(!jansPerson.hasCustomAttributes()) { - log.infov("Jans User with keycloak login username {0} returned no custom attributes.",userSession.getLoginUsername()); - return; - } - - AttributeType attributeType = createAttributeType(protocolMapper, jansAttribute); - List values = jansPerson.customAttributeValues(jansAttribute); - if(values == null) { - log.infov("Jans user with keycloak login username {0} returned no values for attribute {1}", - userSession.getLoginUsername(),jansAttribute.getName()); - return; - } - - values.forEach(attributeType::addAttributeValue); - attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType)); - } - - private JansAttribute findJansAttributeByName(final String jansAttrName) { - - final String [] attrs = new String [] { - "displayName", - "jansAttrTyp", - "jansClaimName", - "jansSAML1URI", - "jansSAML2URI", - "jansStatus", - "jansAttrName" - }; - - final Filter filter = Filter.createEqualityFilter("jansAttrName",jansAttrName); - return getPersistenceEntryManager().findEntries("ou=attributes,o=jans",JansAttribute.class,filter,attrs).get(0); - } - - private JansPerson findJansPersonByUsername(final String username, final String [] returnattributes) { - - final Filter uidsearchfilter = Filter.createEqualityFilter("uid",username); - final Filter mailsearchfilter = Filter.createEqualityFilter("mail",username); - final Filter usersearchfilter = Filter.createORFilter(uidsearchfilter,mailsearchfilter); - - return getPersistenceEntryManager().findEntries("ou=people,o=jans",JansPerson.class,usersearchfilter,returnattributes).get(0); - } - - private AttributeType createAttributeType(ProtocolMapperModel model, JansAttribute jansAttributeMeta) { - - String attributeName = jansAttributeMeta.getSaml2Uri(); - String attributeNameFormat = JBossSAMLURIConstants.ATTRIBUTE_FORMAT_URI.get(); - if(jansAttributeMeta.getSaml2Uri() == null || jansAttributeMeta.getSaml1Uri().isEmpty()) { - attributeName = jansAttributeMeta.getName(); - attributeNameFormat = JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get(); - } - - AttributeType ret = new AttributeType(attributeName); - ret.setNameFormat(attributeNameFormat); - if(jansAttributeMeta.getDisplayName() != null && !jansAttributeMeta.getDisplayName().trim().isEmpty()) { - ret.setFriendlyName(jansAttributeMeta.getDisplayName()); - } - return ret; - } - -} \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java deleted file mode 100644 index 6216727822d..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationException.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jans.kc.protocol.mapper.config; - -public class PersistenceConfigurationException extends RuntimeException { - - - public PersistenceConfigurationException(String msg) { - super(msg); - } - - public PersistenceConfigurationException(String msg, Throwable cause) { - super(msg,cause); - } -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java deleted file mode 100644 index 2769907a433..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/config/PersistenceConfigurationFactory.java +++ /dev/null @@ -1,158 +0,0 @@ -package io.jans.kc.protocol.mapper.config; - -import io.jans.orm.PersistenceEntryManager; -import io.jans.orm.PersistenceEntryManagerFactory; -import io.jans.orm.model.PersistenceConfiguration; - -import io.jans.orm.service.PersistanceFactoryService; -import io.jans.orm.service.StandalonePersistanceFactoryService; -import io.jans.util.StringHelper; -import io.jans.util.exception.ConfigurationException; -import io.jans.orm.util.properties.FileConfiguration; -import io.jans.util.security.PropertiesDecrypter; -import io.jans.util.security.StringEncrypter; - - -import java.io.File; -import java.util.Properties; - -import org.apache.commons.lang.StringUtils; - -import org.jboss.logging.Logger; - -public class PersistenceConfigurationFactory { - - - private static final Logger log = Logger.getLogger(PersistenceConfigurationFactory.class); - private static final String DEFAULT_CONFIG_FILE_NAME = "jans.properties"; - private static final String SALT_FILE_NAME = "salt"; - - private final PersistanceFactoryService persistenceFactoryService; - private final PersistenceConfiguration persistenceConfig; - private final PersistenceEntryManager persistenceEntryManager; - - public PersistenceConfigurationFactory(final PersistanceFactoryService persistenceFactoryService, - final PersistenceConfiguration persistenceConfig, final PersistenceEntryManager persistenceEntryManager) { - - this.persistenceFactoryService = persistenceFactoryService; - this.persistenceConfig = persistenceConfig; - this.persistenceEntryManager = persistenceEntryManager; - } - - public final PersistenceEntryManager getPersistenceEntryManager() { - - return persistenceEntryManager; - } - - public static PersistenceConfigurationFactory create() { - - PersistanceFactoryService persistenceFactoryService = new StandalonePersistanceFactoryService(); - PersistenceConfiguration config = persistenceFactoryService.loadPersistenceConfiguration(getDefaultConfigurationFileName()); - if(config == null) { - - throw new PersistenceConfigurationException("Failed to load persistence configuration from file. " + - "\n+ Jans configuration base directory: " + getJansConfigurationBaseDir() + - "\n+ Jans configuration default file: " + getDefaultConfigurationFileName()); - } - - FileConfiguration baseConfiguration = loadBaseConfiguration(); - final String jansConfigBaseDir = getJansConfigurationBaseDir(); - String confdir = config.getConfiguration().getString("confDir"); - if(!StringUtils.isNotBlank(confdir)) { - confdir = getJansConfigurationBaseDir() + File.separator + "conf" + File.separator; - } - final String salt = cryptographicSaltFromFile(confdir+SALT_FILE_NAME); - if(!StringUtils.isNotBlank(salt)) { - throw new PersistenceConfigurationException("Failed to load cryptographic material from configuration"); - } - PersistenceEntryManager persistenceEntryManager = createPersistenceEntryManager(config, persistenceFactoryService, salt); - return new PersistenceConfigurationFactory(persistenceFactoryService,config,persistenceEntryManager); - } - - private static final String getDefaultConfigurationFileName() { - - return DEFAULT_CONFIG_FILE_NAME; - } - - private static final FileConfiguration loadBaseConfiguration() { - - return createFileConfiguration(DEFAULT_CONFIG_FILE_NAME,true); - } - - private static final FileConfiguration createFileConfiguration(String fileName, boolean mandatory) { - - try { - return new FileConfiguration(fileName); - }catch(Exception e) { - log.errorv(e,"Failed to load configuration from {0}",fileName); - if(mandatory && fileName != null) { - throw new PersistenceConfigurationException("Failed to load configuration from "+fileName,e); - }else if(mandatory && fileName == null) { - throw new PersistenceConfigurationException("Failed to load configuration because filename was invalid",e); - } - return null; - } - } - - private static final StringEncrypter createStringEncrypterFromSaltFile(final String path) { - - try { - final String salt = cryptographicSaltFromFile(path); - if(StringHelper.isEmpty(salt)) { - throw new PersistenceConfigurationException("Failed to create string encrypter. No cryptographic salt"); - } - return StringEncrypter.instance(salt); - }catch(StringEncrypter.EncryptionException e) { - throw new PersistenceConfigurationException("Failed to create string encrypter",e); - } - } - - private static final String cryptographicSaltFromFile(final String path) { - - try { - FileConfiguration cryptoconfig = new FileConfiguration(path); - return cryptoconfig.getString("encodeSalt"); - }catch(Exception e){ - log.errorv(e,"Failed to load cryptographic salt from {}",path); - throw new PersistenceConfigurationException("Failed to load cryptographic salt from " + path,e); - } - } - - private static final Properties preparePersistenceProperties(final PersistenceConfiguration persistenceConfiguration, final String salt) { - - try { - FileConfiguration config = persistenceConfiguration.getConfiguration(); - Properties connprops = (Properties) config.getProperties(); - return PropertiesDecrypter.decryptAllProperties(StringEncrypter.defaultInstance(),connprops,salt); - }catch(StringEncrypter.EncryptionException e) { - throw new PersistenceConfigurationException("Failed to decrypt persistence connection parameters",e); - } - } - - private static final PersistenceEntryManager createPersistenceEntryManager(final PersistenceConfiguration config, - final PersistanceFactoryService persistenceFactoryService, final String salt) { - - try { - Properties persistenceconnprops = preparePersistenceProperties(config,salt); - PersistenceEntryManagerFactory persistenceEntryManagerFactory = persistenceFactoryService.getPersistenceEntryManagerFactory(config); - return persistenceEntryManagerFactory.createEntryManager(persistenceconnprops); - }catch(Exception e) { - throw new PersistenceConfigurationException("Failed to create persistence entry manager",e); - } - } - - private static final String getJansConfigurationBaseDir() { - - if(System.getProperty("jans.base") != null) { - return System.getProperty("jans.base"); - }else if((System.getProperty("catalina.base") != null) && (System.getProperty("catalina.base.ignore") == null)) { - return System.getProperty("catalina.base"); - }else if(System.getProperty("catalina.home") != null) { - return System.getProperty("catalina.home"); - }else if(System.getProperty("jboss.home.dir") != null) { - return System.getProperty("jboss.home.dir"); - } - - return null; - } -} diff --git a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java b/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java deleted file mode 100644 index b7b7ab12ccd..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/java/io/jans/kc/protocol/mapper/model/JansPerson.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.jans.kc.protocol.mapper.model; - -import java.io.Serializable; - -import java.util.ArrayList; -import java.util.List; - -import io.jans.model.JansAttribute; -import io.jans.orm.annotation.*; -import io.jans.orm.model.base.CustomObjectAttribute; - -@DataEntry -@ObjectClass(value="jansPerson") -public class JansPerson implements Serializable { - - private static final long serialVersionUID = 1L; - - @DN - private String dn; - - @AttributesList(name="name",value="values",multiValued="multiValued") - private List customAttributes = new ArrayList<>(); - - - public JansPerson() { - - } - - public String getDn() { - - return this.dn; - } - - public void setDn(final String dn) { - - this.dn = dn; - } - - public void setCustomAttributes(List customAttributes) { - - this.customAttributes = customAttributes; - } - - public List getCustomAttributes() { - - return this.customAttributes; - } - - public boolean hasCustomAttributes() { - - return (this.customAttributes != null && !this.customAttributes.isEmpty()); - } - - public boolean isMultiValuedCustomAttributes() { - - return hasCustomAttributes() && this.customAttributes.size() > 1; - } - - public List customAttributeValues(final JansAttribute attributeMeta) { - - - for(CustomObjectAttribute customAttribute : customAttributes) { - if(customAttribute.getName().equals(attributeMeta.getName())) { - List values = customAttribute.getValues(); - if(values == null || values.size() == 0) { - return null; - } - return convertToString(values,attributeMeta); - } - } - return null; - } - - private List convertToString(List values, final JansAttribute attributeMeta) { - - List ret = new ArrayList<>(); - for(Object val : values) { - if(val instanceof String) { - ret.add((String) val); - }else { - ret.add(val.toString()); - } - } - return ret; - } -} \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper deleted file mode 100644 index 62078d8daf0..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ /dev/null @@ -1 +0,0 @@ -io.jans.kc.protocol.mapper.SamlProtocolMapper \ No newline at end of file diff --git a/jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE b/jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE deleted file mode 100644 index c7c1c13b13c..00000000000 --- a/jans-keycloak-integration/protocol-mapper/src/main/resources/assembly/.DONOTDELETE +++ /dev/null @@ -1 +0,0 @@ -This file is used to prevent build errors during the run of the maven assembly plugin \ No newline at end of file From c4054772f6a7a4d32ab5c8be5f1e3cb9670b7a3e Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Thu, 20 Jun 2024 16:13:27 +0100 Subject: [PATCH 28/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * removed reference to storage-spi module * restored job-scheduler module in build pom Signed-off-by: Rolain Djeumen --- jans-keycloak-integration/pom.xml | 2 +- .../storage-spi/.gitignore | 36 --- jans-keycloak-integration/storage-spi/pom.xml | 97 ------- .../storage-spi/src/assembly/dependencies.xml | 17 -- .../storage/config/PluginConfiguration.java | 84 ------ .../exception/JansConfigurationException.java | 27 -- .../CredentialAuthenticatingService.java | 42 --- .../service/RemoteUserStorageProvider.java | 179 ------------ .../RemoteUserStorageProviderFactory.java | 57 ---- .../kc/spi/storage/service/ScimService.java | 187 ------------ .../kc/spi/storage/service/UserAdapter.java | 141 ---------- .../service/UsersApiLegacyService.java | 66 ----- .../jans/kc/spi/storage/util/Constants.java | 34 --- .../kc/spi/storage/util/JansDataUtil.java | 107 ------- .../io/jans/kc/spi/storage/util/JansUtil.java | 266 ------------------ ...eycloak.storage.UserStorageProviderFactory | 1 - 16 files changed, 1 insertion(+), 1342 deletions(-) delete mode 100644 jans-keycloak-integration/storage-spi/.gitignore delete mode 100644 jans-keycloak-integration/storage-spi/pom.xml delete mode 100644 jans-keycloak-integration/storage-spi/src/assembly/dependencies.xml delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/config/PluginConfiguration.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/exception/JansConfigurationException.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/CredentialAuthenticatingService.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProvider.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProviderFactory.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/ScimService.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UserAdapter.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UsersApiLegacyService.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/Constants.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansDataUtil.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansUtil.java delete mode 100644 jans-keycloak-integration/storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index 22c49f0ce6c..1502fc74538 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -58,7 +58,7 @@ authenticator - storage-spi + job-scheduler spi diff --git a/jans-keycloak-integration/storage-spi/.gitignore b/jans-keycloak-integration/storage-spi/.gitignore deleted file mode 100644 index c53194b720c..00000000000 --- a/jans-keycloak-integration/storage-spi/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# Eclipse -.project -jans-config-api-access*.log -.classpath -.settings/ -bin/ - -# IntelliJ -.idea -*.ipr -*.iml -*.iws - -# NetBeans -nb-configuration.xml - -# Visual Studio Code -.vscode - -# OSX -.DS_Store - -# Vim -*.swp -*.swo - -# patch -*.orig -*.rej - -# Maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -release.properties diff --git a/jans-keycloak-integration/storage-spi/pom.xml b/jans-keycloak-integration/storage-spi/pom.xml deleted file mode 100644 index 5add606ecf0..00000000000 --- a/jans-keycloak-integration/storage-spi/pom.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - 4.0.0 - kc-jans-storage-plugin - kc-jans-storage-plugin - jar - - - io.jans - jans-kc-parent - 1.1.3-SNAPSHOT - - - - - - - io.jans - jans-scim-model - - - - - jakarta.ws.rs - jakarta.ws.rs-api - - - jakarta.servlet - jakarta.servlet-api - - - - - org.keycloak - keycloak-core - - - org.keycloak - keycloak-server-spi - - - org.keycloak - keycloak-model-legacy - - - - - org.apache.commons - commons-collections4 - - - org.apache.commons - commons-lang3 - - - - - com.fasterxml.jackson.jaxrs - jackson-jaxrs-base - - - com.fasterxml.jackson.jaxrs - jackson-jaxrs-json-provider - - - - - - io.swagger.core.v3 - swagger-core-jakarta - - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.apache.maven.plugins - maven-assembly-plugin - - - - - - diff --git a/jans-keycloak-integration/storage-spi/src/assembly/dependencies.xml b/jans-keycloak-integration/storage-spi/src/assembly/dependencies.xml deleted file mode 100644 index 392faa3826e..00000000000 --- a/jans-keycloak-integration/storage-spi/src/assembly/dependencies.xml +++ /dev/null @@ -1,17 +0,0 @@ - - deps - - zip - - false - - - target/deps/ - . - - *.jar - - - - \ No newline at end of file diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/config/PluginConfiguration.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/config/PluginConfiguration.java deleted file mode 100644 index 77950fe6c11..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/config/PluginConfiguration.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.jans.kc.spi.storage.config; - -import java.util.Arrays; -import java.util.ArrayList; -import java.util.List; -import org.keycloak.Config; - -public class PluginConfiguration { - - private static final String AUTH_TOKEN_ENDPOINT_KEY = "auth-token-endpoint"; - private static final String SCIM_USER_ENDPOINT_KEY = "scim-user-endpoint"; - private static final String SCIM_USER_SEARCH_ENDPOINT_KEY = "scim-user-search-endpoint"; - private static final String SCIM_OAUTH_SCOPES_KEY = "scim-oauth-scopes"; - private static final String SCIM_CLIENT_ID_KEY = "scim-client-id"; - private static final String SCIM_CLIENT_SECRET = "scim-client-secret"; - - private String authTokenEndpoint; - private String scimUserEndpoint; - private String scimUserSearchEndpoint; - private List scimOauthScopes; - private String scimClientId; - private String scimClientSecret; - - private PluginConfiguration() { - - } - - public static PluginConfiguration fromKeycloakConfiguration(Config.Scope config) { - - PluginConfiguration ret = new PluginConfiguration(); - ret.authTokenEndpoint = config.get(AUTH_TOKEN_ENDPOINT_KEY); - ret.scimUserEndpoint = config.get(SCIM_USER_ENDPOINT_KEY); - ret.scimUserSearchEndpoint = config.get(SCIM_USER_SEARCH_ENDPOINT_KEY); - ret.scimOauthScopes = new ArrayList<>(); - String tmpscopes = config.get(SCIM_OAUTH_SCOPES_KEY); - if(tmpscopes != null) { - ret.scimOauthScopes = Arrays.asList(tmpscopes.split(",")); - } - ret.scimClientId = config.get(SCIM_CLIENT_ID_KEY); - ret.scimClientSecret = config.get(SCIM_CLIENT_SECRET); - return ret; - } - - public String getAuthTokenEndpoint() { - - return authTokenEndpoint; - } - - public String getScimUserEndpoint() { - - return scimUserEndpoint; - } - - - public String getScimUserSearchEndpoint() { - - return scimUserSearchEndpoint; - } - - public List getScimOauthScopes() { - - return scimOauthScopes; - } - - public String getScimClientId() { - - return scimClientId; - } - - public String getScimClientSecret() { - - return scimClientSecret; - } - - public boolean isValid() { - - return authTokenEndpoint != null - && scimUserEndpoint != null - && scimUserSearchEndpoint != null - && scimOauthScopes != null - && scimClientId != null - && scimClientSecret != null; - } -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/exception/JansConfigurationException.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/exception/JansConfigurationException.java deleted file mode 100644 index e4461547a96..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/exception/JansConfigurationException.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Janssen Project software is available under the Apache License (2004). See http://www.apache.org/licenses/ for full text. - * - * Copyright (c) 2020, Janssen Project - */ - -package io.jans.kc.spi.storage.exception; - -public class JansConfigurationException extends RuntimeException { - - - public JansConfigurationException() { - } - - public JansConfigurationException(String message) { - super(message); - } - - public JansConfigurationException(Throwable cause) { - super(cause); - } - - public JansConfigurationException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/CredentialAuthenticatingService.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/CredentialAuthenticatingService.java deleted file mode 100644 index 14fc98309c3..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/CredentialAuthenticatingService.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.jans.kc.spi.storage.service; - -import io.jans.kc.spi.storage.util.Constants; -import io.jans.kc.spi.storage.util.JansUtil; - -import jakarta.ws.rs.core.MediaType; - -import org.apache.commons.lang.StringUtils; - -import org.jboss.logging.Logger; - -public class CredentialAuthenticatingService { - - private static Logger log = Logger.getLogger(CredentialAuthenticatingService.class); - - private JansUtil jansUtil; - - public CredentialAuthenticatingService(JansUtil jansUtil) { - this.jansUtil = jansUtil; - } - - public boolean authenticateUser(final String username, final String password) { - log.debugv("CredentialAuthenticatingService::authenticateUser() - username:{0}, password:{1} ", username, - password); - boolean isValid = false; - try { - - String token = jansUtil.requestUserToken(jansUtil.getTokenEndpoint(), username, password, null, - Constants.RESOURCE_OWNER_PASSWORD_CREDENTIALS, null, MediaType.APPLICATION_FORM_URLENCODED); - - log.debugv("CredentialAuthenticatingService::authenticateUser() - Final token token - {0}", token); - - if (StringUtils.isNotBlank(token)) { - isValid = true; - } - } catch (Exception ex) { - log.debug("CredentialAuthenticatingService::authenticateUser() - Error while authenticating", ex); - } - return isValid; - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProvider.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProvider.java deleted file mode 100644 index 3a70142e225..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProvider.java +++ /dev/null @@ -1,179 +0,0 @@ -package io.jans.kc.spi.storage.service; - -import io.jans.kc.spi.storage.config.PluginConfiguration; -import io.jans.kc.spi.storage.util.JansUtil; -import io.jans.scim.model.scim2.user.UserResource; - -import org.keycloak.component.ComponentModel; -import org.keycloak.credential.CredentialInput; -import org.keycloak.credential.CredentialInputValidator; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.credential.PasswordCredentialModel; -import org.keycloak.storage.StorageId; -import org.keycloak.storage.UserStorageProvider; -import org.keycloak.storage.user.UserLookupProvider; - -import org.jboss.logging.Logger; - - -public class RemoteUserStorageProvider implements CredentialInputValidator, UserLookupProvider, UserStorageProvider { - - private static Logger log = Logger.getLogger(RemoteUserStorageProvider.class); - - private KeycloakSession session; - private ComponentModel model; - private UsersApiLegacyService usersService; - private CredentialAuthenticatingService credentialAuthenticatingService; - - public RemoteUserStorageProvider(KeycloakSession session, ComponentModel model, PluginConfiguration pluginConfiguration) { - log.debugv("RemoteUserStorageProvider() - session:{0}, model:{1}", session, model); - JansUtil jansUtil = new JansUtil(pluginConfiguration); - this.session = session; - this.model = model; - this.usersService = new UsersApiLegacyService(session, model,new ScimService(jansUtil)); - this.credentialAuthenticatingService = new CredentialAuthenticatingService(jansUtil); - } - - @Override - public boolean supportsCredentialType(String credentialType) { - log.debugv("RemoteUserStorageProvider::supportsCredentialType() - credentialType:{0}", credentialType); - return PasswordCredentialModel.TYPE.equals(credentialType); - } - - @Override - public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { - log.debugv("RemoteUserStorageProvider::isConfiguredFor() - realm:{0}, user:{1}, credentialType:{2} ", realm, user, - credentialType); - return user.credentialManager().isConfiguredFor(credentialType); - } - - @Override - public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { - log.debugv( - "RemoteUserStorageProvider::isValid() - realm:{0}, user:{1}, credentialInput:{2}, user.getUsername():{2}, credentialInput.getChallengeResponse():{}", - realm, user, credentialInput, user.getUsername(), credentialInput.getChallengeResponse()); - - boolean valid = credentialAuthenticatingService.authenticateUser(user.getUsername(), - credentialInput.getChallengeResponse()); - - log.debugv("RemoteUserStorageProvider::isValid() - valid:{0}", valid); - - return valid; - - } - - /** - * Get user based on id - */ - public UserModel getUserById(RealmModel paramRealmModel, String id) { - log.debugv("RemoteUserStorageProvider::getUserById() - paramRealmModel:{0}, id:{1}", paramRealmModel, id); - - UserModel userModel = null; - try { - UserResource user = usersService.getUserById(StorageId.externalId(id)); - log.debugv("RemoteUserStorageProvider::getUserById() - user fetched based on id:{0} is user:{1}", id, user); - if (user != null) { - userModel = createUserModel(paramRealmModel, user); - log.debugv(" RemoteUserStorageProvider::getUserById() - userModel:{0}", userModel); - - if (userModel != null) { - log.debugv( - "RemoteUserStorageProvider::getUserById() - Final userModel fetched with id:{0}, userModel:{1}, userModel.getAttributes(:{2})", - id, userModel, userModel.getAttributes()); - } - } - - log.debugv( - "RemoteUserStorageProvider::getUserById() - User fetched with id:{0} from external service is:{1}", - id, user); - - } catch (Exception ex) { - log.errorv(ex, - "RemoteUserStorageProvider::getUserById() - Error fetching user id:{0} from external service", - id); - - } - - return userModel; - } - - /** - * Get user based on name - */ - public UserModel getUserByUsername(RealmModel paramRealmModel, String name) { - log.debugv("RemoteUserStorageProvider::getUserByUsername() - paramRealmModel:{0}, name:{1}", paramRealmModel, - name); - - UserModel userModel = null; - try { - UserResource user = usersService.getUserByName(name); - log.debugv( - "RemoteUserStorageProvider::getUserByUsername() - User fetched with name:{0} from external service is:{1}", - name, user); - - if (user != null) { - userModel = createUserModel(paramRealmModel, user); - log.debugv("RemoteUserStorageProvider::getUserByUsername() - userModel:{0}", userModel); - } - if (userModel != null) { - log.debugv( - "RemoteUserStorageProvider::getUserByUsername() - Final User fetched with name:{0}, userModel:{1}, userModel.getAttributes():{2}", - name, userModel, userModel.getAttributes()); - } - - } catch (Exception ex) { - log.errorv(ex, - "\n RemoteUserStorageProvider::getUserByUsername() - Error fetching user name:{0}", - name); - - } - return userModel; - } - - public UserModel getUserByEmail(RealmModel paramRealmModel, String email) { - log.debugv("RemoteUserStorageProvider::getUserByEmail() - paramRealmModel:{0}, email:{1}", paramRealmModel, - email); - - UserModel userModel = null; - try { - UserResource user = usersService.getUserByEmail(email); - log.debugv( - "RemoteUserStorageProvider::getUserByEmail() - User fetched with email:{0} from external service is:{1}", - email, user); - - if (user != null) { - userModel = createUserModel(paramRealmModel, user); - log.debugv("RemoteUserStorageProvider::getUserByEmail() - userModel:{0}", userModel); - } - - if (userModel != null) { - log.debugv( - "RemoteUserStorageProvider::getUserByEmail() - Final User fetched with email:{0}, userModel:{1}, userModel.getAttributes(:{2})", - email, userModel, userModel.getAttributes()); - } - - } catch (Exception ex) { - log.errorv(ex, - "RemoteUserStorageProvider::getUserByEmail() - Error fetching user email:{0}", - email); - - } - return userModel; - } - - public void close() { - log.debug("RemoteUserStorageProvider::close()"); - - } - - private UserModel createUserModel(RealmModel realm, UserResource user) { - log.debugv("RemoteUserStorageProvider::createUserModel() - realm:{0} , user:{1}", realm, user); - - UserModel userModel = new UserAdapter(session, realm, model, user); - log.debugv("Final RemoteUserStorageProvider::createUserModel() - userModel:{0}", userModel); - - return userModel; - } -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProviderFactory.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProviderFactory.java deleted file mode 100644 index 77a1c8fbeae..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/RemoteUserStorageProviderFactory.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.jans.kc.spi.storage.service; - -import io.jans.kc.spi.storage.config.PluginConfiguration; -import io.jans.kc.spi.storage.util.Constants; - -import org.jboss.logging.Logger; - -import org.keycloak.Config; -import org.keycloak.component.ComponentModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.storage.UserStorageProviderFactory; - - - - -public class RemoteUserStorageProviderFactory implements UserStorageProviderFactory { - - private static Logger log = Logger.getLogger(RemoteUserStorageProviderFactory.class); - - public static final String PROVIDER_NAME = "jans-keycloak-storage-api"; - private PluginConfiguration pluginConfiguration; - - @Override - public RemoteUserStorageProvider create(KeycloakSession session, ComponentModel model) { - log.debugv("RemoteUserStorageProviderFactory::create() - session:{}, model:{}", session, model); - return new RemoteUserStorageProvider(session, model,pluginConfiguration); - } - - @Override - public String getId() { - - return Constants.PROVIDER_ID; - } - - @Override - public String getHelpText() { - return "Janssen User Storage Provider"; - } - - @Override - public void init(Config.Scope config) { - - this.pluginConfiguration = PluginConfiguration.fromKeycloakConfiguration(config); - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - log.debug("RemoteUserStorageProviderFactory::postInit()"); - } - - @Override - public void close() { - log.debug("RemoteUserStorageProviderFactory::close() - Exit"); - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/ScimService.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/ScimService.java deleted file mode 100644 index 13002f1de2f..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/ScimService.java +++ /dev/null @@ -1,187 +0,0 @@ -package io.jans.kc.spi.storage.service; - -import com.fasterxml.jackson.databind.JsonNode; - -import io.jans.kc.spi.storage.util.JansUtil; -import io.jans.scim.model.scim2.SearchRequest; -import io.jans.scim.model.scim2.user.UserResource; - -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClientBuilder; - -import org.jboss.logging.Logger; - -import org.keycloak.broker.provider.util.SimpleHttp; -import org.keycloak.util.JsonSerialization; - - -public class ScimService { - - private static Logger log = Logger.getLogger(ScimService.class); - - private JansUtil jansUtil; - - public ScimService(JansUtil jansUtil) { - this.jansUtil = jansUtil; - } - - private String getScimUserEndpoint() { - String scimUserEndpoint = jansUtil.getScimUserEndpoint(); - log.debugv("ScimService::getScimUserEndpoint() - scimUserEndpoint:{0}", scimUserEndpoint); - return scimUserEndpoint; - } - - private String getScimUserSearchEndpoint() { - String scimUserSearchEndpoint = jansUtil.getScimUserSearchEndpoint(); - log.debugv("ScimService::getScimUserSearchEndpoint() - scimUserSearchEndpoint:{0}", scimUserSearchEndpoint); - return scimUserSearchEndpoint; - } - - private String requestAccessToken() { - log.debug("ScimService::requestAccessToken()"); - String token = null; - - try { - token = jansUtil.requestScimAccessToken(); - log.debugv("ScimService::requestAccessToken() - token:{}", token); - } catch (Exception ex) { - log.errorv(ex,"ScimService::requestAccessToken() - Error while generating access token for SCIM"); - throw new RuntimeException( - "ScimService::requestAccessToken() - Error while generating access token for SCIM endpoint",ex); - } - return token; - } - - public UserResource getUserById(String inum) { - log.infov(" ScimService::getUserById() - inum:{0}", inum); - try { - return getData(getScimUserEndpoint() + "/" + inum, this.requestAccessToken()); - } catch (Exception ex) { - log.errorv(ex, - "ScimService::getUserById() - Error fetching user based on inum:{0} from external service", - inum); - } - return null; - } - - public UserResource getUserByName(String username) { - log.infov("ScimService::getUserByName() - username:{0}", username); - try { - - String filter = "userName eq \"" + username + "\""; - return postData(this.getScimUserSearchEndpoint(), this.requestAccessToken(), filter); - } catch (Exception ex) { - log.errorv(ex, - "ScimService::getUserByName() - Error fetching user based on username:{0} from external service", - username); - } - return null; - } - - public UserResource getUserByEmail(String email) { - log.debugv(" ScimService::getUserByEmail() - email:{}", email); - try { - - String filter = "emails[value eq \"" + email + "\"]"; - log.debugv(" ScimService::getUserByEmail() - filter:{}", filter); - return postData(this.getScimUserSearchEndpoint(), this.requestAccessToken(), filter); - } catch (Exception ex) { - log.errorv(ex, - " ScimService::getUserByEmail() - Error fetching user based on email:{0}", - email); - - } - return null; - } - - public UserResource postData(String uri, String accessToken, String filter) { - UserResource user = null; - log.debugv("ScimService::postData() - uri:{0}, accessToken:{1}, filter:{2}", uri, accessToken, filter); - try { - HttpClient client = HttpClientBuilder.create().build(); - - SearchRequest searchRequest = createSearchRequest(filter); - log.debugv("ScimService::postData() - client:{0}, searchRequest:{1}, accessToken:{2}", client, searchRequest.toString(), - accessToken); - - JsonNode jsonNode = SimpleHttp.doPost(uri, client).auth(accessToken).json(searchRequest).asJson(); - - log.debugv("\n\n ScimService::postData() - jsonNode:{0}", jsonNode); - - user = getUserResourceFromList(jsonNode); - - log.debugv("ScimService::postData() - user:{0}", user); - - } catch (Exception ex) { - log.errorv(ex,"ScimService::postData() - Error while fetching data"); - } - return user; - } - - public UserResource getData(String uri, String accessToken) { - UserResource user = null; - log.debugv("ScimService::getData() - uri:{0}, accessToken:{1}", uri, accessToken); - try { - HttpClient client = HttpClientBuilder.create().build(); - - JsonNode jsonNode = SimpleHttp.doGet(uri, client).auth(accessToken).asJson(); - - log.debugv("\n\n ScimService::getData() - jsonNode:{0}", jsonNode); - - user = getUserResource(jsonNode); - - log.debugv("ScimService::getData() - user:{}", user); - - } catch (Exception ex) { - log.errorv(ex,"\n\n ScimService::getData() - Error while fetching data"); - } - return user; - } - - private SearchRequest createSearchRequest(String filter) { - log.debugv("ScimService::createSearchRequest() - createSearchRequest() - filter:{0}", filter); - SearchRequest searchRequest = new SearchRequest(); - searchRequest.setFilter(filter); - - log.debugv(" ScimService::createSearchRequest() - searchRequest:{0}", searchRequest); - - return searchRequest; - } - - private UserResource getUserResourceFromList(JsonNode jsonNode) { - log.debugv(" \n\n ScimService::getUserResourceFromList() - jsonNode:{0}", jsonNode); - - UserResource user = null; - try { - if (jsonNode != null) { - if (jsonNode.get("Resources") != null) { - JsonNode value = jsonNode.get("Resources").get(0); - log.debugv("*** ScimService::getUserResourceFromList() - value:{0}, value.getClass():{1}", value, - value.getClass()); - user = JsonSerialization.readValue(JsonSerialization.writeValueAsBytes(value), UserResource.class); - log.debugv(" ScimService::getUserResourceFromList() - user:{0}, user.getClass():{1}", user, - user.getClass()); - } - } - } catch (Exception ex) { - log.errorv(ex,"\n\n ScimService::getUserResourceFromList() - Error while fetching data"); - } - return user; - } - - private UserResource getUserResource(JsonNode jsonNode) { - log.debugv("ScimService::getUserResource() - jsonNode:{0}", jsonNode); - - UserResource user = null; - try { - if (jsonNode != null) { - user = JsonSerialization.readValue(JsonSerialization.writeValueAsBytes(jsonNode), UserResource.class); - log.debugv(" ScimService::getUserResource() - user:{0}, user.getClass():{1}", user, user.getClass()); - } - } catch (Exception ex) { - log.errorv(ex,"\n\n ScimService::getUserResource() - Error while fetching data is ex:{}"); - } - return user; - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UserAdapter.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UserAdapter.java deleted file mode 100644 index 8e52331fb93..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UserAdapter.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.jans.kc.spi.storage.service; - -import io.jans.kc.spi.storage.util.JansDataUtil; -import io.jans.scim.model.scim2.user.UserResource; -import io.jans.scim.model.scim2.util.DateUtil; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import org.apache.commons.lang3.StringUtils; -import org.keycloak.common.util.MultivaluedHashMap; -import org.keycloak.component.ComponentModel; -import org.keycloak.credential.LegacyUserCredentialManager; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.SubjectCredentialManager; -import org.keycloak.models.UserModel; -import org.keycloak.storage.StorageId; -import org.keycloak.storage.adapter.AbstractUserAdapter; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class UserAdapter extends AbstractUserAdapter { - private static Logger logger = LoggerFactory.getLogger(UserAdapter.class); - private final UserResource user; - - public UserAdapter(KeycloakSession session, RealmModel realm, ComponentModel model, UserResource user) { - - super(session, realm, model); - logger.debug( - " UserAdapter() - model:{}, user:{}, storageProviderModel:{}, storageProviderModel.getId():{}, user.getId():{}", - model, user, storageProviderModel, storageProviderModel.getId(), user.getId()); - this.storageId = new StorageId(storageProviderModel.getId(), user.getId()); - this.user = user; - - logger.debug("UserAdapter() - All User Resource field():{}", printUserResourceField()); - } - - @Override - public String getUsername() { - return user.getUserName(); - } - - @Override - public String getFirstName() { - return user.getDisplayName(); - } - - @Override - public String getLastName() { - return user.getNickName(); - } - - @Override - public String getEmail() { - return ((user.getEmails() != null && user.getEmails().get(0) != null) ? user.getEmails().get(0).getValue() - : null); - } - - @Override - public SubjectCredentialManager credentialManager() { - return new LegacyUserCredentialManager(session, realm, this); - } - - public Map getCustomAttributes() { - printUserCustomAttributes(); - return user.getCustomAttributes(); - } - - @Override - public boolean isEnabled() { - boolean enabled = false; - if (user != null) { - enabled = user.getActive(); - } - return enabled; - } - - @Override - public Long getCreatedTimestamp() { - Long createdDate = null; - if (user.getMeta().getCreated() != null) { - String created = user.getMeta().getCreated(); - if (created != null && StringUtils.isNotBlank(created)) { - createdDate = DateUtil.ISOToMillis(created); - } - } - return createdDate; - } - - @Override - public Map> getAttributes() { - MultivaluedHashMap attributes = new MultivaluedHashMap<>(); - attributes.add(UserModel.USERNAME, getUsername()); - attributes.add(UserModel.EMAIL, getEmail()); - attributes.add(UserModel.FIRST_NAME, getFirstName()); - attributes.add(UserModel.LAST_NAME, getLastName()); - return attributes; - } - - @Override - public Stream getAttributeStream(String name) { - if (name.equals(UserModel.USERNAME)) { - return Stream.of(getUsername()); - } - return Stream.empty(); - } - - @Override - protected Set getRoleMappingsInternal() { - return Set.of(); - } - - private void printUserCustomAttributes() { - logger.info(" UserAdapter::printUserCustomAttributes() - user:{}", user); - if (user == null || user.getCustomAttributes() == null || user.getCustomAttributes().isEmpty()) { - return; - } - - user.getCustomAttributes().keySet().stream() - .forEach(key -> logger.info("key:{} , value:{}", key, user.getCustomAttributes().get(key))); - - } - - private Map printUserResourceField() { - logger.info(" UserAdapter::printUserResourceField() - user:{}", user); - Map propertyTypeMap = null; - if (user == null) { - return propertyTypeMap; - } - - propertyTypeMap = JansDataUtil.getFieldTypeMap(user.getClass()); - logger.info("UserAdapter::printUserResourceField() - all fields of user:{}", propertyTypeMap); - return propertyTypeMap; - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UsersApiLegacyService.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UsersApiLegacyService.java deleted file mode 100644 index 4bb8d75b2a6..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/service/UsersApiLegacyService.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.jans.kc.spi.storage.service; - -import io.jans.scim.model.scim2.user.UserResource; - -import java.util.Properties; - -import org.jboss.logging.Logger; - -import org.keycloak.component.ComponentModel; -import org.keycloak.models.KeycloakSession; - -public class UsersApiLegacyService { - - private static Logger log = Logger.getLogger(UsersApiLegacyService.class); - - private ScimService scimService; - protected Properties jansProperties = new Properties(); - - public UsersApiLegacyService(KeycloakSession session, ComponentModel model, ScimService scimService) { - - log.debugv(" UsersApiLegacyService() - session:{0}, model:{1}", session, model); - this.scimService = scimService; - } - - public UserResource getUserById(String inum) { - log.debugv("UsersApiLegacyService::getUserById() - inum:{0}", inum); - try { - return scimService.getUserById(inum); - } catch (Exception ex) { - log.errorv(ex, - "UsersApiLegacyService::getUserById() - Error fetching user based on inum:{0} from external service", - inum); - - } - return null; - } - - public UserResource getUserByName(String username) { - log.debugv(" UsersApiLegacyService::getUserByName() - username:{0}", username); - try { - - return scimService.getUserByName(username); - } catch (Exception ex) { - log.errorv(ex, - "UsersApiLegacyService::getUserByName() - Error fetching user based on username:{0} from external service", - username); - - } - return null; - } - - public UserResource getUserByEmail(String email) { - log.infov(" UsersApiLegacyService::getUserByEmail() - email:{0}", email); - try { - - return scimService.getUserByEmail(email); - } catch (Exception ex) { - log.errorv( - " UsersApiLegacyService::getUserByEmail() - Error fetching user based on email:{0}", - email); - - } - return null; - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/Constants.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/Constants.java deleted file mode 100644 index 4740e3c734f..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/Constants.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Janssen Project software is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text. - * - * Copyright (c) 2020, Janssen Project - */ - -package io.jans.kc.spi.storage.util; - -public class Constants { - - private Constants() {} - - public static final String PROVIDER_ID = "kc-jans-storage"; - public static final String JANS_CONFIG_PROP_PATH = "jans.config.prop.path"; - public static final String KEYCLOAK_USER = "/keycloak-user"; - public static final String BASE_URL = "https://localhost"; - - public static final String SCOPE_TYPE_OPENID = "openid"; - public static final String UTF8_STRING_ENCODING = "UTF-8"; - public static final String CLIENT_SECRET_BASIC = "client_secret_basic"; - public static final String CLIENT_CREDENTIALS = "client_credentials"; - public static final String RESOURCE_OWNER_PASSWORD_CREDENTIALS = "password"; - - //properties - public static final String KEYCLOAK_SERVER_URL = "keycloak.server.url"; - public static final String AUTH_TOKEN_ENDPOINT = "auth.token.endpoint"; - public static final String SCIM_USER_ENDPOINT = "scim.user.endpoint"; - public static final String SCIM_USER_SEARCH_ENDPOINT = "scim.user.search.endpoint"; - public static final String SCIM_OAUTH_SCOPE = "scim.oauth.scope"; - public static final String KEYCLOAK_SCIM_CLIENT_ID = "keycloak.scim.client.id"; - public static final String KEYCLOAK_SCIM_CLIENT_PASSWORD = "keycloak.scim.client.password"; - - -} \ No newline at end of file diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansDataUtil.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansDataUtil.java deleted file mode 100644 index 179dd9bebdd..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansDataUtil.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.jans.kc.spi.storage.util; - -import java.beans.IntrospectionException; -import java.beans.PropertyDescriptor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.jboss.logging.Logger; - -public class JansDataUtil { - - private static final Logger log = Logger.getLogger(JansDataUtil.class); - - public static Object invokeMethod(Class clazz, String methodName, Class... parameterTypes) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - log.debugv("JansDataUtil::invokeMethod() - Invoke clazz:{0} on methodName:{1} with name:{2} ", clazz, methodName, - parameterTypes); - Object obj = null; - if (clazz == null || methodName == null || parameterTypes == null) { - return obj; - } - Method m = clazz.getDeclaredMethod(methodName, parameterTypes); - obj = m.invoke(null, parameterTypes); - - log.debugv("JansDataUtil::invokeMethod() - methodName:{0} returned obj:{1} ", methodName, obj); - return obj; - } - - public Object invokeReflectionGetter(Object obj, String variableName) { - log.debugv("JansDataUtil::invokeMethod() - Invoke obj:{0}, variableName:{1}", obj, variableName); - try { - if (obj == null) { - return obj; - } - PropertyDescriptor pd = new PropertyDescriptor(variableName, obj.getClass()); - Method getter = pd.getReadMethod(); - log.debugv("JansDataUtil::invokeMethod() - Invoke getter:{0}", getter); - if (getter != null) { - return getter.invoke(obj); - } else { - log.errorv( - "JansDataUtil::invokeReflectionGetter() - Getter Method not found for class:{0} property:{1}", - obj.getClass().getName(), variableName); - } - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException - | IntrospectionException e) { - log.errorv(e,"JansDataUtil::invokeReflectionGetter() - Getter Method ERROR for class: {0} property: {1}", - obj.getClass().getName(), variableName); - } - return obj; - } - - public static List getAllFields(Class type) { - log.debugv("JansDataUtil::getAllFields() - type:{0} ", type); - List allFields = new ArrayList<>(); - if (type == null) { - return allFields; - } - getAllFields(allFields, type); - log.debugv("JansDataUtil::getAllFields() - Fields:{0} of type:{1} ", allFields, type); - return allFields; - } - - public static List getAllFields(List fields, Class type) { - log.debugv("JansDataUtil::getAllFields() - fields:{0} , type:{1} ", fields, type); - if (fields == null || type == null) { - return fields; - } - fields.addAll(Arrays.asList(type.getDeclaredFields())); - - if (type.getSuperclass() != null) { - getAllFields(fields, type.getSuperclass()); - } - log.debugv("JansDataUtil::getAllFields() - Final fields:{0} of type:{1} ", fields, type); - return fields; - } - - public static Map getFieldTypeMap(Class clazz) { - log.debugv("JansDataUtil::getFieldTypeMap() - clazz:{0} ", clazz); - Map propertyTypeMap = new HashMap<>(); - - if (clazz == null) { - return propertyTypeMap; - } - - List fields = getAllFields(clazz); - log.debugv("JansDataUtil::getFieldTypeMap() - all-fields:{0} ", fields); - - for (Field field : fields) { - log.debugv( - "JansDataUtil::getFieldTypeMap() - field:{0} , field.getAnnotatedType():{1}, field.getAnnotations():{2} , field.getType().getAnnotations():{3}, field.getType().getCanonicalName():{4} , field.getType().getClass():{5} , field.getType().getClasses():{6} , field.getType().getComponentType():{7}", - field, field.getAnnotatedType(), field.getAnnotations(), field.getType().getAnnotations(), - field.getType().getCanonicalName(), field.getType().getClass(), field.getType().getClasses(), - field.getType().getComponentType()); - propertyTypeMap.put(field.getName(), field.getType().getSimpleName()); - } - log.debugv("JansDataUtil::getFieldTypeMap() - Final propertyTypeMap{0} ", propertyTypeMap); - return propertyTypeMap; - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansUtil.java b/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansUtil.java deleted file mode 100644 index ff33cf8f614..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/java/io/jans/kc/spi/storage/util/JansUtil.java +++ /dev/null @@ -1,266 +0,0 @@ -package io.jans.kc.spi.storage.util; - -import com.fasterxml.jackson.databind.JsonNode; - -import io.jans.kc.spi.storage.config.PluginConfiguration; - -import jakarta.ws.rs.core.MediaType; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.stream.*; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClientBuilder; - -import org.jboss.logging.Logger; - -import org.keycloak.broker.provider.util.SimpleHttp; - -public class JansUtil { - - private static Logger log = Logger.getLogger(JansUtil.class); - private PluginConfiguration pluginConfiguration; - - public JansUtil(PluginConfiguration pluginConfiguration) { - - this.pluginConfiguration = pluginConfiguration; - if (this.pluginConfiguration == null || !this.pluginConfiguration.isValid()) { - throw new RuntimeException("Plugin configuration missing or invalid"); - } - } - - public String getTokenEndpoint() { - log.debugv("JansUtil::getTokenEndpoint() - {0}", - pluginConfiguration.getAuthTokenEndpoint()); - - return pluginConfiguration.getAuthTokenEndpoint(); - } - - public String getScimUserEndpoint() { - log.debugv("JansUtil::getScimUserEndpoint() - {0}", - pluginConfiguration.getScimUserEndpoint()); - - return pluginConfiguration.getScimUserEndpoint(); - } - - public String getScimUserSearchEndpoint() { - log.debugv( - "JansUtil::getScimUserSearchEndpoint() - {0}", - pluginConfiguration.getScimUserSearchEndpoint()); - return pluginConfiguration.getScimUserSearchEndpoint(); - } - - public String getScimClientId() { - log.debugv("JansUtil::getScimClientId() - {0}", - pluginConfiguration.getScimClientId()); - return pluginConfiguration.getScimClientId(); - } - - public String getScimClientSecret() { - log.debugv("JansUtil::getClientPassword() - {0}", - pluginConfiguration.getScimClientSecret()); - return pluginConfiguration.getScimClientSecret(); - } - - public List getScimOauthScopes() { - log.debugv("JansUtil::getScimOauthScope() - {0}", - pluginConfiguration.getScimOauthScopes()); - return pluginConfiguration.getScimOauthScopes(); - } - - public String requestScimAccessToken() throws IOException { - log.debug("JansUtil::requestScimAccessToken() "); - List scopes = getScimOauthScopes(); - String token = requestAccessToken(getScimClientId(), scopes); - log.debugv("JansUtil::requestScimAccessToken() - token:{0} ", token); - return token; - } - - public String requestAccessToken(final String clientId, final List scope) throws IOException { - log.debugv("JansUtil::requestAccessToken() - Request for AccessToken - clientId:{0}, scope:{1} ", clientId, - scope); - - String tokenUrl = getTokenEndpoint(); - String token = getAccessToken(tokenUrl, clientId, scope); - log.debugv("JansUtil::requestAccessToken() - oAuth AccessToken response - token:{0}", token); - - return token; - } - - public String getAccessToken(final String tokenUrl, final String clientId, final List scopes) - throws IOException { - log.debugv("JansUtil::getAccessToken() - Access Token Request - tokenUrl:{0}, clientId:{1}, scopes:{2}", tokenUrl, - clientId, scopes); - - // Get clientSecret - String clientSecret = getScimClientSecret(); - log.debugv("JansUtil::getAccessToken() - Access Token Request - clientId:{0}, clientSecret:{1}", clientId, - clientSecret); - - // distinct scopes - Set scopesSet = new HashSet<>(scopes); - StringBuilder scope = new StringBuilder(Constants.SCOPE_TYPE_OPENID); - for (String s : scopesSet) { - scope.append(" ").append(s); - } - - log.debugv("JansUtil::getAccessToken() - Scope required - {0}", scope); - - String token = requestAccessToken(tokenUrl, clientId, clientSecret, scope.toString(), - Constants.CLIENT_CREDENTIALS, Constants.CLIENT_SECRET_BASIC, MediaType.APPLICATION_FORM_URLENCODED); - log.debugv("JansUtil::getAccessToken() - Final token token - {0}", token); - return token; - } - - public String requestAccessToken(final String tokenUrl, final String clientId, final String clientSecret, - final String scope, String grantType, String authenticationMethod, String mediaType) throws IOException { - log.debugv( - "JansUtil::requestAccessToken() - Request for Access Token - tokenUrl:{0}, clientId:{1}, clientSecret:{2}, scope:{3}, grantType:{4}, authenticationMethod:{5}, mediaType:{6}", - tokenUrl, clientId, clientSecret, scope, grantType, authenticationMethod, mediaType); - String token = null; - try { - - log.debugv("JansUtil::requestAccessToken() - this.getEncodedCredentials():{0}", - this.getEncodedCredentials(clientId, clientSecret)); - HttpClient client = HttpClientBuilder.create().build(); - JsonNode jsonNode = SimpleHttp.doPost(tokenUrl, client) - .header("Authorization", "Basic " + this.getEncodedCredentials(clientId, clientSecret)) - .header("Content-Type", mediaType).param("grant_type", "client_credentials") - .param("username", clientId + ":" + clientSecret).param("scope", scope).param("client_id", clientId) - .param("client_secret", clientSecret).param("authorization_method", "client_secret_basic").asJson(); - log.debugv("JansUtil::requestAccessToken() - POST Request for Access Token - jsonNode:{0} ", jsonNode); - - token = this.getToken(jsonNode); - - log.debugv("\n JansUtil::requestAccessToken() - After Post request for Access Token - token:{0} ", token); - - } catch (Exception ex) { - log.error("JansUtil::requestAccessToken() - Post error is ",ex); - } - return token; - } - - public String requestUserToken(final String tokenUrl, final String username, final String password, - final String scope, String grantType, String authenticationMethod, String mediaType) throws IOException { - log.debugv( - "JansUtil::requestUserToken() - Request for Access Token - tokenUrl:{0}, username:{1}, password:{2}, scope:{3}, grantType:{4}, authenticationMethod:{5}, mediaType:{6}", - tokenUrl, username, password, scope, grantType, authenticationMethod, mediaType); - String token = null; - try { - String clientId = this.getScimClientId(); - String clientSecret = this.getScimClientSecret(); - - log.debugv( - " JansUtil::requestUserToken() - clientId:{0} , clientSecret:{1}, this.getEncodedCredentials():{2}", - clientId, clientSecret, this.getEncodedCredentials(clientId, clientSecret)); - HttpClient client = HttpClientBuilder.create().build(); - JsonNode jsonNode = SimpleHttp.doPost(tokenUrl, client) - .header("Authorization", "Basic " + this.getEncodedCredentials(clientId, clientSecret)) - .header("Content-Type", mediaType).param("grant_type", grantType).param("username", username) - .param("password", password).asJson(); - - log.debugv("JansUtil::requestUserToken() - After invoking post request for user token - jsonNode:{0}", - jsonNode); - - token = this.getToken(jsonNode); - - log.debugv("\n JansUtil::requestUserToken() -POST Request for Access Token - token:{0} ", token); - - } catch (Exception ex) { - log.errorv("\n JansUtil::requestUserToken() - Error getting user token", ex); - } - return token; - } - - private boolean validateTokenScope(JsonNode jsonNode, String scope) { - - log.debugv("JansUtil::validateTokenScope() - jsonNode:{0}, scope:{1}", jsonNode, scope); - boolean validScope = false; - try { - - List scopeList = Stream.of(scope.split(" ", -1)).collect(Collectors.toList()); - - if (jsonNode != null && jsonNode.get("scope") != null) { - JsonNode value = jsonNode.get("scope"); - log.debugv("JansUtil::validateTokenScope() - value:{0}", value); - - if (value != null) { - String responseScope = value.toString(); - log.debugv( - "JansUtil::validateTokenScope() - scope:{0}, responseScope:{1}, responseScope.contains(scope):{2}", - scope, responseScope, responseScope.contains(scope)); - if (scopeList.contains(responseScope)) { - validScope = true; - } - } - - } - log.debugv("JansUtil::validateTokenScope() - validScope:{0}", validScope); - - } catch (Exception ex) { - log.error("JansUtil::validateTokenScope() - Error while validating token scope from response is ", - ex); - } - return validScope; - - } - - private String getToken(JsonNode jsonNode) { - log.debugv("JansUtil::getToken() - jsonNode:{0}", jsonNode); - - String token = null; - try { - - if (jsonNode != null && jsonNode.get("access_token") != null) { - JsonNode value = jsonNode.get("access_token"); - log.debugv("JansUtil::getToken() - value:{0}", value); - - if (value != null) { - token = value.asText(); - } - log.debugv("getToken() - token:{0}", token); - } - } catch (Exception ex) { - log.errorv("Error while getting token from response", ex); - } - return token; - } - - private boolean hasCredentials(String authUsername, String authPassword) { - return (StringUtils.isNotBlank(authUsername) && StringUtils.isNotBlank(authPassword)); - } - - /** - * Returns the client credentials (URL encoded). - * - * @return The client credentials. - */ - private String getCredentials(String authUsername, String authPassword) throws UnsupportedEncodingException { - log.debugv("getCredentials() - authUsername:{0}, authPassword:{1}", authUsername, authPassword); - return URLEncoder.encode(authUsername, Constants.UTF8_STRING_ENCODING) + ":" - + URLEncoder.encode(authPassword, Constants.UTF8_STRING_ENCODING); - } - - private String getEncodedCredentials(String authUsername, String authPassword) { - log.debugv("getEncodedCredentials() - authUsername:{0}, authPassword:{1}", authUsername, authPassword); - try { - if (hasCredentials(authUsername, authPassword)) { - return Base64.encodeBase64String(getBytes(getCredentials(authUsername, authPassword))); - } - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - - return null; - } - - private static byte[] getBytes(String str) { - return str.getBytes(StandardCharsets.UTF_8); - } - -} diff --git a/jans-keycloak-integration/storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/jans-keycloak-integration/storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory deleted file mode 100644 index 2691b848dd3..00000000000 --- a/jans-keycloak-integration/storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory +++ /dev/null @@ -1 +0,0 @@ -io.jans.kc.spi.storage.service.RemoteUserStorageProviderFactory \ No newline at end of file From 0870995b989e17ab0b2cddb844e96c2281cb1e13 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Mon, 24 Jun 2024 07:20:35 +0100 Subject: [PATCH 29/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * removed authenticator source as it was moved to spi Signed-off-by: Rolain Djeumen --- .../authenticator/installation.md | 62 --- .../authenticator/pom.xml | 77 ---- .../src/assembly/dependencies.xml | 17 - .../main/java/io/jans/kc/spi/ProviderIDs.java | 6 - .../jans/kc/spi/auth/JansAuthenticator.java | 436 ------------------ .../spi/auth/JansAuthenticatorConfigProp.java | 75 --- .../kc/spi/auth/JansAuthenticatorFactory.java | 117 ----- .../jans/kc/spi/auth/SessionAttributes.java | 10 - .../kc/spi/auth/oidc/OIDCAccessToken.java | 5 - .../kc/spi/auth/oidc/OIDCAuthRequest.java | 86 ---- .../jans/kc/spi/auth/oidc/OIDCMetaCache.java | 6 - .../kc/spi/auth/oidc/OIDCMetaCacheKeys.java | 7 - .../jans/kc/spi/auth/oidc/OIDCMetaError.java | 12 - .../kc/spi/auth/oidc/OIDCRefreshToken.java | 5 - .../io/jans/kc/spi/auth/oidc/OIDCService.java | 13 - .../jans/kc/spi/auth/oidc/OIDCTokenError.java | 30 -- .../kc/spi/auth/oidc/OIDCTokenRequest.java | 40 -- .../spi/auth/oidc/OIDCTokenRequestError.java | 12 - .../kc/spi/auth/oidc/OIDCTokenResponse.java | 9 - .../kc/spi/auth/oidc/OIDCUserInfoError.java | 30 -- .../auth/oidc/OIDCUserInfoRequestError.java | 13 - .../spi/auth/oidc/OIDCUserInfoResponse.java | 9 - .../oidc/impl/HashBasedOIDCMetaCache.java | 139 ------ .../auth/oidc/impl/NimbusOIDCAccessToken.java | 20 - .../oidc/impl/NimbusOIDCRefreshToken.java | 15 - .../spi/auth/oidc/impl/NimbusOIDCService.java | 222 --------- .../oidc/impl/NimbusOIDCTokenResponse.java | 56 --- .../oidc/impl/NimbusOIDCUserInfoResponse.java | 47 -- .../JansAuthResponseResourceProvider.java | 125 ----- ...nsAuthResponseResourceProviderFactory.java | 42 -- ...ycloak.authentication.AuthenticatorFactory | 1 - ...ices.resource.RealmResourceProviderFactory | 1 - .../messages/messages_en.properties | 9 - ... project favicon transparent 50px 50px.png | Bin 9365 -> 0 bytes .../resources/img/janssen-project.jpg | Bin 88147 -> 0 bytes .../resources/img/janssen_dove_icon.jpg | Bin 36751 -> 0 bytes .../theme-resources/resources/js/jans-auth.js | 0 .../templates/jans-auth-error.ftl | 16 - .../templates/jans-auth-redirect.ftl | 37 -- .../templates/jans-auth-response-complete.ftl | 23 - .../templates/jans-auth-response-error.ftl | 16 - jans-keycloak-integration/pom.xml | 1 - 42 files changed, 1847 deletions(-) delete mode 100644 jans-keycloak-integration/authenticator/installation.md delete mode 100644 jans-keycloak-integration/authenticator/pom.xml delete mode 100644 jans-keycloak-integration/authenticator/src/assembly/dependencies.xml delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/ProviderIDs.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAccessToken.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAuthRequest.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCache.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCacheKeys.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaError.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCRefreshToken.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCService.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenError.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequest.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequestError.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenResponse.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoError.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoRequestError.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoResponse.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/HashBasedOIDCMetaCache.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCAccessToken.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCRefreshToken.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCService.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCTokenResponse.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCUserInfoResponse.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/messages/messages_en.properties delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen project favicon transparent 50px 50px.png delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen-project.jpg delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen_dove_icon.jpg delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/js/jans-auth.js delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-error.ftl delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl delete mode 100644 jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl diff --git a/jans-keycloak-integration/authenticator/installation.md b/jans-keycloak-integration/authenticator/installation.md deleted file mode 100644 index 7726c66a146..00000000000 --- a/jans-keycloak-integration/authenticator/installation.md +++ /dev/null @@ -1,62 +0,0 @@ -## Keycloak Installation configuration for use with Janssen Auth - -### 1- Brief - - This guide contains instructions on how to install keycloak for use with keycloak -and run it in a production setting alongside Janssen. - - -### 2- Keycloak and Plugins Installation - - We will be using the quarkus distribution of keycloak which can be found -[here](https://github.com/keycloak/keycloak/releases/download/22.0.3/keycloak-22.0.3.zip). -directory. -After downloading the binaries , it's suggested to unzip it in the `/opt/keycloak` directory. - -#### 2.1 - Keycloak Authentication Plugin Installation - -Installing the authentication plugin is straightforward. -It resides at the url -https://jenkins.jans.io/maven/io/jans/jans-authenticator// -Binaries of interest have to be copied to the -`/opt/keycloak/providers/` directory. They are: -- `kc-jans-authn-plugin-.jar` -- `kc-jans-authn-plugin--deps.zip`. It's contents have to -be unzipped into the directory. These are the plugin's dependencies. - -No further action is needed after copying these files. - - -### 3 - Running Keycloak - - The following assumptions will be made -- Keycloak has been installed under the directory `/opt/keycloak/` -- The Janssen Server's hostname is `janssen-with-kc.local` -- Keycloak will run behind a reverse proxy/ load balancer (e.g. apache ) - and will be listening only on the local interface on port 8092 - -From the terminal, run the following command -``` -/opt/keycloak/bin/kc.sh --log "console,file" --http-host=127.0.0.1 --http-port=8092 \ ---hostname-url=https://janssen-with-kc.local --spi-connections-http-client-default-disable-trust-manager=true \ ---proxy edge -``` - -#### 3.1 - Database Setup - By default , in a non-production environment , keycloak relies on the embedded H2 database for operation. -In a production setting, a more appropriate database needs to be deployed. -You can find a list of supported databases [here](https://www.keycloak.org/server/db). -Additional database configuration will need to be done. - - - -#### 3.2 - Reverse Proxy -As keycloak will run behind a proxy, there are a couple paths that need to be exposed (or not), with the full list -found [here](https://www.keycloak.org/server/reverseproxy). - - -### 5 - Configuration changes in Keycloak and Janssen-Auth -TBD - -### 6 - Clustering -TBD \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/pom.xml b/jans-keycloak-integration/authenticator/pom.xml deleted file mode 100644 index da1a2a3234c..00000000000 --- a/jans-keycloak-integration/authenticator/pom.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - 4.0.0 - io.jans - kc-jans-authn-plugin - kc-jans-authn-plugin - jar - - - io.jans - jans-kc-parent - 1.1.3-SNAPSHOT - - - - ${maven.min-version} - - - - - - - org.keycloak - keycloak-core - - - - org.keycloak - keycloak-server-spi - - - - org.keycloak - keycloak-server-spi-private - - - - org.keycloak - keycloak-services - - - - - - com.nimbusds - oauth2-oidc-sdk - - - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - - - org.apache.maven.plugins - maven-assembly-plugin - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - - - diff --git a/jans-keycloak-integration/authenticator/src/assembly/dependencies.xml b/jans-keycloak-integration/authenticator/src/assembly/dependencies.xml deleted file mode 100644 index 392faa3826e..00000000000 --- a/jans-keycloak-integration/authenticator/src/assembly/dependencies.xml +++ /dev/null @@ -1,17 +0,0 @@ - - deps - - zip - - false - - - target/deps/ - . - - *.jar - - - - \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/ProviderIDs.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/ProviderIDs.java deleted file mode 100644 index 6b63e573d94..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/ProviderIDs.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jans.kc.spi; - -public class ProviderIDs { - public static final String JANS_AUTHENTICATOR_PROVIDER = "kc-jans-authn"; - public static final String JANS_AUTH_RESPONSE_REST_PROVIDER = "kc-jans-authn-rest-bridge"; -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java deleted file mode 100644 index 1b583fc0ba3..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java +++ /dev/null @@ -1,436 +0,0 @@ -package io.jans.kc.spi.auth; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; - -import java.text.MessageFormat; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.ArrayList; - -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.UriBuilder; - -import org.jboss.logging.Logger; - -import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.authentication.AuthenticationFlowError; -import org.keycloak.authentication.RequiredActionFactory; -import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.KeycloakModelUtils; - -import io.jans.kc.spi.ProviderIDs; -import io.jans.kc.spi.auth.oidc.OIDCAuthRequest; -import io.jans.kc.spi.auth.oidc.OIDCMetaError; -import io.jans.kc.spi.auth.oidc.OIDCService; -import io.jans.kc.spi.auth.oidc.OIDCTokenError; -import io.jans.kc.spi.auth.oidc.OIDCTokenRequest; -import io.jans.kc.spi.auth.oidc.OIDCTokenRequestError; -import io.jans.kc.spi.auth.oidc.OIDCTokenResponse; -import io.jans.kc.spi.auth.oidc.OIDCUserInfoError; -import io.jans.kc.spi.auth.oidc.OIDCUserInfoRequestError; -import io.jans.kc.spi.auth.oidc.OIDCUserInfoResponse; - -public class JansAuthenticator implements Authenticator { - - private static final Logger log = Logger.getLogger(JansAuthenticator.class); - - private static final String JANS_AUTH_REDIRECT_FORM_FTL = "jans-auth-redirect.ftl"; - private static final String JANS_AUTH_ERROR_FTL = "jans-auth-error.ftl"; - - private static final String OPENID_CODE_RESPONSE = "code"; - private static final String OPENID_SCOPE = "openid"; - private static final String USERNAME_SCOPE ="user_name"; - private static final String EMAIL_SCOPE = "email"; - private static final String JANS_LOGIN_URL_ATTRIBUTE = "jansLoginUrl"; - private static final String OPENID_AUTH_PARAMS_ATTRIBUTE = "openIdAuthParams"; - - private static final String URI_PATH_TO_REST_SERVICE = "realms/{realm}/{providerid}/auth-complete"; - - - private OIDCService oidcService; - - public JansAuthenticator(OIDCService oidcService) { - - this.oidcService = oidcService; - } - - @Override - public void authenticate(AuthenticationFlowContext context) { - - Configuration config = extractAndValidateConfiguration(context); - if(config == null) { - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onConfigurationError(context)); - return; - } - - try { - URI redirecturi = createRedirectUri(context); - URI actionuri = createActionUrl(context); - - String state = generateOIDCState(); - String nonce = generateOIDCNonce(); - - OIDCAuthRequest oidcauthrequest = createAuthnRequest(config, state, nonce,redirecturi.toString()); - - URI loginurl = oidcService.createAuthorizationUrl(config.normalizedIssuerUrl(), oidcauthrequest); - URI loginurlnoparams = UriBuilder.fromUri(loginurl.toString()).replaceQuery(null).build(); - - Response response = context - .form() - .setActionUri(actionuri) - .setAttribute(JANS_LOGIN_URL_ATTRIBUTE,loginurlnoparams.toString()) - .setAttribute(OPENID_AUTH_PARAMS_ATTRIBUTE,parseQueryParameters(loginurl.getQuery())) - .createForm(JANS_AUTH_REDIRECT_FORM_FTL); - - saveRealmStringData(context, SessionAttributes.JANS_OIDC_NONCE, nonce); - saveRealmStringData(context, SessionAttributes.KC_ACTION_URI,actionuri.toString()); - saveRealmStringData(context,SessionAttributes.JANS_OIDC_STATE,state); - - context.challenge(response); - }catch(OIDCMetaError e) { - log.errorv(e,"OIDC Error obtaining the authorization url"); - Response response = context.form().createForm(JANS_AUTH_ERROR_FTL); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); - } - } - - @Override - public void action(AuthenticationFlowContext context) { - - Configuration config = extractAndValidateConfiguration(context); - if(config == null) { - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onMissingAuthenticationCode(context)); - return; - } - - String openid_code = getOpenIdCode(context); - if(openid_code == null) { - log.errorv("Missing authentication code during response processing"); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onMissingAuthenticationCode(context)); - return; - } - - OIDCTokenRequest tokenrequest = createTokenRequest(config, openid_code, createRedirectUri(context)); - try { - OIDCTokenResponse tokenresponse = oidcService.requestTokens(config.normalizedIssuerUrl(), tokenrequest); - if(!tokenresponse.indicatesSuccess()) { - OIDCTokenError error = tokenresponse.error(); - log.errorv("Error processing token {0}. ({1}) {2}",error.code(),error.description()); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onTokenRetrievalError(context)); - return; - } - - OIDCUserInfoResponse userinforesponse = oidcService.requestUserInfo(config.normalizedIssuerUrl(),tokenresponse.accessToken()); - if(!userinforesponse.indicatesSuccess()) { - OIDCUserInfoError error = userinforesponse.error(); - log.errorv("Error getting userinfo for authenticated user. ({0}) {1}",error.code(),error.description()); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onUserInfoRetrievalError(context)); - return; - } - - UserModel user = findUserByNameOrEmail(context,userinforesponse.username(),userinforesponse.email()); - if(user == null) { - log.errorv("User with username/email {0} / {1} not found",userinforesponse.username(),userinforesponse.email()); - context.failure(AuthenticationFlowError.UNKNOWN_USER); - return; - } - log.debugv("User {0} authenticated",user.getUsername()); - context.setUser(user); - context.success(); - }catch(OIDCTokenRequestError e) { - log.debugv(e,"Unable to retrieve token information"); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onTokenRetrievalError(context)); - }catch(OIDCUserInfoRequestError e) { - log.debugv(e,"Unable to retrieve user information"); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,onUserInfoRetrievalError(context)); - } - } - - @Override - public boolean requiresUser() { - - return false; - } - - @Override - public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - - return false; - } - - @Override - public void setRequiredActions(KeycloakSession session, RealmModel model, UserModel user) { - - return; - } - - @Override - public List getRequiredActions(KeycloakSession session) { - - return null; - } - - @Override - public void close() { - - return; - } - - private Configuration extractAndValidateConfiguration(AuthenticationFlowContext context) { - - Configuration config = pluginConfigurationFromContext(context); - - if(config == null) { - log.debugv("Plugin probably not configured. Check the Janssen Auth plugin in the authentication flow"); - return null; - } - - ValidationResult validationresult = config.validate(); - if(validationresult.hasErrors()) { - for(String err : validationresult.getErrors()) { - log.errorv("Invalid plugin configuration {0}",err); - } - return null; - } - return config; - } - - private URI createRedirectUri(AuthenticationFlowContext context) { - - try { - String realmname = context.getRealm().getName(); - return UriBuilder.fromUri(context.getSession().getContext().getUri().getBaseUri()) - .path(URI_PATH_TO_REST_SERVICE) - .build(realmname,ProviderIDs.JANS_AUTH_RESPONSE_REST_PROVIDER); - }catch(IllegalArgumentException e) { - log.warnv(e,"Could not create redirect URIs"); - return null; - } - } - - private UserModel findUserByNameOrEmail(AuthenticationFlowContext context, String username,String email) { - - UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(),context.getRealm(),username); - if(user == null) { - user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(),context.getRealm(),email); - } - return user; - } - - - private Map parseQueryParameters(String params) { - - Map ret = new HashMap(); - if(params == null) { - return ret; - } - - String [] parampairs = params.split("&"); - for(String pair : parampairs) { - String [] kv = pair.split("="); - if(kv.length == 1) { - ret.put(kv[0].trim(),""); - }else { - try { - ret.put(kv[0].trim(), - URLDecoder.decode(kv[1].trim(),"UTF-8")); - }catch(UnsupportedEncodingException ue) { - log.debugv(ue,"Failed to decode query parameter data {0}",pair); - } - } - } - - return ret; - } - - private Configuration pluginConfigurationFromContext(AuthenticationFlowContext context) { - - AuthenticatorConfigModel config = context.getAuthenticatorConfig(); - if(config == null || config.getConfig() == null) { - return null; - } - - String server_url = config.getConfig().get(JansAuthenticatorConfigProp.SERVER_URL.getName()); - String client_id = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_ID.getName()); - String client_secret = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_SECRET.getName()); - String issuer = config.getConfig().get(JansAuthenticatorConfigProp.ISSUER.getName()); - String extra_scopes = config.getConfig().get(JansAuthenticatorConfigProp.EXTRA_SCOPES.getName()); - List parsed_extra_scopes = new ArrayList<>(); - if(extra_scopes != null) { - String [] tokens = extra_scopes.split("\\s*,\\s*"); - for(String token : tokens) { - parsed_extra_scopes.add(token); - } - } - - return new Configuration(server_url,client_id,client_secret,issuer,parsed_extra_scopes); - } - - private final String generateOIDCState() { - - return generateRandomString(10); - } - - private final String generateOIDCNonce() { - - return generateRandomString(10); - } - - private final URI createActionUrl(AuthenticationFlowContext context) { - - String accesscode = context.generateAccessCode(); - return context.getActionUrl(accesscode); - } - - - private final void saveRealmStringData(AuthenticationFlowContext context, String key, String value) { - - context.getRealm().setAttribute(key, value); - } - - private String generateRandomString(int length) { - int leftlimit = 48; - int rightlimit = 122; - - return new Random().ints(leftlimit,rightlimit+1) - .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) - .limit(length) - .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) - .toString(); - } - - private OIDCAuthRequest createAuthnRequest(Configuration config, String state, String nonce,String redirecturi) { - // - OIDCAuthRequest request = new OIDCAuthRequest(); - request.setClientId(config.clientId); - request.addScope(OPENID_SCOPE); - request.addScope(USERNAME_SCOPE); - request.addScope(EMAIL_SCOPE); - for(String extrascope : config.scopes) { - request.addScope(extrascope); - } - request.addResponseType(OPENID_CODE_RESPONSE); - request.setNonce(nonce); - request.setState(state); - request.setRedirectUri(redirecturi); - return request; - } - - private OIDCTokenRequest createTokenRequest(Configuration config,String code,URI redirecturi) { - - return new OIDCTokenRequest(code,config.clientId,config.clientSecret,redirecturi); - } - - private final Response onConfigurationError(AuthenticationFlowContext context) { - - return context.form().createForm(JANS_AUTH_ERROR_FTL); - } - - private final Response onMissingAuthenticationCode(AuthenticationFlowContext context) { - - return context.form().createForm(JANS_AUTH_ERROR_FTL); - } - - private final Response onTokenRetrievalError(AuthenticationFlowContext context) { - - return context.form().createForm(JANS_AUTH_ERROR_FTL); - } - - private final Response onUserInfoRetrievalError (AuthenticationFlowContext context) { - - return context.form().createForm(JANS_AUTH_ERROR_FTL); - } - - private final String getOpenIdCode(AuthenticationFlowContext context) { - - return context.getRealm().getAttribute(SessionAttributes.JANS_OIDC_CODE); - } - - - public static class ValidationResult { - - private List errors; - - public void addError(String error) { - - if(errors == null) { - this.errors = new ArrayList(); - } - this.errors.add(error); - } - - public boolean hasErrors() { - - return this.errors != null; - } - - public List getErrors() { - - return this.errors; - } - } - - private class Configuration { - - private String serverUrl; - private String clientId; - private String clientSecret; - private String issuerUrl; - private List scopes; - - public Configuration(String serverUrl,String clientId, String clientSecret, String issuerUrl, List scopes) { - - this.serverUrl = serverUrl; - this.clientId = clientId; - this.clientSecret = clientSecret; - this.issuerUrl = issuerUrl; - this.scopes = scopes; - } - - - public ValidationResult validate() { - - ValidationResult result = new ValidationResult(); - - if(serverUrl == null || serverUrl.isEmpty()) { - result.addError("Missing or empty Server Url"); - } - - if(clientId == null || clientId.isEmpty()) { - result.addError("Missing or empty Client ID"); - } - - if(clientSecret == null || clientSecret.isEmpty()) { - result.addError("Missing or empty client secret"); - } - return result; - } - - public String normalizedIssuerUrl() { - - String effective_url = issuerUrl; - if(effective_url == null) { - effective_url = serverUrl; - } - if(effective_url == null) { - return null; - } - - if(effective_url.charAt(effective_url.length() -1) == '/') { - return effective_url.substring(0, effective_url.length() -1); - } - return effective_url; - } - - } -} \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java deleted file mode 100644 index 22f003a6336..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorConfigProp.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.jans.kc.spi.auth; - -import java.util.List; - -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; - -public enum JansAuthenticatorConfigProp { - - SERVER_URL( - "jans.auth.server.url", - "Janssen Server Url", - "Url of the Janssen Server", - ProviderConfigProperty.STRING_TYPE, - null, - false), - CLIENT_ID( - "jans.auth.client.id", - "Janssen Client ID", - "Client ID of the OpenID Client created in Janssen-Auth", - ProviderConfigProperty.STRING_TYPE, - null, - false), - CLIENT_SECRET( - "jans.auth.client.secret", - "Janssen Client Secret", - "Secret/Password of the OpenID Client created in Janssen-Auth", - ProviderConfigProperty.PASSWORD, - null, - true - ), - ISSUER( - "jans.auth.issuer", - "Janssen OpenID Issuer(Optional)", - "OpenID issuer of the Janssen server (Optional)", - ProviderConfigProperty.STRING_TYPE, - null, - false ), - EXTRA_SCOPES( - "jans.auth.extra_scopes", - "Extra OpenID Scopes", - "Comma delimited list of extra OpenID scopes", - ProviderConfigProperty.STRING_TYPE, - null, - false - ); - - private String name; - private ProviderConfigProperty config; - private JansAuthenticatorConfigProp(String name, String label, String helptext, String type, Object defaultvalue, boolean secret) { - - this.name = name; - this.config = new ProviderConfigProperty(name, label, helptext, type, defaultvalue, secret); - } - - public String getName() { - - return this.name; - } - - public ProviderConfigProperty getConfig() { - - return this.config; - } - - public static final List asList() { - - ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create(); - - for(JansAuthenticatorConfigProp prop : values()) { - builder.property(prop.config); - } - return builder.build(); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java deleted file mode 100644 index 6a49c9cba92..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java +++ /dev/null @@ -1,117 +0,0 @@ -package io.jans.kc.spi.auth; - -import java.util.List; - -import org.jboss.logging.Logger; - -import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.AuthenticatorFactory; - -import org.keycloak.Config; - -import org.keycloak.models.AuthenticationExecutionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; - -import org.keycloak.provider.ProviderConfigProperty; - -import io.jans.kc.spi.ProviderIDs; -import io.jans.kc.spi.auth.oidc.OIDCMetaCache; -import io.jans.kc.spi.auth.oidc.OIDCService; -import io.jans.kc.spi.auth.oidc.impl.HashBasedOIDCMetaCache; -import io.jans.kc.spi.auth.oidc.impl.NimbusOIDCService; - - -public class JansAuthenticatorFactory implements AuthenticatorFactory { - - private static final String PROVIDER_ID = ProviderIDs.JANS_AUTHENTICATOR_PROVIDER; - - private static final String DISPLAY_TYPE = "Janssen Authenticator"; - private static final String REFERENCE_CATEGORY = "Janssen Authenticator"; - private static final String HELP_TEXT= "Janssen authenticator for Keycloak"; - - private static final Logger log = Logger.getLogger(JansAuthenticatorFactory.class); - - private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { - AuthenticationExecutionModel.Requirement.REQUIRED, - AuthenticationExecutionModel.Requirement.ALTERNATIVE, - AuthenticationExecutionModel.Requirement.DISABLED - }; - - - private static final OIDCMetaCache META_CACHE = new HashBasedOIDCMetaCache(); - private static final OIDCService OIDC_SERVICE = new NimbusOIDCService(META_CACHE); - private static final JansAuthenticator INSTANCE = new JansAuthenticator(OIDC_SERVICE); - - @Override - public String getId() { - - return PROVIDER_ID; - } - - @Override - public Authenticator create(KeycloakSession session) { - - log.debug("Janssen authenticator create()"); - return INSTANCE; - } - - @Override - public void init(Config.Scope config) { - - return; - } - - @Override - public void close() { - - return; - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - return; - } - - @Override - public String getDisplayType() { - - return DISPLAY_TYPE; - } - - @Override - public String getReferenceCategory() { - - return REFERENCE_CATEGORY; - } - - @Override - public boolean isConfigurable() { - - return true; - } - - public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { - - return REQUIREMENT_CHOICES; - } - - @Override - public boolean isUserSetupAllowed() { - - return false; - } - - @Override - public String getHelpText() { - - return HELP_TEXT; - } - - @Override - public List getConfigProperties() { - - return JansAuthenticatorConfigProp.asList(); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java deleted file mode 100644 index e7b0331939f..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.jans.kc.spi.auth; - -public class SessionAttributes { - - public static final String JANS_OIDC_STATE = "jans.oidc.state"; - public static final String JANS_OIDC_NONCE = "jans.oidc.nonce"; - public static final String KC_ACTION_URI = "kc.action-uri"; - public static final String JANS_OIDC_CODE = "jans.oidc.code"; - public static final String JANS_SESSION_STATE = "jans.session.state"; -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAccessToken.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAccessToken.java deleted file mode 100644 index d3585611adc..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAccessToken.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public interface OIDCAccessToken { - -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAuthRequest.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAuthRequest.java deleted file mode 100644 index d362488ac4a..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCAuthRequest.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -import java.util.List; -import java.util.ArrayList; - -public class OIDCAuthRequest { - - private String clientId; - private String state; - private String nonce; - private List scopes; - private List responseTypes; - private String redirectUri; - - public OIDCAuthRequest() { - - this.clientId = null; - this.state = null; - this.nonce = null; - this.scopes = new ArrayList(); - this.responseTypes = new ArrayList(); - this.redirectUri = null; - } - - public String getClientId() { - - return this.clientId; - } - - public void setClientId(String clientId) { - - this.clientId = clientId; - } - - public void setState(String state) { - - this.state = state; - } - - public final String getState() { - - return this.state; - } - - public void setNonce(String nonce) { - - this.nonce = nonce; - } - - public final String getNonce() { - - return this.nonce; - } - - public void addScope(String scope) { - - this.scopes.add(scope); - } - - public final List getScopes() { - - return this.scopes; - } - - public void addResponseType(String responseType) { - - this.responseTypes.add(responseType); - } - - public final List getResponseTypes() { - - return this.responseTypes; - } - - - public void setRedirectUri(String redirectUri) { - - this.redirectUri = redirectUri; - } - - public String getRedirectUri() { - - return this.redirectUri; - } - -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCache.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCache.java deleted file mode 100644 index 5db2d94c2be..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCache.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public interface OIDCMetaCache { - public void put(String issuer, String key , Object value); - public Object get(String issuer, String key); -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCacheKeys.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCacheKeys.java deleted file mode 100644 index 76671559992..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaCacheKeys.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public class OIDCMetaCacheKeys { - public static final String AUTHORIZATION_URL = "oidc.authorization.url"; - public static final String TOKEN_URL = "oidc.token.url"; - public static final String USERINFO_URL = "oidc.userinfo.url"; -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaError.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaError.java deleted file mode 100644 index 1f6cc47323a..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCMetaError.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public class OIDCMetaError extends Exception { - - public OIDCMetaError(String message) { - super(message); - } - - public OIDCMetaError(String message, Throwable cause) { - super(message,cause); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCRefreshToken.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCRefreshToken.java deleted file mode 100644 index 7aa7a2d5d23..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCRefreshToken.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public interface OIDCRefreshToken { - -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCService.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCService.java deleted file mode 100644 index b30effb139c..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCService.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -import java.net.URI; - -public interface OIDCService { - - public URI getAuthorizationEndpoint(String issuerUrl) throws OIDCMetaError; - public URI getTokenEndpoint(String issuerUrl) throws OIDCMetaError; - public URI getUserInfoEndpoint(String issuerUrl) throws OIDCMetaError; - public URI createAuthorizationUrl(String issuerUrl, OIDCAuthRequest request) throws OIDCMetaError; - public OIDCTokenResponse requestTokens(String issuerUrl, OIDCTokenRequest tokenreq) throws OIDCTokenRequestError; - public OIDCUserInfoResponse requestUserInfo(String issuerUrl, OIDCAccessToken accesstoken) throws OIDCUserInfoRequestError; -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenError.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenError.java deleted file mode 100644 index d203619aab2..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenError.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public class OIDCTokenError { - - private String code; - private String description; - private int httpStatusCode; - - public OIDCTokenError(String code, String description, int httpStatusCode) { - - this.code = code; - this.description = description; - this.httpStatusCode = httpStatusCode; - } - - public String code() { - - return this.code; - } - - public String description() { - - return this.description; - } - - public int httpStatusCode() { - - return this.httpStatusCode; - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequest.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequest.java deleted file mode 100644 index 99836cfe377..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -import java.net.URI; - -public class OIDCTokenRequest { - - private String code; - //in the future , replace this with a client credentials - //interface to support various authntication credential schemes - private String clientId; - private String clientSecret; - private URI redirecturi; - - public OIDCTokenRequest(String code, String clientId,String clientSecret,URI redirecturi) { - this.code = code; - this.clientId = clientId; - this.clientSecret = clientSecret; - this.redirecturi = redirecturi; - } - - public String getCode() { - - return this.code; - } - - public String getClientId() { - - return this.clientId; - } - - public String getClientSecret() { - - return this.clientSecret; - } - - public URI getRedirectUri() { - - return this.redirecturi; - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequestError.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequestError.java deleted file mode 100644 index 05a96e56896..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenRequestError.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public class OIDCTokenRequestError extends Exception { - - public OIDCTokenRequestError(String msg) { - super(msg); - } - - public OIDCTokenRequestError(String msg, Throwable cause) { - super(msg,cause); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenResponse.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenResponse.java deleted file mode 100644 index 98991558aa5..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCTokenResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public interface OIDCTokenResponse { - - public OIDCAccessToken accessToken(); - public OIDCRefreshToken refreshToken(); - public OIDCTokenError error(); - public boolean indicatesSuccess(); -} \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoError.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoError.java deleted file mode 100644 index f5c25d79223..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoError.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public class OIDCUserInfoError { - - private String code; - private String description; - private int httpStatusCode; - - public OIDCUserInfoError(String code, String description, int httpStatusCode) { - - this.code = code; - this.description = description; - this.httpStatusCode = httpStatusCode; - } - - public String code() { - - return this.code; - } - - public String description() { - - return this.description; - } - - public int httpStatusCode() { - - return this.httpStatusCode; - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoRequestError.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoRequestError.java deleted file mode 100644 index 13d4e9110ef..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoRequestError.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public class OIDCUserInfoRequestError extends Exception { - - public OIDCUserInfoRequestError(String msg) { - super(msg); - } - - - public OIDCUserInfoRequestError(String msg, Throwable cause) { - super(msg,cause); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoResponse.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoResponse.java deleted file mode 100644 index 7787cd06727..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/OIDCUserInfoResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jans.kc.spi.auth.oidc; - -public interface OIDCUserInfoResponse { - - public String username(); - public String email(); - public boolean indicatesSuccess(); - public OIDCUserInfoError error(); -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/HashBasedOIDCMetaCache.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/HashBasedOIDCMetaCache.java deleted file mode 100644 index a04b212d53e..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/HashBasedOIDCMetaCache.java +++ /dev/null @@ -1,139 +0,0 @@ -package io.jans.kc.spi.auth.oidc.impl; - -import java.util.Map; - -import org.jboss.logging.Logger; - -import io.jans.kc.spi.auth.oidc.OIDCMetaCache; - -import java.util.HashMap; - -public class HashBasedOIDCMetaCache implements OIDCMetaCache{ - - private static final Logger log = Logger.getLogger(HashBasedOIDCMetaCache.class); - private static final long DEFAULT_CACHE_TTL = 20*60; // 20 seconds - - private long cacheEntryTtl; - - private Map> cacheEntries; - - public HashBasedOIDCMetaCache() { - this(DEFAULT_CACHE_TTL); - } - - public HashBasedOIDCMetaCache(long cacheEntryTtl) { - - this.cacheEntryTtl = cacheEntryTtl; - if(this.cacheEntryTtl == 0) { - this.cacheEntryTtl = DEFAULT_CACHE_TTL; - } - this.cacheEntryTtl = this.cacheEntryTtl * 1000; // convert to milliseconds - this.cacheEntries = new HashMap>(); - } - - @Override - public void put(String issuer, String key, Object value) { - - synchronized(cacheEntries) { - createIfNotExistIssuerCacheEntry(issuer); - addIssuerCacheEntry(issuer,key,value); - performHouseCleaning(); - } - } - - @Override - public Object get(String issuer, String key) { - - synchronized(cacheEntries) { - if(issuerCacheEntryIsMissing(issuer)) { - performHouseCleaning(); - return null; - } - - Object ret = getIssuerCacheEntryValue(issuer, key); - performHouseCleaning(); - return ret; - } - } - - private boolean issuerCacheEntryIsMissing(String issuer) { - - return cacheEntries.get(issuer) == null; - } - - private Object getIssuerCacheEntryValue(String issuer, String key) { - - Map issuer_cache = cacheEntries.get(issuer); - return issuer_cache.get(key).getValue(); - } - - private void createIfNotExistIssuerCacheEntry(String issuer) { - - if(!cacheEntries.containsKey(issuer)) { - cacheEntries.put(issuer,new HashMap()); - } - } - - private void addIssuerCacheEntry(String issuer,String key, Object value) { - - Map issuerCache = cacheEntries.get(issuer); - for(String existingkey : issuerCache.keySet()) { - if(existingkey.equalsIgnoreCase(key)) { - //update cache entry - CacheEntry cache_entry = issuerCache.get(existingkey); - cache_entry.updateValue(value); - return; - } - } - - issuerCache.put(key,new CacheEntry(cacheEntryTtl, value)); - } - - private void performHouseCleaning() { - - for(String issuer: cacheEntries.keySet()) { - Map issuer_cache = cacheEntries.get(issuer); - for(String key :issuer_cache.keySet()) { - CacheEntry cache_entry = issuer_cache.get(key); - if(cache_entry.isExpired()) { - issuer_cache.remove(key); - } - } - } - } - - private class CacheEntry { - - private long updateTime; - private long ttl; - private Object value; - - public CacheEntry(long ttl, Object value) { - - this.updateTime = System.currentTimeMillis(); - this.ttl = ttl; - this.value = value; - } - - public Object getValue() { - - return this.value; - } - - public boolean isExpired() { - - return (System.currentTimeMillis() - this.updateTime) > (this.ttl * 1000) ; - } - - public void updateValue(Object value) { - - this.value = value; - this.updateTime = System.currentTimeMillis(); - } - - public void refresh() { - - this.updateTime = System.currentTimeMillis(); - } - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCAccessToken.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCAccessToken.java deleted file mode 100644 index da2c7bc9a88..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCAccessToken.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.jans.kc.spi.auth.oidc.impl; - -import com.nimbusds.oauth2.sdk.token.AccessToken; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; - -import io.jans.kc.spi.auth.oidc.OIDCAccessToken; - -public class NimbusOIDCAccessToken implements OIDCAccessToken { - - private AccessToken accessToken; - - public NimbusOIDCAccessToken(AccessToken accessToken) { - this.accessToken = accessToken; - } - - public BearerAccessToken asBearerToken() { - - return new BearerAccessToken(accessToken.getValue()); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCRefreshToken.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCRefreshToken.java deleted file mode 100644 index 7a500071d9e..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCRefreshToken.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.jans.kc.spi.auth.oidc.impl; - -import com.nimbusds.oauth2.sdk.token.RefreshToken; - -import io.jans.kc.spi.auth.oidc.OIDCRefreshToken; - -public class NimbusOIDCRefreshToken implements OIDCRefreshToken{ - - private RefreshToken refreshToken; - - public NimbusOIDCRefreshToken(RefreshToken refreshToken) { - this.refreshToken = refreshToken; - } - -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCService.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCService.java deleted file mode 100644 index 10d9e64b1ce..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCService.java +++ /dev/null @@ -1,222 +0,0 @@ -package io.jans.kc.spi.auth.oidc.impl; - -import java.io.IOException; - -import com.nimbusds.oauth2.sdk.AuthorizationCode; -import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; -import com.nimbusds.oauth2.sdk.GeneralException; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.ResponseType; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.oauth2.sdk.TokenRequest; -import com.nimbusds.oauth2.sdk.TokenResponse; -import com.nimbusds.oauth2.sdk.ResponseType.Value; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; -import com.nimbusds.oauth2.sdk.auth.Secret; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; -import com.nimbusds.oauth2.sdk.id.ClientID; -import com.nimbusds.oauth2.sdk.id.Issuer; -import com.nimbusds.oauth2.sdk.id.State; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.AuthenticationRequest; -import com.nimbusds.openid.connect.sdk.Nonce; -import com.nimbusds.openid.connect.sdk.UserInfoRequest; -import com.nimbusds.openid.connect.sdk.UserInfoResponse; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; - -import io.jans.kc.spi.auth.oidc.OIDCAccessToken; -import io.jans.kc.spi.auth.oidc.OIDCAuthRequest; -import io.jans.kc.spi.auth.oidc.OIDCMetaCache; -import io.jans.kc.spi.auth.oidc.OIDCMetaCacheKeys; -import io.jans.kc.spi.auth.oidc.OIDCMetaError; -import io.jans.kc.spi.auth.oidc.OIDCService; -import io.jans.kc.spi.auth.oidc.OIDCTokenRequest; -import io.jans.kc.spi.auth.oidc.OIDCTokenRequestError; -import io.jans.kc.spi.auth.oidc.OIDCTokenResponse; -import io.jans.kc.spi.auth.oidc.OIDCUserInfoRequestError; -import io.jans.kc.spi.auth.oidc.OIDCUserInfoResponse; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.List; - -import org.jboss.logging.Logger; - -public class NimbusOIDCService implements OIDCService { - - private static final Logger log = Logger.getLogger(NimbusOIDCService.class); - - private OIDCMetaCache metaCache; - - public NimbusOIDCService(OIDCMetaCache metaCache) { - - this.metaCache = metaCache; - } - - @Override - public URI getAuthorizationEndpoint(String issuerUrl) throws OIDCMetaError { - - URI ret = getAuthorizationEndpointFromCache(issuerUrl); - if(ret == null) { - return getAuthorizationEndpointFromServer(issuerUrl); - } - return ret; - } - - @Override - public URI getTokenEndpoint(String issuerUrl) throws OIDCMetaError { - - URI ret = getTokenEndpointFromCache(issuerUrl); - if(ret == null) { - return getTokenEndpointFromServer(issuerUrl); - } - return ret; - } - - @Override - public URI getUserInfoEndpoint(String issuerUrl) throws OIDCMetaError { - - URI ret = getUserInfoEndpointFromCache(issuerUrl); - if(ret == null) { - return getUserInfoEndpointFromServer(issuerUrl); - } - return ret; - } - - @Override - public URI createAuthorizationUrl(String issuerUrl, OIDCAuthRequest request) throws OIDCMetaError { - - try { - - return new AuthenticationRequest.Builder( - extractResponseType(request.getResponseTypes()), - extractScope(request.getScopes()), - new ClientID(request.getClientId()), - new URI(request.getRedirectUri()) - ) - .endpointURI(getAuthorizationEndpoint(issuerUrl)) - .state(new State(request.getState())) - .nonce(new Nonce(request.getNonce())) - .build().toURI(); - }catch(URISyntaxException e) { - throw new OIDCMetaError("Error building the authentication url",e); - } - } - - @Override - public OIDCTokenResponse requestTokens(String issuerUrl,OIDCTokenRequest tokenrequest) throws OIDCTokenRequestError { - - try { - AuthorizationCode code = new AuthorizationCode(tokenrequest.getCode()); - AuthorizationCodeGrant grant = new AuthorizationCodeGrant(code,tokenrequest.getRedirectUri()); - ClientID clientId = new ClientID(tokenrequest.getClientId()); - Secret secret = new Secret(tokenrequest.getClientSecret()); - ClientAuthentication auth = new ClientSecretBasic(clientId,secret); - TokenRequest request = new TokenRequest(getTokenEndpoint(issuerUrl),auth,grant); - TokenResponse tokenresponse = TokenResponse.parse(request.toHTTPRequest().send()); - return new NimbusOIDCTokenResponse(tokenresponse); - }catch(ParseException e) { - throw new OIDCTokenRequestError("Error parsing token response",e); - }catch(IOException e) { - throw new OIDCTokenRequestError("I/O error while retrieving token data",e); - }catch(OIDCMetaError e) { - throw new OIDCTokenRequestError("Error retrieving token endpoint from server", e); - } - } - - @Override - public OIDCUserInfoResponse requestUserInfo(String issuerUrl, OIDCAccessToken accesstoken) throws OIDCUserInfoRequestError { - - if(!(accesstoken instanceof NimbusOIDCAccessToken)){ - throw new OIDCUserInfoRequestError("The specified access token is not supported by the Nimbus Backend"); - } - - BearerAccessToken bearertoken = ((NimbusOIDCAccessToken) accesstoken).asBearerToken(); - try { - HTTPResponse http_response = new UserInfoRequest(getUserInfoEndpoint(issuerUrl),bearertoken).toHTTPRequest().send(); - UserInfoResponse userinforesponse = UserInfoResponse.parse(http_response); - return new NimbusOIDCUserInfoResponse(userinforesponse); - } catch (IOException e) { - throw new OIDCUserInfoRequestError("I/O error trying to obtain user info",e); - }catch(OIDCMetaError e) { - throw new OIDCUserInfoRequestError("Metadata fetch error trying to obtain user info",e); - }catch(ParseException e) { - throw new OIDCUserInfoRequestError("Parse error trying to obtain user info",e); - } - } - - private ResponseType extractResponseType(List rtypes) { - - ResponseType rtype = new ResponseType(); - for(String val : rtypes) { - rtype.add(new Value(val)); - } - return rtype; - } - - private Scope extractScope(List scopes) { - - Scope scope = new Scope(); - for(String val : scopes) { - scope.add(val); - } - return scope; - } - - private URI getAuthorizationEndpointFromCache(String issuerUrl) { - - return (URI) metaCache.get(issuerUrl, OIDCMetaCacheKeys.AUTHORIZATION_URL); - } - - private URI getAuthorizationEndpointFromServer(String issuerUrl) throws OIDCMetaError { - - OIDCProviderMetadata meta = obtainMetadataFromServer(issuerUrl); - cacheMetadataFromServer(issuerUrl,meta); - return getAuthorizationEndpointFromCache(issuerUrl); - } - - private URI getTokenEndpointFromCache(String issuerUrl) { - - return (URI) metaCache.get(issuerUrl,OIDCMetaCacheKeys.TOKEN_URL); - } - - private URI getTokenEndpointFromServer(String issuerUrl) throws OIDCMetaError { - - OIDCProviderMetadata meta = obtainMetadataFromServer(issuerUrl); - cacheMetadataFromServer(issuerUrl, meta); - return getTokenEndpointFromCache(issuerUrl); - } - - private URI getUserInfoEndpointFromServer(String issuerUrl) throws OIDCMetaError { - - OIDCProviderMetadata meta = obtainMetadataFromServer(issuerUrl); - cacheMetadataFromServer(issuerUrl, meta); - return getUserInfoEndpointFromCache(issuerUrl); - } - - private URI getUserInfoEndpointFromCache(String issuerUrl) throws OIDCMetaError { - - return (URI) metaCache.get(issuerUrl,OIDCMetaCacheKeys.USERINFO_URL); - } - - private OIDCProviderMetadata obtainMetadataFromServer(String issuerUrl) throws OIDCMetaError { - - try { - Issuer issuer = new Issuer(issuerUrl); - return OIDCProviderMetadata.resolve(issuer); - }catch(GeneralException e) { - throw new OIDCMetaError("Could not obtain metadata from server",e); - }catch(IOException e) { - throw new OIDCMetaError("Could not obtain metadata from server",e); - } - } - - private void cacheMetadataFromServer(String issuerUrl,OIDCProviderMetadata metadata) { - - metaCache.put(issuerUrl,OIDCMetaCacheKeys.AUTHORIZATION_URL,metadata.getAuthorizationEndpointURI()); - metaCache.put(issuerUrl,OIDCMetaCacheKeys.TOKEN_URL,metadata.getTokenEndpointURI()); - metaCache.put(issuerUrl,OIDCMetaCacheKeys.USERINFO_URL,metadata.getUserInfoEndpointURI()); - } - -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCTokenResponse.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCTokenResponse.java deleted file mode 100644 index 569a6683800..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCTokenResponse.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.jans.kc.spi.auth.oidc.impl; - -import com.nimbusds.oauth2.sdk.AccessTokenResponse; -import com.nimbusds.oauth2.sdk.TokenResponse; -import com.nimbusds.oauth2.sdk.token.Tokens; - -import io.jans.kc.spi.auth.oidc.OIDCAccessToken; -import io.jans.kc.spi.auth.oidc.OIDCRefreshToken; -import io.jans.kc.spi.auth.oidc.OIDCTokenError; -import io.jans.kc.spi.auth.oidc.OIDCTokenResponse; - - -public class NimbusOIDCTokenResponse implements OIDCTokenResponse { - - private TokenResponse tokenResponse; - private NimbusOIDCAccessToken accessToken; - private NimbusOIDCRefreshToken refreshToken; - private OIDCTokenError tokenError; - - public NimbusOIDCTokenResponse(TokenResponse tokenResponse) { - - this.tokenResponse = tokenResponse; - if(this.tokenResponse.indicatesSuccess()) { - AccessTokenResponse atresponse = this.tokenResponse.toSuccessResponse(); - Tokens tokens = atresponse.getTokens(); - this.accessToken = new NimbusOIDCAccessToken(tokens.getAccessToken()); - this.refreshToken = new NimbusOIDCRefreshToken(tokens.getRefreshToken()); - }else { - - } - } - - @Override - public OIDCAccessToken accessToken() { - - return accessToken; - } - - @Override - public OIDCRefreshToken refreshToken() { - - return refreshToken; - } - - @Override - public OIDCTokenError error() { - - return tokenError; - } - - @Override - public boolean indicatesSuccess() { - - return tokenResponse.indicatesSuccess(); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCUserInfoResponse.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCUserInfoResponse.java deleted file mode 100644 index db78425dd9d..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/auth/oidc/impl/NimbusOIDCUserInfoResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.jans.kc.spi.auth.oidc.impl; - -import com.nimbusds.openid.connect.sdk.UserInfoResponse; - -import io.jans.kc.spi.auth.oidc.OIDCUserInfoError; -import io.jans.kc.spi.auth.oidc.OIDCUserInfoResponse; - -public class NimbusOIDCUserInfoResponse implements OIDCUserInfoResponse { - - private static final String USERNAME_CLAIM_NAME = "user_name"; - - private UserInfoResponse userInfoResponse; - - public NimbusOIDCUserInfoResponse(UserInfoResponse userInfoResponse) { - this.userInfoResponse = userInfoResponse; - } - - public String username() { - - return (String) userInfoResponse.toSuccessResponse().getUserInfo().getClaim(USERNAME_CLAIM_NAME); - } - - public String email() { - - return userInfoResponse.toSuccessResponse().getUserInfo().getEmailAddress(); - } - - @Override - public boolean indicatesSuccess() { - - return userInfoResponse.indicatesSuccess(); - } - - @Override - public OIDCUserInfoError error() { - - if(userInfoResponse.indicatesSuccess()) { - return null; - } - - return new OIDCUserInfoError( - userInfoResponse.toErrorResponse().getErrorObject().getCode(), - userInfoResponse.toErrorResponse().getErrorObject().getDescription(), - userInfoResponse.toErrorResponse().getErrorObject().getHTTPStatusCode() - ); - } -} \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java deleted file mode 100644 index 478a4d59fa6..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.jans.kc.spi.rest; - -import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.services.resource.RealmResourceProvider; - -import io.jans.kc.spi.auth.SessionAttributes; - -import java.util.Map; -import java.util.HashMap; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.QueryParam; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; - -import org.jboss.logging.Logger; -import org.jboss.resteasy.annotations.cache.NoCache; - -public class JansAuthResponseResourceProvider implements RealmResourceProvider { - - private static final Logger log = Logger.getLogger(JansAuthResponseResourceProvider.class); - - private static final String ACTION_URI_TPL_PARAM = "actionuri"; - private static final String ERR_MSG_TPL_PARAM = "authError"; - - private static final String JANS_AUTH_RESPONSE_ERR_FTL ="jans-auth-response-error.ftl"; - private static final String JANS_AUTH_RESPONSE_COMPLETE_FTL = "jans-auth-response-complete.ftl"; - - private static final String ERR_MSG_INVALID_REALM = "jans.error-invalid-realm"; - private static final String ERR_MSG_MISSING_DATA = "jans.error-missing-data"; - - private KeycloakSession session; - - public JansAuthResponseResourceProvider(KeycloakSession session) { - - this.session = session; - } - - @Override - public Object getResource() { - - return this; - } - - @Override - public void close() { - - } - - @GET - @NoCache - @Produces(MediaType.TEXT_HTML) - @Path("/auth-complete") - public Response completeAuthentication(@QueryParam("code") String code, - @QueryParam("scope") String scope, - @QueryParam("state") String state) { - - RealmModel realm = getAuthenticationRealm(); - if(!stateIsAssociatedToRealm(realm, state)) { - log.infov("Realm {0} is not associated to authz response and state {1}",realm.getName(),state); - return createErrorResponse(ERR_MSG_INVALID_REALM); - } - - if(!realmHasActionUri(realm)) { - log.infov("Realm {0} has no action uri set to complete authentication",realm.getName()); - return createErrorResponse(ERR_MSG_MISSING_DATA); - } - saveAuthResultInRealm(realm, code, state); - return createFinalizeAuthResponse(realm.getAttribute(SessionAttributes.KC_ACTION_URI)); - } - - private final RealmModel getAuthenticationRealm() { - - return session.getContext().getRealm(); - } - - private final boolean stateIsAssociatedToRealm(RealmModel realm , String state) { - - String expectedstate = realm.getAttribute(SessionAttributes.JANS_OIDC_STATE); - - return state.equals(expectedstate); - } - - private final boolean realmHasActionUri(RealmModel realm) { - - String actionuri = realm.getAttribute(SessionAttributes.KC_ACTION_URI); - return (actionuri != null); - } - - private final void saveAuthResultInRealm(RealmModel realm, String code, String session_state) { - - realm.setAttribute(SessionAttributes.JANS_OIDC_CODE,code); - realm.setAttribute(SessionAttributes.JANS_SESSION_STATE,session_state); - } - - private final Response createResponseWithForm(String formtemplate,Map attributes) { - - LoginFormsProvider lfp = session.getProvider(LoginFormsProvider.class); - - if(attributes != null && !attributes.isEmpty()) { - for(String key: attributes.keySet()) { - lfp.setAttribute(key,attributes.get(key)); - } - } - return lfp.createForm(formtemplate); - } - - private final Response createErrorResponse(String errmsgid) { - - Map attributes = new HashMap(); - attributes.put(ERR_MSG_TPL_PARAM,errmsgid); - return createResponseWithForm(JANS_AUTH_RESPONSE_ERR_FTL,attributes); - } - - private final Response createFinalizeAuthResponse(String actionuri) { - - Map attributes = new HashMap(); - attributes.put(ACTION_URI_TPL_PARAM,actionuri); - return createResponseWithForm(JANS_AUTH_RESPONSE_COMPLETE_FTL, attributes); - } -} diff --git a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java b/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java deleted file mode 100644 index 8384f1bf782..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.jans.kc.spi.rest; - -import org.keycloak.Config.Scope; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.services.resource.RealmResourceProvider; -import org.keycloak.services.resource.RealmResourceProviderFactory; - -import io.jans.kc.spi.ProviderIDs; - -public class JansAuthResponseResourceProviderFactory implements RealmResourceProviderFactory { - - private static final String ID = ProviderIDs.JANS_AUTH_RESPONSE_REST_PROVIDER; - - @Override - public String getId() { - - return ID; - } - - @Override - public RealmResourceProvider create(KeycloakSession session) { - - return new JansAuthResponseResourceProvider(session); - } - - @Override - public void init(Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - - @Override - public void close() { - - } -} \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory deleted file mode 100644 index 3f98bf660d4..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ /dev/null @@ -1 +0,0 @@ -io.jans.kc.spi.auth.JansAuthenticatorFactory \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory deleted file mode 100644 index 66bd969afe6..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory +++ /dev/null @@ -1 +0,0 @@ -io.jans.kc.spi.rest.JansAuthResponseResourceProviderFactory \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/messages/messages_en.properties b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/messages/messages_en.properties deleted file mode 100644 index c710f1fca8a..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/messages/messages_en.properties +++ /dev/null @@ -1,9 +0,0 @@ -jans.redirect.to-jans=You''re being redirected to Janssen... -jans.redirect.too-long-click-here=If it''s taking too long, Click Here -jans.error-title=We''re Sorry ... -jans.error-description=Error processing authentication request. Please contact your administrator. -jans.error-invalid-realm=Error processing authentication request. The associated session is probably stale \ - or does not exist. Please contact your administrator. -jans.error-missing-data=Error processing authentication request. Missing or unavailable data required to complete \ - the authentication. Please contact your administrator. -jans.complete-auth-button=Click here if you're not being redirected \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen project favicon transparent 50px 50px.png b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen project favicon transparent 50px 50px.png deleted file mode 100644 index f3c9c3e58ca60cdefc3a4c6b4bde9196386aaa08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9365 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 Pamd_w53$tHE&6`|1Y5Vp diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen-project.jpg b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen-project.jpg deleted file mode 100644 index b3296c5caed68b107da584363268d9b794b30161..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88147 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 diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen_dove_icon.jpg b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/resources/img/janssen_dove_icon.jpg deleted file mode 100644 index cd254cd3ef2ea8f6cf6288c7f0241ff7072bb9ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36751 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 -<@layout.registrationLayout displayMessage=false; section> - <#if section = "header"> - ${kcSanitize(msg("jans.error-title"))?no_esc} - <#elseif section = "form"> -
-

${kcSanitize(msg("jans.error-description"))?no_esc}

- <#if skipLink??> - <#else> - <#if client?? && client.baseUrl?has_content> -

${kcSanitize(msg("backToApplication"))?no_esc}

- - -
- - \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl deleted file mode 100644 index c54179638ac..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-redirect.ftl +++ /dev/null @@ -1,37 +0,0 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout; section> - <#if section="header"> - ${msg("jans.redirect.to-jans")} - <#elseif section="form"> -
- - - <#list openIdAuthParams?keys as paramname> - - - -
-
-
- -
-
-
-
- - - - - - \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl deleted file mode 100644 index 21e82650879..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-complete.ftl +++ /dev/null @@ -1,23 +0,0 @@ - - - Janssen Bridge :: Completing Authentication - - -
-
- ${msg('jans.complete-auth-button')} - - - \ No newline at end of file diff --git a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl b/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl deleted file mode 100644 index 177ae31a123..00000000000 --- a/jans-keycloak-integration/authenticator/src/main/resources/theme-resources/templates/jans-auth-response-error.ftl +++ /dev/null @@ -1,16 +0,0 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout displayMessage=false; section> - <#if section = "header"> - ${kcSanitize(msg("jans.error-title"))?no_esc} - <#elseif section = "form"> -
-

${kcSanitize(msg(authError))?no_esc}

- <#if skipLink??> - <#else> - <#if client?? && client.baseUrl?has_content> -

${kcSanitize(msg("backToApplication"))?no_esc}

- - -
- - \ No newline at end of file diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml index 1502fc74538..703a6aa857e 100644 --- a/jans-keycloak-integration/pom.xml +++ b/jans-keycloak-integration/pom.xml @@ -57,7 +57,6 @@ - authenticator job-scheduler spi From b571739af5e8c6be7e50aaa44b0b04f428b34fbb Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Mon, 24 Jun 2024 12:04:10 +0100 Subject: [PATCH 30/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 * fixes suggested by static analyser Signed-off-by: Rolain Djeumen --- .../main/java/io/jans/kc/scheduler/App.java | 4 +- .../scheduler/TrustRelationshipSyncJob.java | 15 ------ .../scheduler/job/impl/QuartzJobWrapper.java | 2 - .../jans/kc/model/JansUserAttributeModel.java | 1 - .../java/io/jans/kc/model/JansUserModel.java | 51 ++++++++----------- .../io/jans/kc/model/internal/JansPerson.java | 14 ++--- .../java/io/jans/kc/oidc/OIDCAuthRequest.java | 4 +- .../io/jans/kc/oidc/OIDCMetaCacheKeys.java | 4 ++ .../kc/oidc/impl/HashBasedOIDCMetaCache.java | 40 +++++++-------- .../jans/kc/spi/auth/JansAuthenticator.java | 8 +-- 10 files changed, 55 insertions(+), 88 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index c4d39157eee..db7c1593e4e 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -95,13 +95,15 @@ public static void main(String[] args) throws InterruptedException, ParserCreate } System.exit(-1); return; + }catch(InterruptedException e) { + log.error("Application interrupted",e); + throw e; }catch(Exception e) { log.error("Fatal error starting application",e); if(jobScheduler != null ) { jobScheduler.stop(); } System.exit(-1); - return; } } diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java index d23120b7228..1e01ea212e9 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/TrustRelationshipSyncJob.java @@ -207,14 +207,6 @@ private void addReleasedAttributesToManagedSamlClient(ManagedSamlClient client, List protmappers = releasedattributes.stream().map((r)-> { log.debug("Preparing to add released attribute {} to managed saml client with clientId {}",r.getName(),client.clientId()); - /*return ProtocolMapper - .samlUserAttributeMapper(samlUserAttributeMapperId) - .name(generateKeycloakUniqueProtocolMapperName(r)) - .userAttribute(r.getName()) - .friendlyName(r.getDisplayName()!=null?r.getDisplayName():r.getName()) - .attributeName(r.getSaml2Uri()) - .attributeNameFormatUriReference() - .build(); */ return ProtocolMapper .samlUserAttributeMapper(samlUserAttributeMapperId) .name(generateKeycloakUniqueProtocolMapperName(r)) @@ -228,13 +220,6 @@ private void addReleasedAttributesToManagedSamlClient(ManagedSamlClient client, private void updateManagedSamlClientProtocolMapper(ManagedSamlClient client, ProtocolMapper mapper, JansAttributeRepresentation releasedattribute) { log.debug("Updating managed client released attribute. Client id: {} / Attribute name: {}",client.clientId(),releasedattribute.getName()); - /*ProtocolMapper newmapper = ProtocolMapper - .samlUserAttributeMapper(mapper) - .userAttribute(releasedattribute.getName()) - .friendlyName(releasedattribute.getDisplayName()!=null?releasedattribute.getDisplayName():releasedattribute.getName()) - .attributeName(releasedattribute.getSaml2Uri()) - .attributeNameFormatUriReference() - .build(); */ ProtocolMapper newmapper = ProtocolMapper .samlUserAttributeMapper(mapper) .jansAttributeName(releasedattribute.getName()) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java index 5a7c5cde4a4..cf177c1484f 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/job/impl/QuartzJobWrapper.java @@ -38,8 +38,6 @@ public void execute(JobExecutionContext context) throws JobExecutionException { io.jans.kc.scheduler.job.Job job = (io.jans.kc.scheduler.job.Job) constructor.newInstance(); ExecutionContext effectivecontext = new QuartzExecutionContext(context.getMergedJobDataMap()); job.run(effectivecontext); - } catch(ReflectiveOperationException e) { - throw new JobExecutionException("Failed to run job " + jobname,e); }catch(Exception e) { throw new JobExecutionException("Failed to run job " + jobname,e); } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java index cad9a2c2545..43d4296e6a4 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserAttributeModel.java @@ -8,7 +8,6 @@ import org.apache.commons.lang3.StringUtils; -import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java index 8eb3664af83..fe9ce074ec5 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java @@ -1,7 +1,6 @@ package io.jans.kc.model; import io.jans.kc.model.internal.JansPerson; -import io.jans.orm.model.base.CustomObjectAttribute; import java.util.ArrayList; import java.util.HashMap; @@ -20,8 +19,6 @@ import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.StorageId; -import org.jboss.logging.Logger; - public class JansUserModel implements UserModel { private static final String INUM_ATTR_NAME = "inum"; @@ -31,18 +28,13 @@ public class JansUserModel implements UserModel { private static final String GIVEN_NAME_ATTR_NAME = "givenName"; private static final String MAIL_ATTR_NAME = "mail"; private static final String EMAIL_VERIFIED_ATTR_NAME = "emailVerified"; - - private static final Logger log = Logger.getLogger(JansUserModel.class); + private static final String USER_READ_ONLY_EXCEPTION_MSG = "User is read-only for this update"; private final JansPerson jansPerson; private final StorageId storageId; - private final ComponentModel storageProviderModel; - private final KeycloakSession session; public JansUserModel(KeycloakSession session, ComponentModel storageProviderModel, JansPerson jansPerson) { - this.session = session; - this.storageProviderModel = storageProviderModel; this.jansPerson = jansPerson; String userId = jansPerson.customAttributeValue(INUM_ATTR_NAME); this.storageId = new StorageId(storageProviderModel.getId(),userId); @@ -63,7 +55,7 @@ public String getUsername() { @Override public void setUsername(String username) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -83,7 +75,7 @@ public Long getCreatedTimestamp() { @Override public void setCreatedTimestamp(Long timestamp) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -93,31 +85,28 @@ public boolean isEnabled() { if(enabledStr == null) { return false; } - if("active".equals(enabledStr)) { - return true; - } - return false; + return "active".equals(enabledStr); } @Override public void setEnabled(boolean enabled) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override public void setSingleAttribute(String name, String value) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override public void setAttribute(String name, List value) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override public void removeAttribute(String name) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @@ -170,12 +159,12 @@ public Stream getRequiredActionsStream() { @Override public void addRequiredAction(String action) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override public void removeRequiredAction(String action) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -187,7 +176,7 @@ public String getFirstName() { @Override public void setFirstName(String firstName) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -199,7 +188,7 @@ public String getLastName() { @Override public void setLastName(String lastName) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -211,7 +200,7 @@ public String getEmail() { @Override public void setEmail(final String email) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -231,7 +220,7 @@ public boolean isEmailVerified() { @Override public void setEmailVerified(boolean verified) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -255,13 +244,13 @@ public long getGroupsCountByNameContaining(String search) { @Override public void joinGroup(GroupModel group) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override public void leaveGroup(GroupModel group) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -279,7 +268,7 @@ public String getFederationLink() { @Override public void setFederationLink(String link) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -291,7 +280,7 @@ public String getServiceAccountClientLink() { @Override public void setServiceAccountClientLink(String clientInternalId) { - throw new ReadOnlyException("User is read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -321,7 +310,7 @@ public boolean hasRole(RoleModel role) { @Override public void grantRole(RoleModel role) { - throw new ReadOnlyException("User is in read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } @Override @@ -333,7 +322,7 @@ public Stream getRoleMappingsStream() { @Override public void deleteRoleMapping(RoleModel role) { - throw new ReadOnlyException("User is in read-only for this update"); + throw new ReadOnlyException(USER_READ_ONLY_EXCEPTION_MSG); } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java index 86df86812c7..829f46a16e5 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; -import io.jans.model.JansAttribute; import io.jans.orm.annotation.*; import io.jans.orm.model.base.CustomObjectAttribute; @@ -21,11 +20,6 @@ public class JansPerson implements Serializable { @AttributesList(name="name",value="values",multiValued="multiValued") private List customAttributes = new ArrayList<>(); - - public JansPerson() { - - } - public String getDn() { return this.dn; @@ -72,7 +66,7 @@ public List customAttributeValues(final String name) { for(CustomObjectAttribute customAttribute : customAttributes) { if(customAttribute.getName().equals(name)) { List values = customAttribute.getValues(); - if(values == null || values.size() == 0) { + if(values == null || values.isEmpty()) { return new ArrayList<>(); } return convertToString(values); @@ -95,7 +89,7 @@ public String customAttributeValue(final String attributeName) { for(CustomObjectAttribute customAttribute : customAttributes) { if(customAttribute.getName().equals(attributeName)) { List values = customAttribute.getValues(); - if(values == null || values.size() == 0) { + if(values == null || values.isEmpty()) { return null; } List ret = convertToString(values); @@ -113,8 +107,8 @@ private List convertToString(List values) { List ret = new ArrayList<>(); for(Object val : values) { - if(val instanceof String) { - ret.add((String) val); + if(val instanceof String strval) { + ret.add((String) strval); }else { ret.add(val.toString()); } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java index 5a5b49b7cbd..489e7fa5a0f 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCAuthRequest.java @@ -17,8 +17,8 @@ public OIDCAuthRequest() { this.clientId = null; this.state = null; this.nonce = null; - this.scopes = new ArrayList(); - this.responseTypes = new ArrayList(); + this.scopes = new ArrayList<>(); + this.responseTypes = new ArrayList<>(); this.redirectUri = null; } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java index 3a64b29997d..f5b69c39de0 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/OIDCMetaCacheKeys.java @@ -4,4 +4,8 @@ public class OIDCMetaCacheKeys { public static final String AUTHORIZATION_URL = "oidc.authorization.url"; public static final String TOKEN_URL = "oidc.token.url"; public static final String USERINFO_URL = "oidc.userinfo.url"; + + private OIDCMetaCacheKeys() { + //private constructor + } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java index bf056a3e56e..8e2758938b9 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java @@ -2,16 +2,13 @@ import java.util.Map; -import org.jboss.logging.Logger; - import io.jans.kc.oidc.OIDCMetaCache; import java.util.HashMap; public class HashBasedOIDCMetaCache implements OIDCMetaCache{ - private static final Logger log = Logger.getLogger(HashBasedOIDCMetaCache.class); - private static final long DEFAULT_CACHE_TTL = 20*60; // 20 seconds + private static final long DEFAULT_CACHE_TTL = 20*60l; // 20 seconds private long cacheEntryTtl; @@ -28,7 +25,7 @@ public HashBasedOIDCMetaCache(long cacheEntryTtl) { this.cacheEntryTtl = DEFAULT_CACHE_TTL; } this.cacheEntryTtl = this.cacheEntryTtl * 1000; // convert to milliseconds - this.cacheEntries = new HashMap>(); + this.cacheEntries = new HashMap<>(); } @Override @@ -63,42 +60,41 @@ private boolean issuerCacheEntryIsMissing(String issuer) { private Object getIssuerCacheEntryValue(String issuer, String key) { - Map issuer_cache = cacheEntries.get(issuer); - return issuer_cache.get(key).getValue(); + Map issuerCache = cacheEntries.get(issuer); + return issuerCache.get(key).getValue(); } private void createIfNotExistIssuerCacheEntry(String issuer) { - if(!cacheEntries.containsKey(issuer)) { - cacheEntries.put(issuer,new HashMap()); - } + cacheEntries.computeIfAbsent(issuer, k-> new HashMap<>()); } private void addIssuerCacheEntry(String issuer,String key, Object value) { Map issuerCache = cacheEntries.get(issuer); - for(String existingkey : issuerCache.keySet()) { - if(existingkey.equalsIgnoreCase(key)) { + if(issuerCache == null) { + return; + } + + for(Map.Entry entry : issuerCache.entrySet()) { + if(entry.getKey().equalsIgnoreCase(key)) { //update cache entry - CacheEntry cache_entry = issuerCache.get(existingkey); - cache_entry.updateValue(value); + entry.getValue().updateValue(value); return; } } - issuerCache.put(key,new CacheEntry(cacheEntryTtl, value)); } private void performHouseCleaning() { for(String issuer: cacheEntries.keySet()) { - Map issuer_cache = cacheEntries.get(issuer); - for(String key :issuer_cache.keySet()) { - CacheEntry cache_entry = issuer_cache.get(key); - if(cache_entry.isExpired()) { - issuer_cache.remove(key); - } - } + Map issuerCache = cacheEntries.get(issuer); + for(Map.Entry entry : issuerCache.entrySet()) { + if(entry.getValue().isExpired()) { + issuerCache.remove(entry.getKey()); + } + } } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java index 2fa0cea1a7c..aa686dbfcaa 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java @@ -5,12 +5,12 @@ import java.net.URISyntaxException; import java.net.URLDecoder; +import java.security.SecureRandom; import java.text.MessageFormat; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; import java.util.ArrayList; import jakarta.ws.rs.core.Response; @@ -267,9 +267,9 @@ private Configuration pluginConfigurationFromContext(AuthenticationFlowContext c String extra_scopes = config.getConfig().get(JansAuthenticatorConfigProp.EXTRA_SCOPES.getName()); List parsed_extra_scopes = new ArrayList<>(); if(extra_scopes != null) { - String [] tokens = extra_scopes.split("\\s*,\\s*"); + String [] tokens = extra_scopes.split(","); for(String token : tokens) { - parsed_extra_scopes.add(token); + parsed_extra_scopes.add(token.trim()); } } @@ -302,7 +302,7 @@ private String generateRandomString(int length) { int leftlimit = 48; int rightlimit = 122; - return new Random().ints(leftlimit,rightlimit+1) + return new SecureRandom().ints(leftlimit,rightlimit+1) .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) .limit(length) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) From 1a1de0f47fb28d3a7035d671c75d03a8db0a2124 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Mon, 24 Jun 2024 13:33:09 +0100 Subject: [PATCH 31/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 Signed-off-by: Rolain Djeumen --- .../main/java/io/jans/kc/scheduler/App.java | 2 +- .../java/io/jans/kc/model/JansUserModel.java | 3 +- .../io/jans/kc/model/internal/JansPerson.java | 2 +- .../kc/oidc/impl/HashBasedOIDCMetaCache.java | 10 ++-- .../kc/oidc/impl/NimbusOIDCRefreshToken.java | 4 ++ .../jans/kc/oidc/impl/NimbusOIDCService.java | 12 ++-- .../kc/oidc/impl/NimbusOIDCTokenResponse.java | 2 - .../main/java/io/jans/kc/spi/ProviderIDs.java | 4 ++ .../jans/kc/spi/auth/JansAuthenticator.java | 56 +++++++++---------- .../kc/spi/auth/JansAuthenticatorFactory.java | 6 +- .../jans/kc/spi/auth/SessionAttributes.java | 4 ++ .../impl/DefaultJansThinBridgeProvider.java | 3 +- .../DefaultJansThinBridgeProviderFactory.java | 37 ++---------- .../saml/JansSamlUserAttributeMapper.java | 12 +--- .../JansAuthResponseResourceProvider.java | 14 ++--- ...nsAuthResponseResourceProviderFactory.java | 4 +- .../spi/storage/JansUserStorageProvider.java | 6 +- 17 files changed, 75 insertions(+), 106 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index db7c1593e4e..78e300b8600 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -97,7 +97,7 @@ public static void main(String[] args) throws InterruptedException, ParserCreate return; }catch(InterruptedException e) { log.error("Application interrupted",e); - throw e; + System.exit(-1); }catch(Exception e) { log.error("Fatal error starting application",e); if(jobScheduler != null ) { diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java index fe9ce074ec5..e53bb9544ba 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/JansUserModel.java @@ -12,7 +12,6 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.models.RoleModel; import org.keycloak.models.SubjectCredentialManager; @@ -33,7 +32,7 @@ public class JansUserModel implements UserModel { private final JansPerson jansPerson; private final StorageId storageId; - public JansUserModel(KeycloakSession session, ComponentModel storageProviderModel, JansPerson jansPerson) { + public JansUserModel(ComponentModel storageProviderModel, JansPerson jansPerson) { this.jansPerson = jansPerson; String userId = jansPerson.customAttributeValue(INUM_ATTR_NAME); diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java index 829f46a16e5..936ed16b191 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/model/internal/JansPerson.java @@ -108,7 +108,7 @@ private List convertToString(List values) { List ret = new ArrayList<>(); for(Object val : values) { if(val instanceof String strval) { - ret.add((String) strval); + ret.add(strval); }else { ret.add(val.toString()); } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java index 8e2758938b9..9c1847c16b3 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/HashBasedOIDCMetaCache.java @@ -88,11 +88,11 @@ private void addIssuerCacheEntry(String issuer,String key, Object value) { private void performHouseCleaning() { - for(String issuer: cacheEntries.keySet()) { - Map issuerCache = cacheEntries.get(issuer); - for(Map.Entry entry : issuerCache.entrySet()) { - if(entry.getValue().isExpired()) { - issuerCache.remove(entry.getKey()); + for(Map.Entry> cacheEntry: cacheEntries.entrySet()) { + Map issuerCache = cacheEntries.get(cacheEntry.getKey()); + for(Map.Entry issuerEntry : issuerCache.entrySet()) { + if(issuerEntry.getValue().isExpired()) { + issuerCache.remove(issuerEntry.getKey()); } } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java index 70c868dab03..76ea7434acc 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCRefreshToken.java @@ -12,4 +12,8 @@ public NimbusOIDCRefreshToken(RefreshToken refreshToken) { this.refreshToken = refreshToken; } + private RefreshToken refreshTokenRef() { + + return this.refreshToken; + } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java index 318f504c16f..030f611d5e1 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCService.java @@ -41,12 +41,10 @@ import java.net.URISyntaxException; import java.util.List; -import org.jboss.logging.Logger; public class NimbusOIDCService implements OIDCService { - private static final Logger log = Logger.getLogger(NimbusOIDCService.class); - + private OIDCMetaCache metaCache; public NimbusOIDCService(OIDCMetaCache metaCache) { @@ -134,8 +132,8 @@ public OIDCUserInfoResponse requestUserInfo(String issuerUrl, OIDCAccessToken ac BearerAccessToken bearertoken = ((NimbusOIDCAccessToken) accesstoken).asBearerToken(); try { - HTTPResponse http_response = new UserInfoRequest(getUserInfoEndpoint(issuerUrl),bearertoken).toHTTPRequest().send(); - UserInfoResponse userinforesponse = UserInfoResponse.parse(http_response); + HTTPResponse httpResponse = new UserInfoRequest(getUserInfoEndpoint(issuerUrl),bearertoken).toHTTPRequest().send(); + UserInfoResponse userinforesponse = UserInfoResponse.parse(httpResponse); return new NimbusOIDCUserInfoResponse(userinforesponse); } catch (IOException e) { throw new OIDCUserInfoRequestError("I/O error trying to obtain user info",e); @@ -205,9 +203,7 @@ private OIDCProviderMetadata obtainMetadataFromServer(String issuerUrl) throws O try { Issuer issuer = new Issuer(issuerUrl); return OIDCProviderMetadata.resolve(issuer); - }catch(GeneralException e) { - throw new OIDCMetaError("Could not obtain metadata from server",e); - }catch(IOException e) { + }catch(GeneralException | IOException e) { throw new OIDCMetaError("Could not obtain metadata from server",e); } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java index 74cb56d1f76..6f9ddd249b9 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/oidc/impl/NimbusOIDCTokenResponse.java @@ -25,8 +25,6 @@ public NimbusOIDCTokenResponse(TokenResponse tokenResponse) { Tokens tokens = atresponse.getTokens(); this.accessToken = new NimbusOIDCAccessToken(tokens.getAccessToken()); this.refreshToken = new NimbusOIDCRefreshToken(tokens.getRefreshToken()); - }else { - } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java index 2a383043442..e349ecd6808 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/ProviderIDs.java @@ -6,4 +6,8 @@ public class ProviderIDs { public static final String JANS_SAML_USER_ATTRIBUTE_MAPPER_PROVIDER = "kc-jans-saml-user-attribute-mapper"; public static final String JANS_DEFAULT_THIN_BRIDGE_PROVIDER = "kc-jans-thin-bridge-default"; public static final String JANS_USER_STORAGE_PROVIDER = "kc-jans-user-storage"; + + private ProviderIDs() { + //private constructor + } } \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java index aa686dbfcaa..763ab33f516 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java @@ -2,11 +2,8 @@ import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URISyntaxException; import java.net.URLDecoder; - import java.security.SecureRandom; -import java.text.MessageFormat; import java.util.HashMap; import java.util.List; @@ -101,6 +98,10 @@ public void authenticate(AuthenticationFlowContext context) { log.errorv(e,"OIDC Error obtaining the authorization url"); Response response = context.form().createForm(JANS_AUTH_ERROR_FTL); context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); + }catch(NullPointerException e) { + log.errorv(e,"NullPointerException obtaining the authorization url"); + Response response = context.form().createForm(JANS_AUTH_ERROR_FTL); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); } } @@ -113,14 +114,14 @@ public void action(AuthenticationFlowContext context) { return; } - String openid_code = getOpenIdCode(context); - if(openid_code == null) { + String openidCode = getOpenIdCode(context); + if(openidCode == null) { log.errorv("Missing authentication code during response processing"); context.failure(AuthenticationFlowError.INTERNAL_ERROR,onMissingAuthenticationCode(context)); return; } - OIDCTokenRequest tokenrequest = createTokenRequest(config, openid_code, createRedirectUri(context)); + OIDCTokenRequest tokenrequest = createTokenRequest(config, openidCode, createRedirectUri(context)); try { OIDCTokenResponse tokenresponse = oidcService.requestTokens(config.normalizedIssuerUrl(), tokenrequest); if(!tokenresponse.indicatesSuccess()) { @@ -170,20 +171,19 @@ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserMode @Override public void setRequiredActions(KeycloakSession session, RealmModel model, UserModel user) { - - return; + //for now no required actions to specify } @Override public List getRequiredActions(KeycloakSession session) { - return null; + return new ArrayList<>(); } @Override public void close() { - return; + // nothing to do for now when then authenticator is shutdown } private Configuration extractAndValidateConfiguration(AuthenticationFlowContext context) { @@ -230,7 +230,7 @@ private UserModel findUserByNameOrEmail(AuthenticationFlowContext context, Strin private Map parseQueryParameters(String params) { - Map ret = new HashMap(); + Map ret = new HashMap<>(); if(params == null) { return ret; } @@ -260,20 +260,20 @@ private Configuration pluginConfigurationFromContext(AuthenticationFlowContext c return null; } - String server_url = config.getConfig().get(JansAuthenticatorConfigProp.SERVER_URL.getName()); - String client_id = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_ID.getName()); - String client_secret = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_SECRET.getName()); + String serverUrl = config.getConfig().get(JansAuthenticatorConfigProp.SERVER_URL.getName()); + String clientId = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_ID.getName()); + String clientSecret = config.getConfig().get(JansAuthenticatorConfigProp.CLIENT_SECRET.getName()); String issuer = config.getConfig().get(JansAuthenticatorConfigProp.ISSUER.getName()); - String extra_scopes = config.getConfig().get(JansAuthenticatorConfigProp.EXTRA_SCOPES.getName()); - List parsed_extra_scopes = new ArrayList<>(); - if(extra_scopes != null) { - String [] tokens = extra_scopes.split(","); + String extraScopes = config.getConfig().get(JansAuthenticatorConfigProp.EXTRA_SCOPES.getName()); + List parsedExtraScopes = new ArrayList<>(); + if(extraScopes != null) { + String [] tokens = extraScopes.split(","); for(String token : tokens) { - parsed_extra_scopes.add(token.trim()); + parsedExtraScopes.add(token.trim()); } } - return new Configuration(server_url,client_id,client_secret,issuer,parsed_extra_scopes); + return new Configuration(serverUrl,clientId,clientSecret,issuer,parsedExtraScopes); } private final String generateOIDCState() { @@ -364,7 +364,7 @@ public static class ValidationResult { public void addError(String error) { if(errors == null) { - this.errors = new ArrayList(); + this.errors = new ArrayList<>(); } this.errors.add(error); } @@ -418,18 +418,18 @@ public ValidationResult validate() { public String normalizedIssuerUrl() { - String effective_url = issuerUrl; - if(effective_url == null) { - effective_url = serverUrl; + String effectiveUrl = issuerUrl; + if(effectiveUrl == null) { + effectiveUrl = serverUrl; } - if(effective_url == null) { + if(effectiveUrl == null) { return null; } - if(effective_url.charAt(effective_url.length() -1) == '/') { - return effective_url.substring(0, effective_url.length() -1); + if(effectiveUrl.charAt(effectiveUrl.length() -1) == '/') { + return effectiveUrl.substring(0, effectiveUrl.length() -1); } - return effective_url; + return effectiveUrl; } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java index 4cd5cefe7c2..eca25248403 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticatorFactory.java @@ -59,19 +59,19 @@ public Authenticator create(KeycloakSession session) { @Override public void init(Config.Scope config) { - return; + //nothing to do for now during initialization } @Override public void close() { - return; + //nothing to do for now during shutdown } @Override public void postInit(KeycloakSessionFactory factory) { - return; + //nothing to do postInit } @Override diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java index e7b0331939f..0b6c6a1dd05 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/SessionAttributes.java @@ -7,4 +7,8 @@ public class SessionAttributes { public static final String KC_ACTION_URI = "kc.action-uri"; public static final String JANS_OIDC_CODE = "jans.oidc.code"; public static final String JANS_SESSION_STATE = "jans.session.state"; + + private SessionAttributes() { + //private constructor + } } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java index 169aa5bc7bd..40087ffd5a7 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProvider.java @@ -5,7 +5,6 @@ import io.jans.kc.model.JansUserAttributeModel; import io.jans.kc.model.internal.JansPerson; import io.jans.kc.spi.custom.JansThinBridgeProvider; -import io.jans.kc.spi.custom.JansThinBridgeInitException; import io.jans.kc.spi.custom.JansThinBridgeOperationException; import io.jans.model.JansAttribute; @@ -37,7 +36,7 @@ public DefaultJansThinBridgeProvider(final PersistenceEntryManager persistenceEn @Override public void close() { - + //for now , nothing to do during the close of the provider } @Override diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java index ece1c65ac53..3565a88a24f 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/custom/impl/DefaultJansThinBridgeProviderFactory.java @@ -6,7 +6,6 @@ import io.jans.kc.spi.custom.JansThinBridgeProviderFactory; import io.jans.kc.spi.ProviderIDs; import io.jans.kc.spi.custom.JansThinBridgeInitException; -import io.jans.kc.spi.custom.JansThinBridgeInitException; import io.jans.orm.model.PersistenceConfiguration; import io.jans.orm.PersistenceEntryManager; import io.jans.orm.PersistenceEntryManagerFactory; @@ -75,19 +74,17 @@ public JansThinBridgeProvider create(KeycloakSession session) { @Override public void init(Config.Scope config) { - - + //nothing to do during init for now } @Override public void postInit(KeycloakSessionFactory factory) { - + //nothing to do during postInit for now } @Override public void close() { - - + //nothing to do during cost for now } @Override @@ -96,32 +93,6 @@ public String getId() { return PROVIDER_ID; } - - - private final FileConfiguration loadBaseConfiguration(final String filename) { - - try { - return new FileConfiguration(filename); - }catch(Exception e) { - log.errorv(e,"Failed to load configuration from {0}",filename); - final String errordesc = (filename != null ? " from file " + filename : ". Invalid filename specified"); - throw new JansThinBridgeInitException("Failed to load configuration" + errordesc); - } - } - - private final StringEncrypter createStringEncrypterFromSaltFile(final String path) { - - try { - final String salt = cryptographicSaltFromFile(path); - if(StringUtils.isEmpty(salt)) { - return null; - } - return StringEncrypter.instance(salt); - }catch(StringEncrypter.EncryptionException e) { - throw new JansThinBridgeInitException("Failed to create string encrypted",e); - } - } - private final String cryptographicSaltFromFile(final String path) { FileConfiguration cryptoconfig = new FileConfiguration(path); @@ -132,7 +103,7 @@ private final Properties preparePersistenceProperties(final PersistenceConfigura try { FileConfiguration config = persistenceConfiguration.getConfiguration(); - Properties connprops = (Properties) config.getProperties(); + Properties connprops = config.getProperties(); return PropertiesDecrypter.decryptAllProperties(StringEncrypter.defaultInstance(),connprops,salt); }catch(StringEncrypter.EncryptionException e) { throw new JansThinBridgeInitException("Failed to decrypt persistence connection parameters",e); diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java index 89c636096a2..936c6c25b5f 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/protocol/mapper/saml/JansSamlUserAttributeMapper.java @@ -4,7 +4,6 @@ import io.jans.kc.spi.ProviderIDs; import io.jans.kc.spi.custom.JansThinBridgeOperationException; import io.jans.kc.spi.custom.JansThinBridgeProvider; -import io.jans.model.GluuStatus; import java.util.List; @@ -56,24 +55,19 @@ public class JansSamlUserAttributeMapper extends AbstractSAMLProtocolMapper impl .build(); } - public JansSamlUserAttributeMapper() { - - - } - @Override public void init(Config.Scope scope) { - + //nothing for now to do in init } @Override public void close() { - + //nothing for now to do in close } @Override public void postInit(KeycloakSessionFactory factory) { - + //nothing to do for now in postInit } @Override diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java index 478a4d59fa6..8e6bde77b78 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProvider.java @@ -48,7 +48,7 @@ public Object getResource() { @Override public void close() { - + //nothing to do for now } @GET @@ -91,10 +91,10 @@ private final boolean realmHasActionUri(RealmModel realm) { return (actionuri != null); } - private final void saveAuthResultInRealm(RealmModel realm, String code, String session_state) { + private final void saveAuthResultInRealm(RealmModel realm, String code, String sessionState) { realm.setAttribute(SessionAttributes.JANS_OIDC_CODE,code); - realm.setAttribute(SessionAttributes.JANS_SESSION_STATE,session_state); + realm.setAttribute(SessionAttributes.JANS_SESSION_STATE,sessionState); } private final Response createResponseWithForm(String formtemplate,Map attributes) { @@ -102,8 +102,8 @@ private final Response createResponseWithForm(String formtemplate,Map attrEntry: attributes.entrySet()) { + lfp.setAttribute(attrEntry.getKey(),attrEntry.getValue()); } } return lfp.createForm(formtemplate); @@ -111,14 +111,14 @@ private final Response createResponseWithForm(String formtemplate,Map attributes = new HashMap(); + Map attributes = new HashMap<>(); attributes.put(ERR_MSG_TPL_PARAM,errmsgid); return createResponseWithForm(JANS_AUTH_RESPONSE_ERR_FTL,attributes); } private final Response createFinalizeAuthResponse(String actionuri) { - Map attributes = new HashMap(); + Map attributes = new HashMap<>(); attributes.put(ACTION_URI_TPL_PARAM,actionuri); return createResponseWithForm(JANS_AUTH_RESPONSE_COMPLETE_FTL, attributes); } diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java index 8384f1bf782..6c93fc96f04 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/rest/JansAuthResponseResourceProviderFactory.java @@ -32,11 +32,11 @@ public void init(Scope config) { @Override public void postInit(KeycloakSessionFactory factory) { - + //nothing to do here post init } @Override public void close() { - + //nothing to do here on close } } \ No newline at end of file diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java index 554f20c9321..4b065273030 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/storage/JansUserStorageProvider.java @@ -44,7 +44,7 @@ public UserModel getUserByUsername(RealmModel realm, String username) { log.infov("getUserByUsername(). Username: {0}",username); JansPerson person = jansThinBridge.getJansUserByUsername(username); if(person != null) { - return new JansUserModel(session,model,person); + return new JansUserModel(model,person); } return null; }catch(JansThinBridgeOperationException e) { @@ -60,7 +60,7 @@ public UserModel getUserByEmail(RealmModel realm, String email) { log.infov("getUserByEmail(). Email : {0}",email); JansPerson person = jansThinBridge.getJansUserByEmail(email); if(person != null) { - return new JansUserModel(session,model,person); + return new JansUserModel(model,person); } return null; }catch(JansThinBridgeOperationException e) { @@ -78,7 +78,7 @@ public UserModel getUserById(RealmModel realm, String id) { final String inum = storageId.getExternalId(); JansPerson person = jansThinBridge.getJansUserByInum(inum); if(person != null) { - return new JansUserModel(session,model,person); + return new JansUserModel(model,person); } return null; }catch(JansThinBridgeOperationException e) { From f60935145f34267d2efaa816d512e7eb0c7d1dc6 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Mon, 24 Jun 2024 14:52:36 +0100 Subject: [PATCH 32/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 Signed-off-by: Rolain Djeumen --- .../main/java/io/jans/kc/scheduler/App.java | 2 +- .../jans/kc/spi/auth/JansAuthenticator.java | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java index 78e300b8600..ad6edd2bec2 100644 --- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java +++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/App.java @@ -97,7 +97,7 @@ public static void main(String[] args) throws InterruptedException, ParserCreate return; }catch(InterruptedException e) { log.error("Application interrupted",e); - System.exit(-1); + Thread.currentThread().interrupt(); }catch(Exception e) { log.error("Fatal error starting application",e); if(jobScheduler != null ) { diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java index 763ab33f516..860b57434d1 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java @@ -70,8 +70,17 @@ public void authenticate(AuthenticationFlowContext context) { return; } + Response response = null; try { URI redirecturi = createRedirectUri(context); + + if(redirecturi == null) { + log.error("Invalid redirect URI"); + response = context.form().createForm(JANS_AUTH_ERROR_FTL); + context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); + response.close(); + return; + } URI actionuri = createActionUrl(context); String state = generateOIDCState(); @@ -82,7 +91,7 @@ public void authenticate(AuthenticationFlowContext context) { URI loginurl = oidcService.createAuthorizationUrl(config.normalizedIssuerUrl(), oidcauthrequest); URI loginurlnoparams = UriBuilder.fromUri(loginurl.toString()).replaceQuery(null).build(); - Response response = context + response = context .form() .setActionUri(actionuri) .setAttribute(JANS_LOGIN_URL_ATTRIBUTE,loginurlnoparams.toString()) @@ -94,14 +103,12 @@ public void authenticate(AuthenticationFlowContext context) { saveRealmStringData(context,SessionAttributes.JANS_OIDC_STATE,state); context.challenge(response); + response.close(); }catch(OIDCMetaError e) { log.errorv(e,"OIDC Error obtaining the authorization url"); - Response response = context.form().createForm(JANS_AUTH_ERROR_FTL); - context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); - }catch(NullPointerException e) { - log.errorv(e,"NullPointerException obtaining the authorization url"); - Response response = context.form().createForm(JANS_AUTH_ERROR_FTL); + response = context.form().createForm(JANS_AUTH_ERROR_FTL); context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); + response.close(); } } From d5f9221a9435b1993e46f0dfe39baaea3f9aba59 Mon Sep 17 00:00:00 2001 From: Rolain Djeumen Date: Mon, 24 Jun 2024 15:36:20 +0100 Subject: [PATCH 33/33] feat(jans-keycloak-integration): enhancements to jans-keycloak-integration #8614 Signed-off-by: Rolain Djeumen --- .../src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java index 860b57434d1..c59bbe8665e 100644 --- a/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java +++ b/jans-keycloak-integration/spi/src/main/java/io/jans/kc/spi/auth/JansAuthenticator.java @@ -78,7 +78,6 @@ public void authenticate(AuthenticationFlowContext context) { log.error("Invalid redirect URI"); response = context.form().createForm(JANS_AUTH_ERROR_FTL); context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); - response.close(); return; } URI actionuri = createActionUrl(context); @@ -103,12 +102,10 @@ public void authenticate(AuthenticationFlowContext context) { saveRealmStringData(context,SessionAttributes.JANS_OIDC_STATE,state); context.challenge(response); - response.close(); }catch(OIDCMetaError e) { log.errorv(e,"OIDC Error obtaining the authorization url"); response = context.form().createForm(JANS_AUTH_ERROR_FTL); context.failure(AuthenticationFlowError.INTERNAL_ERROR,response); - response.close(); } }