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} + + +