diff --git a/jans-core/saml/pom.xml b/jans-core/saml/pom.xml
index b60f26c7f44..84d053dd652 100644
--- a/jans-core/saml/pom.xml
+++ b/jans-core/saml/pom.xml
@@ -18,6 +18,8 @@
true
**/*.xml
+ **/*.xsd
+ **/*.dtd
**/services/*
**/*.properties
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/EndpointBuilder.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/EndpointBuilder.java
index 28e2fb1b91b..ff0890a50aa 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/EndpointBuilder.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/EndpointBuilder.java
@@ -1,5 +1,6 @@
package io.jans.saml.metadata.builder;
+import io.jans.saml.metadata.model.SAMLBinding;
import io.jans.saml.metadata.model.Endpoint;
public class EndpointBuilder {
@@ -11,7 +12,7 @@ public EndpointBuilder(final Endpoint endpoint) {
this.endpoint = endpoint;
}
- public EndpointBuilder binding(final String binding) {
+ public EndpointBuilder binding(final SAMLBinding binding) {
this.endpoint.setBinding(binding);
return this;
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/SPSSODescriptorBuilder.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/SPSSODescriptorBuilder.java
index fd095206c0c..83d60d81f7e 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/SPSSODescriptorBuilder.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/builder/SPSSODescriptorBuilder.java
@@ -21,7 +21,7 @@ public SPSSODescriptorBuilder wantAssertionsSigned(final Boolean wantAssertionsS
return this;
}
- public IndexedEndpointBuilder assersionConsumerService() {
+ public IndexedEndpointBuilder assertionConsumerService() {
IndexedEndpoint endpoint = new IndexedEndpoint();
spssoDescriptor().addAssertionConsumerService(endpoint);
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/Endpoint.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/Endpoint.java
index d3cd6816ceb..893417a11c3 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/Endpoint.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/Endpoint.java
@@ -2,7 +2,7 @@
public class Endpoint {
- private String binding;
+ private SAMLBinding binding;
private String location;
private String responseLocation;
@@ -13,12 +13,12 @@ public Endpoint() {
this.responseLocation = null;
}
- public String getBinding() {
+ public SAMLBinding getBinding() {
return this.binding;
}
- public void setBinding(final String binding) {
+ public void setBinding(final SAMLBinding binding) {
this.binding = binding;
}
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/EntityDescriptor.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/EntityDescriptor.java
index 567532d067c..ec026fec56b 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/EntityDescriptor.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/EntityDescriptor.java
@@ -70,6 +70,14 @@ public List getSpssoDescriptors() {
return this.spssoDescriptors;
}
+ public SPSSODescriptor getFirstSpssoDescriptor() {
+
+ if(!this.spssoDescriptors.isEmpty()) {
+ return this.spssoDescriptors.get(0);
+ }
+ return null;
+ }
+
public void addSpssoDescriptor(final SPSSODescriptor descriptor) {
this.spssoDescriptors.add(descriptor);
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/KeyDescriptor.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/KeyDescriptor.java
index e924db6901f..b045a425ee9 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/KeyDescriptor.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/KeyDescriptor.java
@@ -24,6 +24,16 @@ public String getUse() {
return this.use;
}
+ public boolean isEncryptionKey() {
+
+ return "encryption".equalsIgnoreCase(use);
+ }
+
+ public boolean isSigningKey() {
+
+ return "signing".equalsIgnoreCase(use);
+ }
+
public void setUse(final String use) {
this.use = use;
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/RoleDescriptor.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/RoleDescriptor.java
index d2bc84eacc9..0eda80303d4 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/RoleDescriptor.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/RoleDescriptor.java
@@ -90,6 +90,22 @@ public List getKeyDescriptors() {
return this.keyDescriptors;
}
+ public List getEncryptionKeys() {
+
+ return keyDescriptors
+ .stream()
+ .filter((k) -> { return k.isEncryptionKey();})
+ .toList();
+ }
+
+ public List getSigningKeys() {
+
+ return keyDescriptors
+ .stream()
+ .filter((k)-> { return k.isSigningKey();})
+ .toList();
+ }
+
public void addKeyDescriptor(final KeyDescriptor keyDescriptor) {
this.keyDescriptors.add(keyDescriptor);
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLBinding.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLBinding.java
new file mode 100644
index 00000000000..fb69487f499
--- /dev/null
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLBinding.java
@@ -0,0 +1,32 @@
+package io.jans.saml.metadata.model;
+
+public enum SAMLBinding {
+ SOAP("urn:oasis:names:tc:SAML:2.0:bindings:SOAP"),
+ REVERSE_SOAP("urn:oasis:names:tc:SAML:2.0:bindings:PAOS"),
+ HTTP_REDIRECT("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"),
+ HTTP_POST("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
+ HTTP_ARTIFACT("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"),
+ URI("urn:oasis:names:tc:SAML:2.0:bindings:URI"),
+ UNKNOWN("");
+
+ private final String value;
+
+ private SAMLBinding(final String value) {
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+
+ public static SAMLBinding fromString(final String value) {
+
+ for(SAMLBinding binding: SAMLBinding.values()) {
+ if(binding.value.equals(value)) {
+ return binding;
+ }
+ }
+ return UNKNOWN;
+ }
+}
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLMetadata.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLMetadata.java
index 585ced4e060..9abbccb8c38 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLMetadata.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SAMLMetadata.java
@@ -1,6 +1,8 @@
package io.jans.saml.metadata.model;
import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
public class SAMLMetadata {
@@ -15,4 +17,21 @@ public final List getEntityDescriptors() {
return this.entityDescriptors;
}
+
+
+ public final EntityDescriptor getEntityDescriptorByEntityId(final String entityId) {
+
+ Optional ret = entityDescriptors.stream()
+ .filter((e) -> {return e.getEntityId().equals(entityId);})
+ .findFirst();
+
+ return (ret.isEmpty()? null : ret.get());
+ }
+
+ public final List getEntityDescriptorIds() {
+
+ return entityDescriptors.stream()
+ .map((e) -> {return e.getEntityId();})
+ .collect(Collectors.toList());
+ }
}
\ No newline at end of file
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SPSSODescriptor.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SPSSODescriptor.java
index 161d0809d53..16ba5861958 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SPSSODescriptor.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/SPSSODescriptor.java
@@ -41,6 +41,21 @@ public List getAssertionConsumerServices() {
return this.assertionConsumerServices;
}
+ public List getAssertionConsumerServices(SAMLBinding binding) {
+
+ return assertionConsumerServices.stream()
+ .filter((e) -> { return e.getBinding() == binding; })
+ .toList();
+ }
+
+ public List getAssertionConsumerServicesLocations(SAMLBinding binding) {
+
+ return assertionConsumerServices.stream()
+ .filter((e) -> { return e.getBinding() == binding;})
+ .map((e) -> { return e.getLocation();})
+ .toList();
+ }
+
public void addAssertionConsumerService(final IndexedEndpoint service) {
this.assertionConsumerServices.add(service);
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/ds/X509Data.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/ds/X509Data.java
index 8501ab9fbea..5bd76754ab7 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/model/ds/X509Data.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/model/ds/X509Data.java
@@ -17,6 +17,14 @@ public List getX509Certificates() {
return this.x509certificates;
}
+ public String getFirstX509Certificate() {
+
+ if(!x509certificates.isEmpty()) {
+ return x509certificates.get(0);
+ }
+ return null;
+ }
+
public void addX509Certificate(final String x509certificate) {
this.x509certificates.add(x509certificate);
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/parser/SAMLMetadataParser.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/parser/SAMLMetadataParser.java
index d238dd2ebe3..00313173553 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/parser/SAMLMetadataParser.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/parser/SAMLMetadataParser.java
@@ -62,7 +62,7 @@ public SAMLMetadata parse(File metadatafile) {
validator.validate(new StreamSource(metadatafile));
final DocumentBuilderFactory docbuilderfactory = DocumentBuilderFactory.newInstance();
- docbuilderfactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
+ docbuilderfactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",false);
docbuilderfactory.setSchema(schema);
docbuilderfactory.setNamespaceAware(true);
final DocumentBuilder docbuilder = docbuilderfactory.newDocumentBuilder();
@@ -126,7 +126,7 @@ private final void parseSPSSODescriptor(final XPath xpath,
NodeList assertionconsumerservicelist = XPathUtils.assertionConsumerServiceListFromParentNode(xpath, node);
for(int i=0; i < assertionconsumerservicelist.getLength(); i++) {
- parseIndexedEndpoint(xpath,assertionconsumerservicelist.item(i),builder.assersionConsumerService());
+ parseIndexedEndpoint(xpath,assertionconsumerservicelist.item(i),builder.assertionConsumerService());
}
}
@@ -242,8 +242,8 @@ private final void parseX509Data(final XPath xpath, final Node node,final X509Da
}
private final void parseEndpoint(final XPath xpath, final Node node, final EndpointBuilder builder) throws XPathExpressionException {
-
- builder.binding(XPathUtils.bindingAttributeValue(xpath, node))
+
+ builder.binding(SAMLBinding.fromString(XPathUtils.bindingAttributeValue(xpath, node)))
.location(XPathUtils.locationAttributeValue(xpath, node))
.responseLocation(XPathUtils.responseLocationAttributeValue(xpath, node));
}
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/util/LSResourceResolverImpl.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/util/LSResourceResolverImpl.java
index b1cd8805fcc..f9351d101e4 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/util/LSResourceResolverImpl.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/util/LSResourceResolverImpl.java
@@ -17,14 +17,14 @@ public class LSResourceResolverImpl implements LSResourceResolver {
static {
namespacemap = new HashMap<>();
- namespacemap.put("http://www.w3.org/XML/1998/namespace","/schema/www.w3.org/xml.xsd");
- namespacemap.put("urn:oasis:names:tc:SAML:2.0:assertion","/schema/saml/saml-schema-assertion-2.0.xsd");
- namespacemap.put("http://www.w3.org/2000/09/xmldsig#","/schema/www.w3.org/xmldsig-core-schema.xsd");
- namespacemap.put("http://www.w3.org/2001/04/xmlenc#","/schema/www.w3.org/xenc-schema.xsd");
+ namespacemap.put("http://www.w3.org/XML/1998/namespace","/META-INF/xml.schemas/xml.xsd");
+ namespacemap.put("urn:oasis:names:tc:SAML:2.0:assertion","/META-INF/saml.schemas/saml-schema-assertion-2.0.xsd");
+ namespacemap.put("http://www.w3.org/2000/09/xmldsig#","/META-INF/xml.schemas/xmldsig-core-schema.xsd");
+ namespacemap.put("http://www.w3.org/2001/04/xmlenc#","/META-INF/xml.schemas/xenc-schema.xsd");
publicidmap = new HashMap<>();
- publicidmap.put("-//W3C//DTD XMLSchema 200102//EN","/schema/www.w3.org/XMLSchema.dtd");
- publicidmap.put("datatypes","/schema/www.w3.org/datatypes.dtd");
+ publicidmap.put("-//W3C//DTD XMLSchema 200102//EN","/META-INF/xml.schemas/XMLSchema.dtd");
+ publicidmap.put("datatypes","/META-INF/xml.schemas/datatypes.dtd");
}
@Override
@@ -33,7 +33,7 @@ public LSInput resolveResource(String type, String nameSpaceURI, String publicId
if(nameSpaceURI != null) {
String resourcepath = namespacemap.get(nameSpaceURI);
if(resourcepath!=null) {
-
+
return new LSInputImpl(getClass().getResourceAsStream(resourcepath));
}
}
diff --git a/jans-core/saml/src/main/java/io/jans/saml/metadata/util/SAXUtils.java b/jans-core/saml/src/main/java/io/jans/saml/metadata/util/SAXUtils.java
index d74a1a98ac2..93851fbd6d6 100644
--- a/jans-core/saml/src/main/java/io/jans/saml/metadata/util/SAXUtils.java
+++ b/jans-core/saml/src/main/java/io/jans/saml/metadata/util/SAXUtils.java
@@ -14,33 +14,46 @@
import org.xml.sax.SAXException;
+import io.jans.saml.metadata.parser.ParserCreateError;
+
public class SAXUtils {
private static final Source [] schemasources = new Source [] {
- new StreamSource(SAXUtils.class.getResourceAsStream("/schema/saml/saml-schema-metadata-2.0.xsd"))
+ new StreamSource(SAXUtils.class.getResourceAsStream("/META-INF/saml.schemas/saml-schema-metadata-2.0.xsd"))
};
+ private static SAXParserFactory parserfactory = null;
+
+ private SAXUtils() {
+
+ }
+
+ public static void init() throws ParserConfigurationException, SAXException {
+ if(parserfactory == null) {
+ parserfactory = SAXParserFactory.newInstance();
+ parserfactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false);
+ parserfactory.setSchema(newSchemaFactory().newSchema(schemasources)) ;
+ parserfactory.setNamespaceAware(true);
+ }
+ }
+
public static final SAXParser createParser() throws ParserConfigurationException, SAXException {
- SAXParserFactory factory = SAXParserFactory.newInstance();
- factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
- factory.setSchema(newSchemaFactory().newSchema(schemasources));
- factory.setNamespaceAware(true);
- return factory.newSAXParser();
+
+ if(parserfactory == null) {
+ throw new ParserCreateError("Please call SAXParser.init() first");
+ }
+ return parserfactory.newSAXParser();
}
private static final SchemaFactory newSchemaFactory() throws SAXException {
final SchemaFactory schemafactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
- schemafactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
+ schemafactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl",false);
schemafactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA,"");
schemafactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
schemafactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING,true);
schemafactory.setResourceResolver(new LSResourceResolverImpl());
return schemafactory;
}
-
- private SAXUtils() {
-
- }
}
\ No newline at end of file
diff --git a/jans-keycloak-integration/job-scheduler/pom.xml b/jans-keycloak-integration/job-scheduler/pom.xml
index a7bdad8a2ef..be442c88d59 100644
--- a/jans-keycloak-integration/job-scheduler/pom.xml
+++ b/jans-keycloak-integration/job-scheduler/pom.xml
@@ -73,6 +73,15 @@
compile
+
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta.annotation-api.version}
+ compile
+
+
@@ -81,6 +90,11 @@
compile
+
+
+ io.jans
+ jans-core-saml
+
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/KeycloakApi.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/KeycloakApi.java
index 3fcc8fdfb4a..eaf57fe44bd 100644
--- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/KeycloakApi.java
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/KeycloakApi.java
@@ -1,7 +1,16 @@
package io.jans.kc.api.admin.client;
import io.jans.kc.api.admin.client.KeycloakConfiguration;
+import io.jans.kc.api.admin.client.model.AuthenticationFlow;
import io.jans.kc.api.admin.client.model.ManagedSamlClient;
+import io.jans.kc.api.admin.client.model.ProtocolMapper;
+import io.jans.saml.metadata.model.EntityDescriptor;
+import io.jans.saml.metadata.model.IndexedEndpoint;
+import io.jans.saml.metadata.model.KeyDescriptor;
+import io.jans.saml.metadata.model.SAMLBinding;
+import io.jans.saml.metadata.model.SPSSODescriptor;
+import io.jans.saml.metadata.model.ds.KeyInfo;
+import io.jans.saml.metadata.model.ds.X509Data;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -9,18 +18,28 @@
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.core.Response;
+
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ArrayList;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
+import org.keycloak.admin.client.resource.AuthenticationManagementResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ProtocolMappersResource;
import org.keycloak.OAuth2Constants;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.client.HttpClients;
@@ -35,10 +54,16 @@
public class KeycloakApi {
+
+
private static final Integer DEFAULT_CONNPOOL_SIZE = 5;
private static final Integer DEFAULT_MAX_CONN_PER_ROUTE = 100;
private static final String SAML_PROTOCOL = "saml";
- private static final Pattern CLIENT_NAME_REGEX = Pattern.compile("^jans_saml_([a-zA-Z0-9\\-]+)$");
+ private static final Pattern MANAGED_SAML_CLIENT_NAME_REGEX = Pattern.compile("^managed_saml_client_([a-zA-Z0-9\\-]+)$");
+ private static final String MANAGED_SAML_CLIENT_NAME_FORMAT = "managed_saml_client_%s";
+ private static final String MANAGED_SAML_CLIENT_DESC_FORMAT = "#REF %s.\r\n!!! DO NOT ALTER THIS CLIENT MANUALLY!!!";
+ private static final String BROWSER_AUTHN_FLOW_KEY = "browser";
+
private static final Logger log = LoggerFactory.getLogger(KeycloakApi.class);
private Keycloak keycloak;
@@ -48,15 +73,34 @@ private KeycloakApi(Keycloak keycloak) {
this.keycloak = keycloak;
}
+ public AuthenticationFlow getAuthenticationFlowFromAlias(String realmname, String alias) {
+
+ try {
+
+ RealmResource realmresource = realmByName(realmname);
+ AuthenticationManagementResource authnmanagementresource = realmresource.flows();
+ List flows = authnmanagementresource.getFlows();
+ Optional authnflow = flows
+ .stream()
+ .filter((f)-> {
+ return f.getAlias()!=null && f.getAlias().equals(alias);
+ }).findFirst();
+
+ return (authnflow.isPresent()?new AuthenticationFlow(authnflow.get()):null);
+ }catch(Exception e) {
+ throw new KeycloakAdminClientApiError("Could not find authentication flow with alias " + alias,e);
+ }
+ }
+
public List findAllManagedSamlClients(String realmname) {
try {
+
RealmResource realmresource = realmByName(realmname);
- ClientsResource clientsresource = realmresource.clients();
- List clientsrep = clientsresource.findAll();
+ List clientsrep = realmresource.clients().findAll();
log.debug("Clients from realm count : {}",clientsrep.size());
return clientsrep.stream()
- .filter(KeycloakApi::isManagedClientRepresentation)
+ .filter(KeycloakApi::isManagedSamlClientRepresentation)
.map(KeycloakApi::toManagedSamlClient)
.collect(Collectors.toList());
}catch(Exception e) {
@@ -68,20 +112,219 @@ public void deleteManagedSamlClient(String realmname,ManagedSamlClient client) {
try {
RealmResource realmresource = realmByName(realmname);
- ClientsResource clientsresource = realmresource.clients();
- ClientResource clientresource = clientsresource.get(client.keycloakId());
+ ClientResource clientresource = realmresource.clients().get(client.keycloakId());
if(clientresource != null) {
clientresource.remove();
}
}catch(Exception e) {
- throw new KeycloakAdminClientApiError("Could not delete managed client",e);
+ throw new KeycloakAdminClientApiError("Could not delete managed saml client",e);
+ }
+ }
+
+
+ public ManagedSamlClient createManagedSamlClient(String realmname,String externalref,
+ AuthenticationFlow browserflow, EntityDescriptor entitydesc) {
+
+ try {
+ RealmResource realmresource = realmByName(realmname);
+ ClientsResource clientsresource = realmresource.clients();
+ ClientRepresentation clientrep = new ClientRepresentation();
+ ManagedSamlClient client = new ManagedSamlClient(clientrep,externalref);
+
+ //configure basic properties
+ configureBasicManagedClientProperties(managedSamlClientName(externalref),managedSamlClientDescription(externalref),
+ entitydesc.getEntityId(), client);
+ //configure saml redirect uris
+ configureSamlRedirectUris(entitydesc, client);
+ //configure signing and encryption
+ configureSamlEncryptionAndSigning(entitydesc,client);
+ //configure keycloak authentication
+ configureKeycloakAuthentication(browserflow,client);
+
+ Response response = clientsresource.create(clientrep);
+ int code = response.getStatus();
+ if(code != Response.Status.CREATED.getStatusCode()) {
+ String body = response.readEntity(String.class);
+ throw new KeycloakAdminClientApiError(String.format("Could not create managed saml client(http code %d). %s.",code,body));
+ }
+
+ String id = clientsresource.findByClientId(client.clientId()).get(0).getId();
+ client.setKeycloakId(id);
+ return client;
+ }catch(Exception e) {
+ throw new KeycloakAdminClientApiError("Could not create managed saml client",e);
+ }
+ }
+
+ public void updateManagedSamlClient(String realmname,ManagedSamlClient client, EntityDescriptor entitydesc) {
+
+ try {
+ RealmResource realmresource = realmByName(realmname);
+ ClientResource clientresource = realmresource.clients().get(client.keycloakId());
+ String externalref = client.externalRef();
+ String description = managedSamlClientDescription(client.externalRef());
+ configureBasicManagedClientProperties(null,description,entitydesc.getEntityId(),client);
+ configureSamlRedirectUris(entitydesc, client);
+ //configure signing and encryption
+ configureSamlEncryptionAndSigning(entitydesc, client);
+ clientresource.update(client.clientRepresentation());
+ }catch(Exception e) {
+ throw new KeycloakAdminClientApiError("Could not create update managed saml client",e);
+ }
+ }
+
+ public void addProtocolMappersToManagedSamlClient(String realmname, ManagedSamlClient client,List mappers) {
+
+ try {
+ RealmResource realmresource = realmByName(realmname);
+ ClientResource clientresource = realmresource.clients().get(client.keycloakId());
+ clientresource.getProtocolMappers().createMapper(mappers.stream().map((m)-> {
+ return m.representation();
+ }).toList());
+ }catch(Exception e) {
+ e.printStackTrace();
+ throw new KeycloakAdminClientApiError("Could not add protocol mapper to managed saml client",e);
+ }
+ }
+
+ public void updateManagedSamlClientProtocolMapper(String realmname, ManagedSamlClient client, ProtocolMapper mapper) {
+
+ try {
+ RealmResource realmresource =realmByName(realmname);
+ ClientResource clientresource = realmresource.clients().get(client.keycloakId());
+ clientresource.getProtocolMappers().update(mapper.getId(),mapper.representation());
+ }catch(Exception e) {
+ throw new KeycloakAdminClientApiError("Could not update protocol mapper for managed saml client",e);
+ }
+ }
+
+ public List getManagedSamlClientProtocolMappers(String realmname, ManagedSamlClient client) {
+
+ try {
+ RealmResource realmresource = realmByName(realmname);
+ ClientResource clientresource = realmresource.clients().get(client.keycloakId());
+ ProtocolMappersResource protocolmappers = clientresource.getProtocolMappers();
+ List mappers = protocolmappers.getMappersPerProtocol(ProtocolMapper.Protocol.SAML.value());
+ return mappers.stream().map((m)-> { return new ProtocolMapper(m);}).toList();
+ }catch(Exception e) {
+ throw new KeycloakAdminClientApiError("Could not get managed saml client protocol mappers",e);
+ }
+ }
+
+ public void deleteManagedSamlClientProtocolMapper(String realmname,ManagedSamlClient client, ProtocolMapper mapper) {
+
+ try {
+ RealmResource realmresource = realmByName(realmname);
+ ClientResource clientresource = realmresource.clients().get(client.keycloakId());
+ clientresource.getProtocolMappers().delete(mapper.getId());
+ }catch(Exception e) {
+ throw new KeycloakAdminClientApiError("Could not delete managed saml client protocol mapper",e);
+ }
+ }
+
+ private void configureBasicManagedClientProperties(String name, String description, String clientid, ManagedSamlClient client) {
+
+ if(name != null) {
+ client.setName(name);
+ }
+
+ if(description != null) {
+ client.setDescription(description);
+ }
+
+ if(clientid != null) {
+ client.setClientId(clientid);
+ }
+ }
+
+ private void configureSamlRedirectUris(EntityDescriptor entitydescriptor, ManagedSamlClient client) {
+
+ SPSSODescriptor spssodescriptor = entitydescriptor.getFirstSpssoDescriptor();
+ if(spssodescriptor != null) {
+ List acs_endpoints = spssodescriptor.getAssertionConsumerServices();
+ client.setSamlRedirectUris(
+ acs_endpoints
+ .stream()
+ .filter((e) -> { return (e.getBinding() == SAMLBinding.HTTP_REDIRECT || e.getBinding() == SAMLBinding.HTTP_POST);})
+ .map((e) -> { return e.getLocation();})
+ .toList());
+ boolean has_no_http_get_urls = acs_endpoints.stream().filter((e) -> { return e.getBinding() == SAMLBinding.HTTP_REDIRECT;}).count() == 0;
+ client.samlForcePostBinding((acs_endpoints.size()>0 && has_no_http_get_urls));
+ }
+ }
+
+ private void configureSamlEncryptionAndSigning(EntityDescriptor entitydescriptor, ManagedSamlClient client) {
+
+ SPSSODescriptor spssodescriptor = entitydescriptor.getFirstSpssoDescriptor();
+ if(spssodescriptor!=null) {
+ client.samlClientSignatureRequired(spssodescriptor.getAuthnRequestsSigned());
+ client.samlSignAssertions(spssodescriptor.getWantAssertionsSigned());
+
+ List signingkeys = spssodescriptor.getSigningKeys();
+ if(!signingkeys.isEmpty()) {
+ configureSamlSigningKey(signingkeys.get(0),client);
+ }
+
+ List encryptionkeys = spssodescriptor.getEncryptionKeys();
+ if(!encryptionkeys.isEmpty()) {
+ configureSamlEncryptionKey(encryptionkeys.get(0),client);
+ }
+ }
+ }
+
+ private void configureSamlSigningKey(KeyDescriptor keydescriptor, ManagedSamlClient client) {
+
+
+ KeyInfo keyinfo = keydescriptor.getKeyInfo();
+ List certdata = keyinfo.getDatalist();
+ if(!certdata.isEmpty()) {
+ client.samlClientSignatureRequired(true);
+ client.samlClientSigningCertificate(certdata.get(0).getFirstX509Certificate());
}
}
+ private void configureSamlEncryptionKey(KeyDescriptor keydescriptor, ManagedSamlClient client) {
+
+ KeyInfo keyinfo = keydescriptor.getKeyInfo();
+ List certdata = keyinfo.getDatalist();
+ if(!certdata.isEmpty()) {
+ client.samlEncryptAssertions(true);
+ client.samlClientEncryptionCertificate(certdata.get(0).getFirstX509Certificate());
+ }
+ }
+
+ private void configureKeycloakAuthentication(final AuthenticationFlow browserflow, ManagedSamlClient client) {
+
+ client.setBrowserFlow(browserflow.getId());
+ }
+
+ private Optional authnFlowFromAlias(RealmResource realm, String flowalias) {
+
+ AuthenticationManagementResource authn = realm.flows();
+ List flows = authn.getFlows();
+ return flows
+ .stream()
+ .filter((f)-> { return f.getAlias().equalsIgnoreCase(flowalias);})
+ .findFirst();
+ }
+
private RealmResource realmByName(String realmname) {
return keycloak.realm(realmname);
}
+
+ private Optional authnFlowByName(String realmname, String flowname) {
+
+ RealmResource realm = keycloak.realm(realmname);
+ if(realm == null) {
+ return null;
+ }
+ AuthenticationManagementResource authnmanagement = realm.flows();
+ List realmflows = authnmanagement.getFlows();
+ return realmflows.stream() .filter((f)-> {
+ return f.getAlias().equalsIgnoreCase(flowname);
+ }).findFirst();
+ }
public static final KeycloakApi createInstance(KeycloakConfiguration kcConfig) {
@@ -124,28 +367,40 @@ private static final Client createResteasyClient(KeycloakConfiguration kcConfig)
return ( (ResteasyClientBuilder) ClientBuilder.newBuilder()).httpEngine(engine).build();
}
- private static boolean isManagedClientRepresentation(ClientRepresentation client) {
+ private static boolean isManagedSamlClientRepresentation(ClientRepresentation client) {
- log.debug("Protocol: {} / Name: {}",client.getProtocol(),client.getName());
if(!SAML_PROTOCOL.equalsIgnoreCase(client.getProtocol())) {
- log.debug("Protocol does not match");
return false;
}
-
- Matcher matcher = CLIENT_NAME_REGEX.matcher(client.getName());
- boolean res = matcher.matches();
- log.debug("Matches: {}",res);
- return res;
+ Matcher matcher = MANAGED_SAML_CLIENT_NAME_REGEX.matcher(client.getName());
+ return matcher.matches();
}
private static ManagedSamlClient toManagedSamlClient(ClientRepresentation client) {
- Matcher matcher = CLIENT_NAME_REGEX.matcher(client.getName());
+ Matcher matcher = MANAGED_SAML_CLIENT_NAME_REGEX.matcher(client.getName());
if(matcher.matches() == true) {
return new ManagedSamlClient(client,matcher.group(1));
}
return null;
}
-
+ private static String managedSamlClientName(final String externalclientref) {
+
+ return String.format(MANAGED_SAML_CLIENT_NAME_FORMAT,externalclientref);
+ }
+
+ private static String managedSamlClientDescription(final String externalclientref) {
+
+ return String.format(MANAGED_SAML_CLIENT_DESC_FORMAT,externalclientref);
+ }
+
+ private static Map authnFlowBindingOverrides(AuthenticationFlowRepresentation browserflow) {
+
+ return new HashMap() {{
+ put(BROWSER_AUTHN_FLOW_KEY,browserflow.getId());
+ }};
+ }
+
+
}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/AuthenticationFlow.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/AuthenticationFlow.java
new file mode 100644
index 00000000000..3189a818cc0
--- /dev/null
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/AuthenticationFlow.java
@@ -0,0 +1,28 @@
+package io.jans.kc.api.admin.client.model;
+
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+
+public class AuthenticationFlow {
+
+ private AuthenticationFlowRepresentation flowRepresentation;
+
+ public AuthenticationFlow(AuthenticationFlowRepresentation flowRepresentation) {
+
+ this.flowRepresentation = flowRepresentation;
+ }
+
+ public String getId() {
+
+ return flowRepresentation.getId();
+ }
+
+ public String getAlias() {
+
+ return flowRepresentation.getAlias();
+ }
+
+ public String getDescription() {
+
+ return flowRepresentation.getDescription();
+ }
+}
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 f1836859155..e1ff0b209b6 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
@@ -1,25 +1,351 @@
package io.jans.kc.api.admin.client.model;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
import org.keycloak.representations.idm.ClientRepresentation;
public class ManagedSamlClient {
- private String trustRelationshipInum;
+ public enum AuthenticatorType {
+ CLIENT_SECRET("client-secret"),
+ CLIENT_JWT("client-jwt");
+
+ private final String value;
+
+ private AuthenticatorType(final String value) {
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+ }
+
+ public enum Protocol {
+ SAML("saml"),
+ OPENID("openid");
+
+ private final String value;
+ private Protocol(final String value) {
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+ }
+
+ public enum AuthnFlowOverrideType {
+ BROWSER("browser"),
+ DIRECT_GRANT("direct-grant");
+
+ private final String value;
+
+ private AuthnFlowOverrideType(final String value){
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+ }
+
+ private String externalRef;
private ClientRepresentation clientRepresentation;
+ private final Map authnFlowBindingOverrides = new HashMap();
+ private final Map attributes = new HashMap();
+
+ //saml client attributes
+
+ // sign assertions (should assertions inside saml documents be signed? not needed if the document is already signed)
+ //default: "false"
+ private static final String ATTR_SAML_ASSERTION_SIGNATURE = "saml.assertion.signature";
+ //saml client encryption private key (seems not useful in our context)
+ //default: ""
+ private static final String ATTR_SAML_ENCRYPTION_PRIVATE_KEY = "saml.encryption.private.key";
+ //always use POST binding for responses
+ //default: "false"
+ private static final String ATTR_SAML_FORCE_POST_BINDING = "saml.force.post.binding";
+ //encrypt assertions (saml assertions will be encrypted with the client's key)
+ //default: "false"
+ private static final String ATTR_SAML_ENCRYPT = "saml.encrypt";
+ //post logout redirect uris
+ //a list of uris delimited by ##
+ //default: ""
+ private static final String ATTR_POST_LOGOUT_REDIRECT_URIS = "post.logout.redirect.uris";
+ // sign documents (should documents be signed by the realm?)
+ //default: "false"
+ private static final String ATTR_SAML_SERVER_SIGNATURE = "saml.server.signature";
+
+ //unsure what this does
+ //default: "false"
+ private static final String ATTR_SAML_SERVER_SIGNATURE_KEYINFO_EXT = "saml.server.signature.keyinfo.ext";
+ //saml client signing certificate
+ //default: ""
+ private static final String ATTR_SAML_SIGNING_CERTIFICATE = "saml.signing.certificate";
+ //unsure what this does
+ //default: unknown
+ private static final String ATTR_SAML_ARTIFACT_BINDING_ID = "saml.artifact.binding.identifier";
+ //force artifact binding
+ //default: "false"
+ private static final String ATTR_SAML_ARTIFACT_BINDING = "saml.artifact.binding";
+ //idp side signature algorithm
+ //default: "RSA_SHA256"
+ private static final String ATTR_SAML_SIGNATURE_ALGORITHM = "saml.signature.algorithm";
+ //ignore requested nameid format and use the one configured in keycloak (ui)
+ //default: "false"
+ private static final String ATTR_SAML_FORCE_NAME_ID_FORMAT = "saml.force_name_id_format";
+ //client signature required (if clients signs their saml requests and responses and should be validated)
+ //default: "false"
+ private static final String ATTR_SAML_CLIENT_SIGNATURE = "saml.client.signature";
+ //saml client public key used for encryption
+ //default: ""
+ private static final String ATTR_SAML_ENCRYPTION_CERTIFICATE = "saml.encryption.certificate";
+ //include authnstatement
+ //default: "true"
+ private static final String ATTR_SAML_AUTHNSTATEMENT = "saml.authnstatement";
+ //nameid formats (supported values are username, email,persistent,transient,unspecified)
+ //default: "unspecified"
+ private static final String ATTR_SAML_NAMEID_FORMAT = "saml_name_id_format";
+ //saml client private key used for signing (not sure it's used somewhere)
+ //default: ""
+ private static final String ATTR_SAML_SIGNING_PRIVATE_KEY = "saml.signing.private.key";
+ //allow ecp flow
+ //default: "false"
+ private static final String ATTR_SAML_ALLOW_ECP_FLOW = "saml.allow.ecp.flow";
+ //canonicalization method for xml signatures
+ //default: "http://www.w3.org/2001/10/xml-exc-c14n#"
+ private static final String ATTR_SAML_SIGNATURE_C14N_METHOD = "saml_signature_canonicalization_method";
+ //should a OneTimeUse condition be included in the login response ?
+ //default: "false"
+ private static final String ATTR_SAML_ONETIMEUSE_CONDITION = "saml.onetimeuse.condition";
- public ManagedSamlClient(ClientRepresentation clientRepresentation, String trustRelationshipInum) {
+ public ManagedSamlClient(String externalRef) {
+
+ this.externalRef = externalRef;
+ clientRepresentation = new ClientRepresentation();
+ initClientRepresentation();
+ }
+
+ public ManagedSamlClient(ClientRepresentation clientRepresentation, String externalRef) {
this.clientRepresentation = clientRepresentation;
- this.trustRelationshipInum = trustRelationshipInum;
+ this.externalRef = externalRef;
+ initClientRepresentation();
+ }
+
+ private void initClientRepresentation() {
+
+ if(clientRepresentation != null) {
+ clientRepresentation.setEnabled(true);
+ clientRepresentation.setProtocol(Protocol.SAML.value());
+ clientRepresentation.setAlwaysDisplayInConsole(false);
+ clientRepresentation.setClientAuthenticatorType(AuthenticatorType.CLIENT_JWT.value());
+ clientRepresentation.setConsentRequired(false);
+ clientRepresentation.setAttributes(attributes);
+ clientRepresentation.setAuthenticationFlowBindingOverrides(authnFlowBindingOverrides);
+
+ //set default saml attributes
+ samlShoulDocumentsBeSigned(false);
+ samlSignAssertions(false);
+ samlForcePostBinding(false);
+ samlEncryptAssertions(false);
+ samlForceArtifactBinding(false);
+ samlSignatureAlgorithm(SamlSignatureAlgorithm.RSA_SHA256);
+ samlForceNameIdFormat(false);
+ samlClientSignatureRequired(false);
+ samlIncludeAuthnStatement(true);
+ samlNameIDFormat(SamlNameIDFormat.USERNAME);
+ samlAllowEcpFLow(false);
+ samlXmlSignatureCanonicalizationMethod("http://www.w3.org/2001/10/xml-exc-c14n#");
+ samlIncludeOneTimeUseCondition(false);
+ }
}
- public String trustRelationshipInum() {
+ public String externalRef() {
- return this.trustRelationshipInum;
+ return this.externalRef;
+ }
+
+ public ClientRepresentation clientRepresentation() {
+
+ return this.clientRepresentation;
+ }
+
+ public boolean correspondsToExternalRef(String externalRef) {
+
+ return this.externalRef.equals(externalRef);
}
public String keycloakId() {
return this.clientRepresentation.getId();
}
+
+ public ManagedSamlClient setKeycloakId(String keycloakid) {
+
+ this.clientRepresentation.setId(keycloakid);
+ return this;
+ }
+
+ public ManagedSamlClient setName(final String name) {
+
+ this.clientRepresentation.setName(name);
+ return this;
+ }
+
+ public ManagedSamlClient setDescription(final String description) {
+
+ this.clientRepresentation.setDescription(description);
+ return this;
+ }
+
+ public ManagedSamlClient setClientId(final String clientid) {
+
+ this.clientRepresentation.setClientId(clientid);
+ return this;
+ }
+
+ public String clientId() {
+
+ return this.clientRepresentation.getClientId();
+ }
+
+ public ManagedSamlClient setEnabled(final Boolean enabled) {
+
+ this.clientRepresentation.setEnabled(enabled);
+ return this;
+ }
+
+ public ManagedSamlClient setBrowserFlow(final String browserflowid) {
+
+ this.authnFlowBindingOverrides.put(AuthnFlowOverrideType.BROWSER.value(),browserflowid);
+ return this;
+ }
+
+ public ManagedSamlClient setDirectGrantFlow(final String directgrantflowid) {
+
+ this.authnFlowBindingOverrides.put(AuthnFlowOverrideType.DIRECT_GRANT.value(),directgrantflowid);
+ return this;
+ }
+
+ public ManagedSamlClient setSamlRedirectUris(List uris) {
+
+ clientRepresentation.setRedirectUris(uris);
+ return this;
+ }
+
+ public ManagedSamlClient samlSignAssertions(final Boolean sign) {
+
+ attributes.put(ATTR_SAML_ASSERTION_SIGNATURE,sign.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlEncryptionPrivateKey(final String privatekey) {
+
+ attributes.put(ATTR_SAML_ENCRYPTION_PRIVATE_KEY,privatekey);
+ return this;
+ }
+
+ public ManagedSamlClient samlForcePostBinding(final Boolean forcepostbinding) {
+
+ attributes.put(ATTR_SAML_FORCE_POST_BINDING,forcepostbinding.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlEncryptAssertions(final Boolean encryptassertions) {
+
+ attributes.put(ATTR_SAML_ENCRYPT,encryptassertions.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlPostLogoutRedirectUrls(final List urls) {
+
+ attributes.put(ATTR_POST_LOGOUT_REDIRECT_URIS,String.join("##",urls));
+ return this;
+ }
+
+ public ManagedSamlClient samlShoulDocumentsBeSigned(final Boolean signed) {
+
+ attributes.put(ATTR_SAML_SERVER_SIGNATURE,signed.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlClientSigningCertificate(final String certificate) {
+
+ attributes.put(ATTR_SAML_SIGNING_CERTIFICATE,certificate);
+ return this;
+ }
+
+ public ManagedSamlClient samlForceArtifactBinding(final Boolean force) {
+
+ attributes.put(ATTR_SAML_ARTIFACT_BINDING,force.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlSignatureAlgorithm(final SamlSignatureAlgorithm algorithm) {
+
+ attributes.put(ATTR_SAML_SIGNATURE_ALGORITHM,algorithm.value());
+ return this;
+ }
+
+ public ManagedSamlClient samlForceNameIdFormat(final Boolean force) {
+
+ attributes.put(ATTR_SAML_FORCE_NAME_ID_FORMAT,force.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlClientSignatureRequired(final Boolean required) {
+
+ attributes.put(ATTR_SAML_CLIENT_SIGNATURE,required.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlClientEncryptionCertificate(final String certificate) {
+
+ attributes.put(ATTR_SAML_ENCRYPTION_CERTIFICATE,certificate);
+ return this;
+ }
+
+ public ManagedSamlClient samlIncludeAuthnStatement(final Boolean include) {
+
+ attributes.put(ATTR_SAML_AUTHNSTATEMENT,include.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlNameIDFormat(final SamlNameIDFormat format) {
+
+ attributes.put(ATTR_SAML_NAMEID_FORMAT,format.value());
+ return this;
+ }
+
+ public ManagedSamlClient samlClientPrivateKey(final String privatekey) {
+
+ attributes.put(ATTR_SAML_SIGNING_PRIVATE_KEY,privatekey);
+ return this;
+ }
+
+ public ManagedSamlClient samlAllowEcpFLow(final Boolean allow) {
+
+ attributes.put(ATTR_SAML_ALLOW_ECP_FLOW,allow.toString());
+ return this;
+ }
+
+ public ManagedSamlClient samlXmlSignatureCanonicalizationMethod(final String method) {
+
+ attributes.put(ATTR_SAML_SIGNATURE_C14N_METHOD,method);
+ return this;
+ }
+
+ public ManagedSamlClient samlIncludeOneTimeUseCondition(final Boolean include) {
+
+ attributes.put(ATTR_SAML_ONETIMEUSE_CONDITION,include.toString());
+ return this;
+ }
}
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
new file mode 100644
index 00000000000..e42c9e4c16b
--- /dev/null
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/ProtocolMapper.java
@@ -0,0 +1,133 @@
+package io.jans.kc.api.admin.client.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+
+public class ProtocolMapper {
+
+ public enum Protocol {
+ OPENID("openid"),
+ SAML("saml");
+ private final String value;
+
+ private Protocol(final String value) {
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+ }
+ private final ProtocolMapperRepresentation representation;
+
+ public ProtocolMapper() {
+
+ this.representation = new ProtocolMapperRepresentation();
+ }
+
+ public ProtocolMapper(ProtocolMapperRepresentation representation) {
+
+ this.representation = representation;
+ }
+
+ public String getId() {
+
+ return representation.getId();
+ }
+
+ public ProtocolMapperRepresentation representation() {
+
+ return representation;
+ }
+
+ public String getName() {
+
+ return representation.getName();
+ }
+
+ public static SamlUserAttributeMapperBuilder samlUserAttributeMapper(final String id) {
+
+ return new SamlUserAttributeMapperBuilder(id);
+ }
+
+ public static SamlUserAttributeMapperBuilder samlUserAttributeMapper(final ProtocolMapper mapper) {
+
+ return new SamlUserAttributeMapperBuilder(mapper);
+ }
+
+ public static class SamlUserAttributeMapperBuilder {
+
+ private static final String NAMEFORMAT_OPT_BASIC = "Basic";
+ private static final String NAMEFORMAT_OPT_URI_REFERENCE = "URI Reference";
+ private static final String NAMEFORMAT_OPT_UNSPECIFIED = "Unspecified";
+
+ private final ProtocolMapper mapper;
+ private final Map config;
+
+ public SamlUserAttributeMapperBuilder(final String mapperid) {
+
+ ProtocolMapperRepresentation pmr = new ProtocolMapperRepresentation();
+ pmr.setProtocol(Protocol.SAML.value());
+ pmr.setProtocolMapper(mapperid);
+ this.mapper = new ProtocolMapper(pmr);
+ this.config = new HashMap<>();
+ pmr.setConfig(this.config);
+ }
+
+ private SamlUserAttributeMapperBuilder(final ProtocolMapper other) {
+
+ this.mapper = new ProtocolMapper(other.representation);
+ this.config = this.mapper.representation().getConfig();
+ }
+
+ public SamlUserAttributeMapperBuilder name(final String name) {
+
+ mapper.representation.setName(name);
+ return this;
+ }
+
+ public SamlUserAttributeMapperBuilder userAttribute(final String userattribute) {
+
+ config.put("user.attribute",userattribute);
+ return this;
+ }
+
+ public SamlUserAttributeMapperBuilder friendlyName(final String friendlyname) {
+
+ config.put("friendly.name",friendlyname);
+ return this;
+ }
+
+ public SamlUserAttributeMapperBuilder attributeName(final String attributename) {
+
+ config.put("attribute.name",attributename);
+ return this;
+ }
+
+ public SamlUserAttributeMapperBuilder attributeNameFormatBasic() {
+
+ config.put("attribute.nameformat",NAMEFORMAT_OPT_BASIC);
+ return this;
+ }
+
+ public SamlUserAttributeMapperBuilder attributeNameFormatUriReference() {
+
+ config.put("attribute.nameformat",NAMEFORMAT_OPT_URI_REFERENCE);
+ return this;
+ }
+
+ public SamlUserAttributeMapperBuilder attributeNameFormatUnspecified() {
+
+ config.put("attribute.nameformat",NAMEFORMAT_OPT_UNSPECIFIED);
+ return this;
+ }
+
+ public ProtocolMapper build() {
+
+ return this.mapper;
+ }
+ }
+}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/SamlNameIDFormat.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/SamlNameIDFormat.java
new file mode 100644
index 00000000000..d9da282b19a
--- /dev/null
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/SamlNameIDFormat.java
@@ -0,0 +1,22 @@
+package io.jans.kc.api.admin.client.model;
+
+
+public enum SamlNameIDFormat {
+ USERNAME("username"),
+ EMAIL("email"),
+ PERSISTENT("persistent"),
+ TRANSIENT("transient"),
+ UNSPECIFIED("unspecified");
+
+ private final String value;
+
+ private SamlNameIDFormat(final String value) {
+
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/SamlSignatureAlgorithm.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/SamlSignatureAlgorithm.java
new file mode 100644
index 00000000000..3a6b0211809
--- /dev/null
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/admin/client/model/SamlSignatureAlgorithm.java
@@ -0,0 +1,32 @@
+package io.jans.kc.api.admin.client.model;
+
+
+public enum SamlSignatureAlgorithm {
+ RSA_SHA1("RSA_SHA1"),
+ RSA_SHA256("RSA_SHA256"),
+ RSA_SHA256_MGF1("RSA_SHA256_MGF1"),
+ RSA_SHA512("RSA_SHA512"),
+ RSA_SHA512_MGF1("RSA_SHA512_MGF1"),
+ DSA_SHA1("DSA_SHA1");
+
+ private final String value;
+
+ private SamlSignatureAlgorithm(final String value) {
+ this.value = value;
+ }
+
+ public String value() {
+
+ return this.value;
+ }
+
+ public static SamlSignatureAlgorithm fromString(final String value) {
+
+ for(SamlSignatureAlgorithm alg : SamlSignatureAlgorithm.values()) {
+ if(alg.equals(value)) {
+ return alg;
+ }
+ }
+ return null;
+ }
+}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/JansConfigApi.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/JansConfigApi.java
index edd78798f20..a84eaa6fc51 100644
--- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/JansConfigApi.java
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/JansConfigApi.java
@@ -1,17 +1,38 @@
package io.jans.kc.api.config.client;
import io.jans.config.api.client.ApiClient;
+import io.jans.config.api.client.AttributeApi;
import io.jans.config.api.client.SamlTrustRelationshipApi;
import io.jans.config.api.client.ApiException;
+import io.jans.config.api.client.model.JansAttribute;
import io.jans.config.api.client.model.TrustRelationship;
+import io.jans.kc.api.config.client.model.JansAttributeRepresentation;
import io.jans.kc.api.config.client.model.JansTrustRelationship;
+import io.jans.saml.metadata.builder.SAMLMetadataBuilder;
+import io.jans.saml.metadata.builder.SPSSODescriptorBuilder;
+import io.jans.saml.metadata.parser.ParseError;
+import io.jans.saml.metadata.model.SAMLBinding;
+import io.jans.saml.metadata.model.SAMLMetadata;
+import io.jans.saml.metadata.parser.SAMLMetadataParser;
+
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
public class JansConfigApi {
+ private static final int HTTP_CODE_404 = 404;
+ private static final Logger log = LoggerFactory.getLogger(JansConfigApi.class);
private SamlTrustRelationshipApi trApi;
+ private AttributeApi attributeApi;
private JansConfigApi() {
@@ -23,6 +44,9 @@ public boolean trustRelationshipExists(String inum) {
TrustRelationship tr = trApi.getTrustRelationshipById(inum);
return (tr != null);
}catch(ApiException e) {
+ if(e.getCode() == HTTP_CODE_404) {
+ return false;
+ }
throw new JansConfigApiError("trustRelationshipExists() failed",e);
}
}
@@ -39,24 +63,119 @@ public List findAllTrustRelationships() {
}
}
- public static JansConfigApi createInstance(ApiCredentials credentials) {
+ public SAMLMetadata getTrustRelationshipSamlMetadata(JansTrustRelationship trustrelationship) {
+
+ if(trustrelationship.metadataIsFile()) {
+ return getTrustRelationshipFileMetadata(trustrelationship);
+ }else if(trustrelationship.metadataIsManual()) {
+ return getTrustRelationshipManualMetadata(trustrelationship);
+ }
+ throw new JansConfigApiError("Unsupported Saml metadata type specified");
+ }
+
+ public List getTrustRelationshipReleasedAttributes(JansTrustRelationship trustrelationship) {
+
+ List inums = trustrelationship.getReleasedAttributesInums();
+ List ret = new ArrayList();
+ try {
+ for(String inum : inums) {
+ JansAttribute attr = attributeApi.getAttributesByInum(inum);
+ if(attr != null) {
+ ret.add(new JansAttributeRepresentation(attr));
+ }
+ }
+ }catch(ApiException e) {
+ throw new JansConfigApiError("Unable to get trust relationship attributes",e);
+ }
+ return ret;
+ }
+
+ private SAMLMetadata getTrustRelationshipFileMetadata(JansTrustRelationship trustrelationship) {
+
+ try {
+ File samlmdfile = trApi.getTrustRelationshipFileMetadata(trustrelationship.getInum());
+ samlmdfile.deleteOnExit();
+ SAMLMetadataParser parser = new SAMLMetadataParser();
+ SAMLMetadata ret = parser.parse(samlmdfile);
+ return ret;
+ }catch(ApiException e) {
+ throw new JansConfigApiError("getTrustRelationshipSamlMetadata() failed",e);
+ }catch(ParseError e) {
+ throw new JansConfigApiError("SAML metadata parsing failed",e);
+ }
+ }
+
+ private SAMLMetadata getTrustRelationshipManualMetadata(JansTrustRelationship trustrelationship) {
+
+ io.jans.config.api.client.model.SAMLMetadata samlmd = trustrelationship.getManualSamlMetadata();
+ if(samlmd == null) {
+ throw new JansConfigApiError("Trustrelationship contains no manual metadata");
+ }
+
+ SAMLMetadataBuilder builder = new SAMLMetadataBuilder();
+
+ SPSSODescriptorBuilder spssobuilder = builder.entityDescriptor()
+ .entityId(samlmd.getEntityId())
+ .spssoDescriptor();
+
+ spssobuilder.authnRequestsSigned(false)
+ .wantAssertionsSigned(false);
+
+ if(samlmd.getSingleLogoutServiceUrl() != null) {
+ spssobuilder.singleLogoutService()
+ .binding(SAMLBinding.HTTP_REDIRECT)
+ .location(samlmd.getSingleLogoutServiceUrl());
+ }
+
+ if(samlmd.getNameIDPolicyFormat() != null) {
+ spssobuilder.nameIDFormats(Arrays.asList(samlmd.getNameIDPolicyFormat()));
+ }
+
+ if(samlmd.getJansAssertionConsumerServiceGetURL() != null) {
+ spssobuilder.assertionConsumerService()
+ .index(0)
+ .isDefault(true)
+ .binding(SAMLBinding.HTTP_REDIRECT)
+ .location(samlmd.getJansAssertionConsumerServiceGetURL());
+ }
+
+ if(samlmd.getJansAssertionConsumerServicePostURL() != null) {
+ spssobuilder.assertionConsumerService()
+ .index(1)
+ .isDefault(samlmd.getJansAssertionConsumerServiceGetURL()==null)
+ .binding(SAMLBinding.HTTP_POST)
+ .location(samlmd.getJansAssertionConsumerServicePostURL());
+ }
+ return builder.build();
+ }
+
+ public static JansConfigApi createInstance(String endpoint,ApiCredentials credentials) {
JansConfigApi client = new JansConfigApi();
- client.trApi = newSamlTrustRelationshipApi(credentials);
+ client.trApi = newSamlTrustRelationshipApi(endpoint,credentials);
+ client.attributeApi = newAttributeApi(endpoint,credentials);
return client;
}
- private static SamlTrustRelationshipApi newSamlTrustRelationshipApi(ApiCredentials credentials) {
+ private static SamlTrustRelationshipApi newSamlTrustRelationshipApi(String endpoint,ApiCredentials credentials) {
SamlTrustRelationshipApi ret = new SamlTrustRelationshipApi();
- ret.setApiClient(createApiClient(credentials));
+ ret.setApiClient(createApiClient(endpoint,credentials));
+ return ret;
+ }
+
+ private static AttributeApi newAttributeApi(String endpoint, ApiCredentials credentials) {
+
+ AttributeApi ret = new AttributeApi();
+ ret.setApiClient(createApiClient(endpoint,credentials));
return ret;
}
- private static ApiClient createApiClient(ApiCredentials credentials) {
+ private static ApiClient createApiClient(String endpoint, ApiCredentials credentials) {
ApiClient apiclient = new ApiClient();
apiclient.setAccessToken(credentials.bearerToken());
+ apiclient.setBasePath(endpoint);
return apiclient;
}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansAttributeRepresentation.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansAttributeRepresentation.java
new file mode 100644
index 00000000000..3cd96f98cf9
--- /dev/null
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansAttributeRepresentation.java
@@ -0,0 +1,50 @@
+package io.jans.kc.api.config.client.model;
+
+import io.jans.config.api.client.model.JansAttribute;
+
+
+public class JansAttributeRepresentation {
+
+ private JansAttribute jansAttribute;
+
+ public JansAttributeRepresentation(JansAttribute jansAttribute) {
+
+ this.jansAttribute = jansAttribute;
+ }
+
+ public String getInum() {
+
+ return jansAttribute.getInum();
+ }
+
+ public String getSourceAttribute() {
+
+ return jansAttribute.getSourceAttribute();
+ }
+
+ public String getNameIdType() {
+
+ return jansAttribute.getNameIdType();
+ }
+
+ public String getName() {
+
+ return jansAttribute.getName();
+ }
+
+ public String getDisplayName() {
+
+ return jansAttribute.getDisplayName();
+ }
+
+ public String getDescription() {
+
+ return jansAttribute.getDescription();
+ }
+
+ public String getSaml2Uri() {
+
+ return jansAttribute.getSaml2Uri();
+ }
+
+}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansTrustRelationship.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansTrustRelationship.java
index 879cf92d73f..d2de9d46d47 100644
--- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansTrustRelationship.java
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/api/config/client/model/JansTrustRelationship.java
@@ -1,9 +1,16 @@
package io.jans.kc.api.config.client.model;
import io.jans.config.api.client.model.TrustRelationship;
+import io.jans.config.api.client.model.SAMLMetadata;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
public class JansTrustRelationship {
-
+
+ private static final Pattern ATTRIBUTE_DN_PATTERN = Pattern.compile("^inum=([A-F0-9]+),ou=attributes,o=jans$");
private TrustRelationship tr;
public JansTrustRelationship(TrustRelationship tr) {
@@ -13,6 +20,37 @@ public JansTrustRelationship(TrustRelationship tr) {
public String getInum() {
- return this.tr.getInum();
+ return tr.getInum();
+ }
+
+ public boolean metadataIsFile() {
+
+ return tr.getSpMetaDataSourceType() == TrustRelationship.SpMetaDataSourceTypeEnum.FILE;
+ }
+
+ public boolean metadataIsManual() {
+
+ return tr.getSpMetaDataSourceType() == TrustRelationship.SpMetaDataSourceTypeEnum.MANUAL;
+ }
+
+ public SAMLMetadata getManualSamlMetadata() {
+
+ return tr.getSamlMetadata();
+ }
+
+ public List getReleasedAttributesInums() {
+
+ List ret = new ArrayList();
+ for(String attributedn : tr.getReleasedAttributes()) {
+ Matcher matcher = ATTRIBUTE_DN_PATTERN.matcher(attributedn);
+ if(matcher.matches()) {
+ try {
+ ret.add(matcher.group(1));
+ }catch(IllegalStateException | IndexOutOfBoundsException ignored) {
+
+ }
+ }
+ }
+ return ret;
}
}
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 fa73e702c55..38144371dba 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
@@ -10,6 +10,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.xml.sax.SAXException;
import io.jans.kc.scheduler.config.ConfigApiAuthnMethod;
import io.jans.kc.scheduler.config.AppConfiguration;
@@ -26,10 +27,15 @@
import io.jans.kc.api.config.client.impl.*;
import io.jans.kc.api.admin.client.*;
+import io.jans.kc.api.admin.client.model.AuthenticationFlow;
import io.jans.kc.api.admin.client.model.ManagedSamlClient;
+import io.jans.saml.metadata.parser.ParserCreateError;
+import io.jans.saml.metadata.util.SAXUtils;
import java.util.List;
+import javax.xml.parsers.ParserConfigurationException;
+
public class App {
private static final String APP_DISPLAY_NAME = "Keycloak";
@@ -46,7 +52,7 @@ public class App {
/*
* Entry point
*/
- public static void main(String[] args) throws InterruptedException {
+ public static void main(String[] args) throws InterruptedException, ParserCreateError, ParserConfigurationException, SAXException {
log.info("Application starting ...");
try {
@@ -59,6 +65,10 @@ public static void main(String[] args) throws InterruptedException {
jansConfigApiFactory = JansConfigApiFactory.createFactory(config);
keycloakApiFactory = KeycloakApiFactory.createFactory(config);
+ //initialize application objects
+ log.debug("Initialization additional application objects");
+ SAXUtils.init();
+
log.debug("Initializing scheduler ");
jobScheduler = createJobScheduler(config);
startJobScheduler(jobScheduler);
@@ -202,16 +212,18 @@ public static KeycloakApiFactory createFactory(AppConfiguration config) {
private static class JansConfigApiFactory {
+ private String endpoint;
private ApiCredentialsProvider credsprovider;
- private JansConfigApiFactory(ApiCredentialsProvider credsprovider) {
+ private JansConfigApiFactory(String endpoint, ApiCredentialsProvider credsprovider) {
+ this.endpoint = endpoint;
this.credsprovider = credsprovider;
}
public JansConfigApi newApiClient() {
- return JansConfigApi.createInstance(credsprovider.getApiCredentials());
+ return JansConfigApi.createInstance(endpoint,credsprovider.getApiCredentials());
}
public static JansConfigApiFactory createFactory(AppConfiguration config) {
@@ -232,7 +244,7 @@ public static JansConfigApiFactory createFactory(AppConfiguration config) {
throw new StartupError("Could not initialize jans-config API. Unsupported authn method");
}
ApiCredentialsProvider provider = OAuthApiCredentialsProvider.create(config.configApiAuthUrl(),authparams);
- return new JansConfigApiFactory(provider);
+ return new JansConfigApiFactory(config.configApiUrl(),provider);
}catch(CredentialsProviderError e) {
throw new StartupError("Could not initialize jans-config API",e);
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 6eb907a5a00..9e776df4fb9 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
@@ -2,13 +2,19 @@
import io.jans.kc.scheduler.job.ExecutionContext;
import io.jans.kc.scheduler.job.RecurringJob;
+import io.jans.saml.metadata.model.EntityDescriptor;
+import io.jans.saml.metadata.model.SAMLMetadata;
import io.jans.kc.api.admin.client.KeycloakApi;
import io.jans.kc.api.admin.client.model.ManagedSamlClient;
+import io.jans.kc.api.admin.client.model.ProtocolMapper;
+import io.jans.kc.api.admin.client.model.AuthenticationFlow;
import io.jans.kc.api.config.client.JansConfigApi;
+import io.jans.kc.api.config.client.model.JansAttributeRepresentation;
import io.jans.kc.api.config.client.model.JansTrustRelationship;
import io.jans.kc.scheduler.App;
import java.util.List;
+import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -21,28 +27,42 @@ public class TrustRelationshipSyncJob extends RecurringJob {
private static final Logger log = LoggerFactory.getLogger(TrustRelationshipSyncJob.class);
- private JansConfigApi jansConfigApi;
- private KeycloakApi keycloakApi;
- private String realm;
+ private final JansConfigApi jansConfigApi;
+ private final KeycloakApi keycloakApi;
+ private final String realm;
+ private AuthenticationFlow authnBrowserFlow;
+ private final String samlUserAttributeMapperId;
public TrustRelationshipSyncJob() {
this.jansConfigApi = App.jansConfigApi();
this.keycloakApi = App.keycloakApi();
- this.realm = App.configuration().keycloakAdminRealm();
+ 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;
+ }
}
@Override
public void run(ExecutionContext context) {
try {
+
log.debug("Performing Saml client housekeeping");
performSamlClientsHousekeeping();
log.debug("Saml client housekeeping complete");
- log.debug("Synchronizing new trustrelationships");
- syncNewTrustRelationships();
- log.debug("New trustrelationships sync complete");
+ log.debug("Creating new managed saml clients");
+ createNewManagedSamlClients();
+ log.debug("Creating new managed saml clients complete");
+
+ log.debug("Updating existing managed saml clients");
+ updateExistingManagedSamlClients();
+ log.debug("Updating existing managed saml clients complete");
}catch(Exception e) {
log.error("Error running tr sync job",e);
}
@@ -50,89 +70,187 @@ public void run(ExecutionContext context) {
private void performSamlClientsHousekeeping() {
- log.debug("Saml housekeeping start for realm -- {}",realm);
- List samlclients = keycloakApi.findAllManagedSamlClients(realm);
- for(ManagedSamlClient samlclient : samlclients) {
- String jans_tr_inum = samlclient.trustRelationshipInum();
- log.debug("Housekeeping attempt for SAML client -- {}",samlclient.keycloakId());
- if(!jansConfigApi.trustRelationshipExists(jans_tr_inum)) {
- log.debug("Deleting SAML client -- {}",samlclient.keycloakId());
- keycloakApi.deleteManagedSamlClient(realm,samlclient);
- }
- }
- log.debug("Saml housekeeping exit for realm -- {}",realm);
+ deleteUnmanagedSamlClients();
}
- private void syncNewTrustRelationships() {
-
- List unmanagedtrs = unmanagedTrustRelationships();
- unmanagedtrs.forEach(new CreateSamlClientFromTrustRelationship());
-
- }
+ private void deleteUnmanagedSamlClients() {
- private void syncExistingTrustRelationships() {
+ log.debug("Deleting unmanaged SAML clients");
+ List managedsamlclients = keycloakApi.findAllManagedSamlClients(realm);
+ if(managedsamlclients.isEmpty()) {
+ log.debug("No previously managed SAML clients found in keycloak.");
+ return;
+ }else {
+ log.debug("Previously managed SAML clients found in keycloak. Count: {}",managedsamlclients.size());
+ }
+ managedsamlclients.forEach((c) -> {
+ if(!jansConfigApi.trustRelationshipExists(c.externalRef())) {
+ log.debug("Deleting previously managed SAML client with id: {}",c.keycloakId());
+ keycloakApi.deleteManagedSamlClient(realm,c);
+ }
+ });
}
- private List unmanagedTrustRelationships() {
+ private void createNewManagedSamlClients() {
+
+ if(this.authnBrowserFlow == null) {
+ log.warn("Misconfigured browser authentication flow, skipping creation of new saml clients");
+ return;
+ }
+ List unassociatedtrs = unassociatedJansTrustRelationships();
+ if(unassociatedtrs.isEmpty()) {
+ log.debug("No unmanaged trust relationships found in Janssen.");
+ return;
+ }else {
+ log.debug("Unmanaged trust relationships found in Janssen. Count: {}",unassociatedtrs.size());
+ }
+ unassociatedtrs.stream().forEach(this::createNewManagedSamlClient);
- List samlclients = keycloakApi.findAllManagedSamlClients(realm);
- return filteredTrustRelationships(new HasAnAssociatedSamlClient(samlclients).negate());
}
- private List managedTrustRelationships() {
-
- List samlclients = keycloakApi.findAllManagedSamlClients(realm);
- return filteredTrustRelationships(new HasAnAssociatedSamlClient(samlclients));
+ private void createNewManagedSamlClient(final JansTrustRelationship trustrelationship) {
+ try {
+ log.debug("Creating managed SAML client from Janssen TR with inum {}",trustrelationship.getInum());
+ SAMLMetadata metadata = jansConfigApi.getTrustRelationshipSamlMetadata(trustrelationship);
+ List entitydescriptors = metadata.getEntityDescriptors();
+ if(!entitydescriptors.isEmpty()) {
+ //use first entity descriptor
+ String trinum = trustrelationship.getInum();
+ ManagedSamlClient client = keycloakApi.createManagedSamlClient(realm,trinum,authnBrowserFlow,entitydescriptors.get(0));
+ //update managed saml client with released attributes
+ List attrs = jansConfigApi.getTrustRelationshipReleasedAttributes(trustrelationship);
+ addReleasedAttributesToManagedSamlClient(client, attrs);
+ log.debug("Created managed SAML client with id {} from Janssen TR with inum {}",client.keycloakId(),trinum);
+ }
+ }catch(Exception e) {
+ log.warn("Could not create managed SAML client using tr with inum {}",trustrelationship.getInum());
+ log.warn("Resulting exception: ",e);
+ }
}
- private List filteredTrustRelationships(final Predicate filter) {
+ private void updateExistingManagedSamlClients() {
List alltr = jansConfigApi.findAllTrustRelationships();
- return alltr
- .stream()
- .filter(filter)
- .collect(Collectors.toList());
+ List clients = keycloakApi.findAllManagedSamlClients(realm);
+ log.debug("Updating existing managed saml clients. Count: {}",clients.size());
+ clients.stream().forEach((c) -> {
+ Optional tr = alltr
+ .stream()
+ .filter((t)->{return c.correspondsToExternalRef(t.getInum());})
+ .findFirst();
+ if(tr.isPresent()) {
+ this.updateExistingSamlClient(c,tr.get());
+ }
+ });
}
- private class HasAnAssociatedSamlClient implements Predicate {
-
- private List samlclients;
-
- public HasAnAssociatedSamlClient(List samlclients) {
+ private void updateExistingSamlClient(ManagedSamlClient client, JansTrustRelationship trustrelationship) {
- this.samlclients = samlclients;
+ try {
+ log.debug("Updating managed SAML client with id {}. Associated trust relationship inum: {}",client.keycloakId(),client.externalRef());
+ SAMLMetadata metadata = jansConfigApi.getTrustRelationshipSamlMetadata(trustrelationship);
+ List entitydescriptors = metadata.getEntityDescriptors();
+ if(!entitydescriptors.isEmpty()) {
+
+ keycloakApi.updateManagedSamlClient(realm, client, entitydescriptors.get(0));
+ List releasedattributes = jansConfigApi.getTrustRelationshipReleasedAttributes(trustrelationship);
+ List mappers = keycloakApi.getManagedSamlClientProtocolMappers(realm, client);
+
+ //delete attributes to stop releasing
+ mappers.forEach((m)-> {
+ String inum = inumFromProtocolMapperName(m.getName());
+ if(!releasedattributes.stream().anyMatch((r)-> { return inum.equals(r.getInum());}))
+ {
+ log.debug("Removing attribute {} for managed saml client {} because it's no more part of the released attributes",
+ m.getName(),client.clientId());
+ deleteProtolMapperFromManagedClient(client,m);
+ }
+ });
+ //create new attributes to release
+ List newattributes = releasedattributes
+ .stream().filter((r)-> {
+ return !mappers.stream().anyMatch((m)-> {
+ String inum = inumFromProtocolMapperName(m.getName());
+ return inum.equals(r.getInum());
+ });
+ }).toList();
+ addReleasedAttributesToManagedSamlClient(client, newattributes);
+
+ //update existing attributes
+ mappers.forEach((m)-> {
+ String inum = inumFromProtocolMapperName(m.getName());
+ Optional attr = releasedattributes.stream().filter((r)->{
+ return inum.equals(r.getInum());
+ }).findFirst();
+ if(attr.isPresent()) {
+ updateManagedSamlClientProtocolMapper(client,m,attr.get());
+ }
+ });
+ }
+ }catch(Exception e) {
+ log.warn("Could not update managed SAML client with id {}",client.keycloakId());
+ log.warn("Resulting exception: ",e);
}
+ }
+ private List unassociatedJansTrustRelationships() {
- @Override
- public boolean test(JansTrustRelationship t) {
+ List alltr = jansConfigApi.findAllTrustRelationships();
+ List clients = keycloakApi.findAllManagedSamlClients(realm);
+ return alltr.stream().filter((t)-> {
+ return clients.stream().noneMatch((c) -> {return c.externalRef().equals(t.getInum());});
+ }).toList();
+ }
- for(ManagedSamlClient c : samlclients) {
- if(c.trustRelationshipInum().equalsIgnoreCase(t.getInum())) {
- return true;
- }
- }
- return false;
- }
+ private void addReleasedAttributesToManagedSamlClient(ManagedSamlClient client, List releasedattributes) {
+
+ 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();
+ }).toList();
+
+ keycloakApi.addProtocolMappersToManagedSamlClient(realm, client, protmappers);
+ }
+ 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();
+ keycloakApi.updateManagedSamlClientProtocolMapper(realm, client,newmapper);
}
- private class CreateSamlClientFromTrustRelationship implements Consumer {
+ private void deleteProtolMapperFromManagedClient(ManagedSamlClient client,ProtocolMapper mapper) {
- @Override
- public void accept(JansTrustRelationship tr) {
+ log.debug("Deleting released attribute from managed client. Client id: {} / Attribute name: {}",client.clientId(),mapper.getName());
+ keycloakApi.deleteManagedSamlClientProtocolMapper(realm,client,mapper);
+ }
- }
+ private final String generateKeycloakUniqueProtocolMapperName(JansAttributeRepresentation rep) {
-
+ return String.format("%s:%s",rep.getInum(),rep.getName());
}
- private class UpdateSamlClientUsingTrustRelationship implements Consumer {
-
- @Override
- public void accept(JansTrustRelationship tr) {
+ private final String inumFromProtocolMapperName(String name) {
+ int idx = name.indexOf(":");
+ if(idx!= -1) {
+ return name.substring(0,idx);
}
+ return "";
}
+
}
diff --git a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/config/AppConfiguration.java b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/config/AppConfiguration.java
index 5dc7599cce8..1b0198cc5e9 100644
--- a/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/config/AppConfiguration.java
+++ b/jans-keycloak-integration/job-scheduler/src/main/java/io/jans/kc/scheduler/config/AppConfiguration.java
@@ -37,6 +37,8 @@ public class AppConfiguration {
private static final String CFG_PROP_JOB_TRSYNC_SCHEDULE_INTERVAL = "app.job.trustrelationship-sync.schedule-interval";
private static final String CFG_PROP_KEYCLOAK_RESOURCES_REALM = "app.keycloak.resources.realm";
+ private static final String CFG_PROP_KEYCLOAK_RESOURCES_AUTHN_BROWSER_FLOW_ALIAS = "app.keycloak.resources.authn.browser.flow-alias";
+ private static final String CFG_PROP_KEYCLOAK_RESOURCES_SAML_USER_ATTRIBUTE_MAPPER = "app.keycloak.resources.saml.user-attribute-mapper";
private final Properties configProperties;
@@ -120,6 +122,16 @@ public String keycloakResourcesRealm() {
return getStringEntry(CFG_PROP_KEYCLOAK_RESOURCES_REALM);
}
+ public String keycloakResourcesBrowserFlowAlias() {
+
+ return getStringEntry(CFG_PROP_KEYCLOAK_RESOURCES_AUTHN_BROWSER_FLOW_ALIAS);
+ }
+
+ public String keycloakResourcesSamlUserAttributeMapper() {
+
+ return getStringEntry(CFG_PROP_KEYCLOAK_RESOURCES_SAML_USER_ATTRIBUTE_MAPPER);
+ }
+
public Duration trustRelationshipSyncScheduleInterval() {
try {
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 cd862028115..48286559a0a 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
@@ -2,6 +2,9 @@
# logging configuration
app.logging.level.root=DEBUG
+app.logging.level.apache.http.client=INFO
+app.logging.level.apache.http.wire=INFO
+app.logging.level.apache.http.header=INFO
app.logging.loghistory=180
@@ -19,7 +22,7 @@ app.config-api.auth.client.scopes=%{scopes}
app.config-api.auth.method=%{auth_method}
-# keycloak configuration
+# keycloak api configuration
app.keycloak-admin.url=%{keycloak_admin_url}
app.keycloak-admin.realm=%{keycloak_admin_realm}
app.keycloak-admin.username=%{keycloak_admin_username}
@@ -30,5 +33,7 @@ app.keycloak-admin.conn.poolsize=10
# trust relationship sync job configuration
app.job.trustrelationship-sync.schedule-interval=PT10M
-# keycloak configuration
+# 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
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 02bdd2ecf94..6a62328f644 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
@@ -35,6 +35,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jans-keycloak-integration/pom.xml b/jans-keycloak-integration/pom.xml
index 8b158b91a1e..a864c55a063 100644
--- a/jans-keycloak-integration/pom.xml
+++ b/jans-keycloak-integration/pom.xml
@@ -26,6 +26,7 @@
${project.version}
3.1.0
6.0.0
+ 3.0.0-M1
3.4.4
2.19.0
1.7.36
@@ -157,6 +158,12 @@
${jakarta.servlet-api.version}
provided
+
+ jarkarta.annotation
+ jakarta.annotation-api
+ ${jakarta.annotation-api.version}
+ provided
+
-
+
+
+ io.jans
+ jans-core-saml
+ ${project.version}
+
+
+