Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bedd806
New SAML 2.0 implementation for GAM
sgrampone Mar 13, 2025
3fc3367
Merge branch 'master' into gamsaml20
sgrampone Mar 13, 2025
a148a96
Change package name to com.genexus.saml20
sgrampone Mar 13, 2025
0ca4d74
Fix RedirectBinding refactor error
sgrampone Mar 13, 2025
eed14a3
Fix code quality issue
sgrampone Mar 13, 2025
24197d4
Fix Attribute class code quality
sgrampone Mar 13, 2025
762b0b9
Change parameter's names for GAM compatibility
sgrampone Mar 19, 2025
53dfed1
Fix signature verification on RedirectBinding, add security measures …
sgrampone Mar 20, 2025
968d0ef
Fix format issues and change signature verification on RedirectBinding
sgrampone Mar 20, 2025
b216d7e
Change certificate parameters
sgrampone Mar 21, 2025
970ae63
Fix typing mistake on artifact id
sgrampone Mar 21, 2025
c8ca345
Add isLogout function
sgrampone Apr 10, 2025
15b1e7d
Fix conflict with SecurityAPI modules in master pom
sgrampone Apr 11, 2025
5802b39
Fix conflict with ordering
sgrampone Apr 11, 2025
ea51d65
Merge branch 'master' into gamsaml20
sgrampone Apr 11, 2025
6ebd469
Fix issue finding attributes on Agesic message
sgrampone Apr 28, 2025
04e7829
Set policy format directly from GAM
sgrampone May 7, 2025
e9ffd32
Manage exception in IsLogout
sgrampone May 19, 2025
e8f461c
Simple fix to read assertion attributes
sgrampone May 22, 2025
56a1ef8
Saml20 EO tests
sgrampone May 29, 2025
c4dbabf
Merge branch 'master' into gamsaml20
sgrampone Jul 30, 2025
2e7a703
Merge branch 'master' into gamsaml20
sgrampone Aug 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions gamsaml20/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.genexus</groupId>
<artifactId>parent</artifactId>
<version>${revision}${changelist}</version>
</parent>

<artifactId>gamsaml20</artifactId>
<name>GAM Saml 2.0 EO</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>${log4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.santuario</groupId>
<artifactId>xmlsec</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
<finalName>gamsaml20</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

</build>

</project>
29 changes: 29 additions & 0 deletions gamsaml20/src/main/java/com/genexus/saml20/Binding.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.genexus.saml20;

import com.genexus.saml20.utils.SamlAssertionUtils;

@SuppressWarnings("unused")
public abstract class Binding {

abstract void init(String input);

static String login(SamlParms parms, String relayState) {
return "";
}

static String logout(SamlParms parms, String relayState) {
return "";
}

abstract boolean verifySignatures(SamlParms parms);

abstract String getLoginAssertions();

abstract String getLoginAttribute(String name);

abstract String getRoles(String name);

abstract String getLogoutAssertions();

abstract boolean isLogout();
}
73 changes: 73 additions & 0 deletions gamsaml20/src/main/java/com/genexus/saml20/PostBinding.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.genexus.saml20;

import com.genexus.saml20.utils.DSig;
import com.genexus.saml20.utils.Encoding;
import com.genexus.saml20.utils.SamlAssertionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Document;

import java.text.MessageFormat;

@SuppressWarnings("unused")
public class PostBinding extends Binding {

private static final Logger logger = LogManager.getLogger(PostBinding.class);

private Document xmlDoc;

public PostBinding() {
logger.trace("PostBinding constructor");
xmlDoc = null;
}
// EXTERNAL OBJECT PUBLIC METHODS - BEGIN


public void init(String xml) {
logger.trace("init");
this.xmlDoc = SamlAssertionUtils.canonicalizeXml(xml);
logger.debug(MessageFormat.format("Init - XML IdP response: {0}", Encoding.documentToString(xmlDoc)));
}

public static String login(SamlParms parms, String relayState) {
//not implemented yet
logger.error("login - NOT IMPLEMENTED");
return "";
}

public static String logout(SamlParms parms, String relayState) {
//not implemented yet
logger.error("logout - NOT IMPLEMENTED");
return "";
}

public boolean verifySignatures(SamlParms parms) {
return DSig.validateSignatures(this.xmlDoc, parms.getTrustCertPath(), parms.getTrustCertAlias(), parms.getTrustCertPass());
}

public String getLoginAssertions() {
logger.trace("getLoginAssertions");
return SamlAssertionUtils.getLoginInfo(this.xmlDoc);
}

public String getLogoutAssertions() {
logger.trace("getLogoutAssertions");
return SamlAssertionUtils.getLogoutInfo(this.xmlDoc);
}

public String getLoginAttribute(String name) {
logger.trace("getLoginAttribute");
return SamlAssertionUtils.getLoginAttribute(this.xmlDoc, name).trim();
}

public String getRoles(String name) {
logger.debug("getRoles");
return SamlAssertionUtils.getRoles(this.xmlDoc, name);
}

public boolean isLogout(){
return SamlAssertionUtils.isLogout(this.xmlDoc);
}

// EXTERNAL OBJECT PUBLIC METHODS - END
}
200 changes: 200 additions & 0 deletions gamsaml20/src/main/java/com/genexus/saml20/RedirectBinding.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package com.genexus.saml20;

import com.genexus.saml20.utils.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.crypto.Signer;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.signers.RSADigestSigner;
import org.bouncycastle.util.encoders.Base64;
import org.w3c.dom.Document;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;

@SuppressWarnings("unused")
public class RedirectBinding extends Binding {

private static final Logger logger = LogManager.getLogger(RedirectBinding.class);

private Document xmlDoc;
private Map<String, String> redirectMessage;

// EXTERNAL OBJECT PUBLIC METHODS - BEGIN


public RedirectBinding() {
logger.trace("RedirectBinding constructor");
}

public void init(String queryString) {
logger.trace("init");
logger.debug(MessageFormat.format("init - queryString : {0}", queryString));
this.redirectMessage = parseRedirect(queryString);
String xml = Encoding.decodeAndInflateXmlParameter(this.redirectMessage.get("SAMLResponse"));
logger.debug("init - inflated xml: {0}", xml);
this.xmlDoc = SamlAssertionUtils.canonicalizeXml(xml);
logger.debug(MessageFormat.format("init - XML IdP response: {0}", Encoding.documentToString(xmlDoc)));
}


public static String login(SamlParms parms, String relayState) {
Document request = SamlAssertionUtils.createLoginRequest(parms.getId(), parms.getEndPointLocation(), parms.getAcs(), parms.getIdentityProviderEntityID(), parms.getPolicyFormat(), parms.getAuthnContext(), parms.getServiceProviderEntityID(), parms.getForceAuthn());
return generateQuery(request, parms.getEndPointLocation(), parms.getCertPath(), parms.getCertPass(), parms.getCertAlias(), relayState);
}

public static String logout(SamlParms parms, String relayState) {
Document request = SamlAssertionUtils.createLogoutRequest(parms.getId(), parms.getServiceProviderEntityID(), parms.getNameID(), parms.getSessionIndex(), parms.getSingleLogoutEndpoint());
return generateQuery(request, parms.getSingleLogoutEndpoint(), parms.getCertPath(), parms.getCertPass(), parms.getCertAlias(), relayState);
}

public boolean verifySignatures(SamlParms parms) {
logger.debug("verifySignatures");

try {
return verifySignature_internal(parms.getTrustCertPath(), parms.getTrustCertPass(), parms.getTrustCertAlias());
} catch (Exception e) {
logger.error("verifySignature", e);
return false;
}
}

public String getLogoutAssertions() {
logger.trace("getLogoutAssertions");
return SamlAssertionUtils.getLogoutInfo(this.xmlDoc);
}

public String getRelayState() {
logger.trace("getRelayState");
try {
return this.redirectMessage.get("RelayState") == null ? "" : URLDecoder.decode(this.redirectMessage.get("RelayState"), StandardCharsets.UTF_8.name());
} catch (Exception e) {
logger.error("getRelayState", e);
return "";
}
}

public String getLoginAssertions() {
//Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding
logger.error("getLoginAssertions - NOT IMPLEMENTED insecure SAML implementation");
return "";
}

public String getRoles(String name) {
//Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding
logger.error("getRoles - NOT IMPLEMENTED insecure SAML implementation");
return "";
}

public String getLoginAttribute(String name) {
//Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding
logger.error("getLoginAttribute - NOT IMPLEMENTED insecure SAML implementation");
return "";
}

public boolean isLogout(){
return SamlAssertionUtils.isLogout(this.xmlDoc);
}

// EXTERNAL OBJECT PUBLIC METHODS - END

private boolean verifySignature_internal(String certPath, String certPass, String certAlias) {
logger.trace("verifySignature_internal");

byte[] signature = Encoding.decodeParameter(this.redirectMessage.get("Signature"));

String signedMessage;
if (this.redirectMessage.containsKey("RelayState")) {
signedMessage = MessageFormat.format("SAMLResponse={0}", this.redirectMessage.get("SAMLResponse"));
signedMessage += MessageFormat.format("&RelayState={0}", this.redirectMessage.get("RelayState"));
signedMessage += MessageFormat.format("&SigAlg={0}", this.redirectMessage.get("SigAlg"));
} else {
signedMessage = MessageFormat.format("SAMLResponse={0}", this.redirectMessage.get("SAMLResponse"));
signedMessage += MessageFormat.format("&SigAlg={0}", this.redirectMessage.get("SigAlg"));
}

byte[] query = signedMessage.getBytes(StandardCharsets.UTF_8);

X509Certificate cert = Keys.loadCertificate(certPath, certAlias, certPass);

try (InputStream inputStream = new ByteArrayInputStream(query)) {
String sigalg = URLDecoder.decode(this.redirectMessage.get("SigAlg"), StandardCharsets.UTF_8.name());
RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(Hash.getHashFromSigAlg(sigalg)));
setUpSigner(signer, inputStream, Keys.getAsymmetricKeyParameter(cert), false);
return signer.verifySignature(signature);
} catch (Exception e) {
logger.error("verifySignature_internal", e);
return false;
}
}

private static Map<String, String> parseRedirect(String request) {
logger.trace("parseRedirect");
Map<String, String> result = new HashMap<>();
String[] redirect = request.split("&");

for (String s : redirect) {
String[] res = s.split("=");
result.put(res[0], res[1]);
}
return result;
}

private static String generateQuery(Document request, String destination, String certPath, String certPass, String alias, String relayState) {
logger.trace("generateQuery");
try {
String samlRequestParameter = Encoding.delfateAndEncodeXmlParameter(Encoding.documentToString(request));
String relayStateParameter = URLEncoder.encode(relayState, StandardCharsets.UTF_8.name());
Hash hash = Keys.isBase64(certPath) ? Hash.getHash(certPass.toUpperCase().trim()) : Hash.getHash(Keys.getHash(certPath, alias, certPass));

String sigAlgParameter = URLEncoder.encode(Hash.getSigAlg(hash), StandardCharsets.UTF_8.name());
String query = MessageFormat.format("SAMLRequest={0}&RelayState={1}&SigAlg={2}", samlRequestParameter, relayStateParameter, sigAlgParameter);
String signatureParameter = URLEncoder.encode(signRequest_RedirectBinding(query, certPath, certPass, hash, alias), StandardCharsets.UTF_8.name());

query += MessageFormat.format("&Signature={0}", signatureParameter);

logger.debug(MessageFormat.format("generateQuery - query: {0}", query));
return MessageFormat.format("{0}?{1}", destination, query);
} catch (Exception e) {
logger.error("generateQuery", e);
return "";
}

}

private static String signRequest_RedirectBinding(String query, String path, String password, Hash hash, String alias) {
logger.trace("signRequest_RedirectBinding");
RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(hash));
byte[] inputText = query.getBytes(StandardCharsets.UTF_8);
try (InputStream inputStream = new ByteArrayInputStream(inputText)) {
setUpSigner(signer, inputStream, Keys.loadPrivateKey(path, alias, password), true);
byte[] outputBytes = signer.generateSignature();
return Base64.toBase64String(outputBytes);
} catch (Exception e) {
logger.error("signRequest_RedirectBinding", e);
return "";
}
}

private static void setUpSigner(Signer signer, InputStream input, AsymmetricKeyParameter asymmetricKeyParameter,
boolean toSign) {
logger.trace("setUpSigner");
byte[] buffer = new byte[8192];
int n;
try {
signer.init(toSign, asymmetricKeyParameter);
while ((n = input.read(buffer)) > 0) {
signer.update(buffer, 0, n);
}
} catch (Exception e) {
logger.error("setUpSigner", e);
}
}
}
Loading
Loading