Skip to content

Commit 7abf36c

Browse files
authored
New SAML 2.0 implementation for GAM (#951)
* New SAML 2.0 implementation for GAM * Change package name to com.genexus.saml20 * Fix RedirectBinding refactor error * Fix code quality issue * Fix Attribute class code quality * Change parameter's names for GAM compatibility * Fix signature verification on RedirectBinding, add security measures for DSig signature validation * Fix format issues and change signature verification on RedirectBinding * Change certificate parameters * Fix typing mistake on artifact id * Add isLogout function * Fix conflict with SecurityAPI modules in master pom * Fix conflict with ordering * Fix issue finding attributes on Agesic message * Set policy format directly from GAM * Manage exception in IsLogout * Simple fix to read assertion attributes * Saml20 EO tests
1 parent 86b1763 commit 7abf36c

File tree

18 files changed

+1681
-0
lines changed

18 files changed

+1681
-0
lines changed

gamsaml20/pom.xml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>com.genexus</groupId>
8+
<artifactId>parent</artifactId>
9+
<version>${revision}${changelist}</version>
10+
</parent>
11+
12+
<artifactId>gamsaml20</artifactId>
13+
<name>GAM Saml 2.0 EO</name>
14+
15+
<properties>
16+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
17+
</properties>
18+
<dependencies>
19+
<dependency>
20+
<groupId>org.bouncycastle</groupId>
21+
<artifactId>bcprov-jdk18on</artifactId>
22+
<version>1.78.1</version>
23+
<scope>compile</scope>
24+
</dependency>
25+
26+
<dependency>
27+
<groupId>org.apache.logging.log4j</groupId>
28+
<artifactId>log4j-core</artifactId>
29+
<version>${log4j.version}</version>
30+
<scope>compile</scope>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.apache.santuario</groupId>
34+
<artifactId>xmlsec</artifactId>
35+
<version>3.0.3</version>
36+
</dependency>
37+
<dependency>
38+
<groupId>commons-io</groupId>
39+
<artifactId>commons-io</artifactId>
40+
<version>2.11.0</version>
41+
<scope>compile</scope>
42+
</dependency>
43+
</dependencies>
44+
45+
<build>
46+
<finalName>gamsaml20</finalName>
47+
<plugins>
48+
<plugin>
49+
<groupId>org.apache.maven.plugins</groupId>
50+
<artifactId>maven-compiler-plugin</artifactId>
51+
<version>3.8.0</version>
52+
</plugin>
53+
<plugin>
54+
<groupId>org.apache.maven.plugins</groupId>
55+
<artifactId>maven-jar-plugin</artifactId>
56+
<version>3.1.1</version>
57+
<executions>
58+
<execution>
59+
<goals>
60+
<goal>test-jar</goal>
61+
</goals>
62+
</execution>
63+
</executions>
64+
</plugin>
65+
</plugins>
66+
67+
</build>
68+
69+
</project>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.genexus.saml20;
2+
3+
import com.genexus.saml20.utils.SamlAssertionUtils;
4+
5+
@SuppressWarnings("unused")
6+
public abstract class Binding {
7+
8+
abstract void init(String input);
9+
10+
static String login(SamlParms parms, String relayState) {
11+
return "";
12+
}
13+
14+
static String logout(SamlParms parms, String relayState) {
15+
return "";
16+
}
17+
18+
abstract boolean verifySignatures(SamlParms parms);
19+
20+
abstract String getLoginAssertions();
21+
22+
abstract String getLoginAttribute(String name);
23+
24+
abstract String getRoles(String name);
25+
26+
abstract String getLogoutAssertions();
27+
28+
abstract boolean isLogout();
29+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.genexus.saml20;
2+
3+
import com.genexus.saml20.utils.DSig;
4+
import com.genexus.saml20.utils.Encoding;
5+
import com.genexus.saml20.utils.SamlAssertionUtils;
6+
import org.apache.logging.log4j.LogManager;
7+
import org.apache.logging.log4j.Logger;
8+
import org.w3c.dom.Document;
9+
10+
import java.text.MessageFormat;
11+
12+
@SuppressWarnings("unused")
13+
public class PostBinding extends Binding {
14+
15+
private static final Logger logger = LogManager.getLogger(PostBinding.class);
16+
17+
private Document xmlDoc;
18+
19+
public PostBinding() {
20+
logger.trace("PostBinding constructor");
21+
xmlDoc = null;
22+
}
23+
// EXTERNAL OBJECT PUBLIC METHODS - BEGIN
24+
25+
26+
public void init(String xml) {
27+
logger.trace("init");
28+
this.xmlDoc = SamlAssertionUtils.canonicalizeXml(xml);
29+
logger.debug(MessageFormat.format("Init - XML IdP response: {0}", Encoding.documentToString(xmlDoc)));
30+
}
31+
32+
public static String login(SamlParms parms, String relayState) {
33+
//not implemented yet
34+
logger.error("login - NOT IMPLEMENTED");
35+
return "";
36+
}
37+
38+
public static String logout(SamlParms parms, String relayState) {
39+
//not implemented yet
40+
logger.error("logout - NOT IMPLEMENTED");
41+
return "";
42+
}
43+
44+
public boolean verifySignatures(SamlParms parms) {
45+
return DSig.validateSignatures(this.xmlDoc, parms.getTrustCertPath(), parms.getTrustCertAlias(), parms.getTrustCertPass());
46+
}
47+
48+
public String getLoginAssertions() {
49+
logger.trace("getLoginAssertions");
50+
return SamlAssertionUtils.getLoginInfo(this.xmlDoc);
51+
}
52+
53+
public String getLogoutAssertions() {
54+
logger.trace("getLogoutAssertions");
55+
return SamlAssertionUtils.getLogoutInfo(this.xmlDoc);
56+
}
57+
58+
public String getLoginAttribute(String name) {
59+
logger.trace("getLoginAttribute");
60+
return SamlAssertionUtils.getLoginAttribute(this.xmlDoc, name).trim();
61+
}
62+
63+
public String getRoles(String name) {
64+
logger.debug("getRoles");
65+
return SamlAssertionUtils.getRoles(this.xmlDoc, name);
66+
}
67+
68+
public boolean isLogout(){
69+
return SamlAssertionUtils.isLogout(this.xmlDoc);
70+
}
71+
72+
// EXTERNAL OBJECT PUBLIC METHODS - END
73+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package com.genexus.saml20;
2+
3+
import com.genexus.saml20.utils.*;
4+
import org.apache.logging.log4j.LogManager;
5+
import org.apache.logging.log4j.Logger;
6+
import org.bouncycastle.crypto.Signer;
7+
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
8+
import org.bouncycastle.crypto.signers.RSADigestSigner;
9+
import org.bouncycastle.util.encoders.Base64;
10+
import org.w3c.dom.Document;
11+
12+
import java.io.ByteArrayInputStream;
13+
import java.io.InputStream;
14+
import java.net.URLDecoder;
15+
import java.net.URLEncoder;
16+
import java.nio.charset.StandardCharsets;
17+
import java.security.cert.X509Certificate;
18+
import java.text.MessageFormat;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
@SuppressWarnings("unused")
23+
public class RedirectBinding extends Binding {
24+
25+
private static final Logger logger = LogManager.getLogger(RedirectBinding.class);
26+
27+
private Document xmlDoc;
28+
private Map<String, String> redirectMessage;
29+
30+
// EXTERNAL OBJECT PUBLIC METHODS - BEGIN
31+
32+
33+
public RedirectBinding() {
34+
logger.trace("RedirectBinding constructor");
35+
}
36+
37+
public void init(String queryString) {
38+
logger.trace("init");
39+
logger.debug(MessageFormat.format("init - queryString : {0}", queryString));
40+
this.redirectMessage = parseRedirect(queryString);
41+
String xml = Encoding.decodeAndInflateXmlParameter(this.redirectMessage.get("SAMLResponse"));
42+
logger.debug("init - inflated xml: {0}", xml);
43+
this.xmlDoc = SamlAssertionUtils.canonicalizeXml(xml);
44+
logger.debug(MessageFormat.format("init - XML IdP response: {0}", Encoding.documentToString(xmlDoc)));
45+
}
46+
47+
48+
public static String login(SamlParms parms, String relayState) {
49+
Document request = SamlAssertionUtils.createLoginRequest(parms.getId(), parms.getEndPointLocation(), parms.getAcs(), parms.getIdentityProviderEntityID(), parms.getPolicyFormat(), parms.getAuthnContext(), parms.getServiceProviderEntityID(), parms.getForceAuthn());
50+
return generateQuery(request, parms.getEndPointLocation(), parms.getCertPath(), parms.getCertPass(), parms.getCertAlias(), relayState);
51+
}
52+
53+
public static String logout(SamlParms parms, String relayState) {
54+
Document request = SamlAssertionUtils.createLogoutRequest(parms.getId(), parms.getServiceProviderEntityID(), parms.getNameID(), parms.getSessionIndex(), parms.getSingleLogoutEndpoint());
55+
return generateQuery(request, parms.getSingleLogoutEndpoint(), parms.getCertPath(), parms.getCertPass(), parms.getCertAlias(), relayState);
56+
}
57+
58+
public boolean verifySignatures(SamlParms parms) {
59+
logger.debug("verifySignatures");
60+
61+
try {
62+
return verifySignature_internal(parms.getTrustCertPath(), parms.getTrustCertPass(), parms.getTrustCertAlias());
63+
} catch (Exception e) {
64+
logger.error("verifySignature", e);
65+
return false;
66+
}
67+
}
68+
69+
public String getLogoutAssertions() {
70+
logger.trace("getLogoutAssertions");
71+
return SamlAssertionUtils.getLogoutInfo(this.xmlDoc);
72+
}
73+
74+
public String getRelayState() {
75+
logger.trace("getRelayState");
76+
try {
77+
return this.redirectMessage.get("RelayState") == null ? "" : URLDecoder.decode(this.redirectMessage.get("RelayState"), StandardCharsets.UTF_8.name());
78+
} catch (Exception e) {
79+
logger.error("getRelayState", e);
80+
return "";
81+
}
82+
}
83+
84+
public String getLoginAssertions() {
85+
//Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding
86+
logger.error("getLoginAssertions - NOT IMPLEMENTED insecure SAML implementation");
87+
return "";
88+
}
89+
90+
public String getRoles(String name) {
91+
//Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding
92+
logger.error("getRoles - NOT IMPLEMENTED insecure SAML implementation");
93+
return "";
94+
}
95+
96+
public String getLoginAttribute(String name) {
97+
//Getting user's data by URL parms (GET) is deemed insecure so we are not implementing this method for redirect binding
98+
logger.error("getLoginAttribute - NOT IMPLEMENTED insecure SAML implementation");
99+
return "";
100+
}
101+
102+
public boolean isLogout(){
103+
return SamlAssertionUtils.isLogout(this.xmlDoc);
104+
}
105+
106+
// EXTERNAL OBJECT PUBLIC METHODS - END
107+
108+
private boolean verifySignature_internal(String certPath, String certPass, String certAlias) {
109+
logger.trace("verifySignature_internal");
110+
111+
byte[] signature = Encoding.decodeParameter(this.redirectMessage.get("Signature"));
112+
113+
String signedMessage;
114+
if (this.redirectMessage.containsKey("RelayState")) {
115+
signedMessage = MessageFormat.format("SAMLResponse={0}", this.redirectMessage.get("SAMLResponse"));
116+
signedMessage += MessageFormat.format("&RelayState={0}", this.redirectMessage.get("RelayState"));
117+
signedMessage += MessageFormat.format("&SigAlg={0}", this.redirectMessage.get("SigAlg"));
118+
} else {
119+
signedMessage = MessageFormat.format("SAMLResponse={0}", this.redirectMessage.get("SAMLResponse"));
120+
signedMessage += MessageFormat.format("&SigAlg={0}", this.redirectMessage.get("SigAlg"));
121+
}
122+
123+
byte[] query = signedMessage.getBytes(StandardCharsets.UTF_8);
124+
125+
X509Certificate cert = Keys.loadCertificate(certPath, certAlias, certPass);
126+
127+
try (InputStream inputStream = new ByteArrayInputStream(query)) {
128+
String sigalg = URLDecoder.decode(this.redirectMessage.get("SigAlg"), StandardCharsets.UTF_8.name());
129+
RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(Hash.getHashFromSigAlg(sigalg)));
130+
setUpSigner(signer, inputStream, Keys.getAsymmetricKeyParameter(cert), false);
131+
return signer.verifySignature(signature);
132+
} catch (Exception e) {
133+
logger.error("verifySignature_internal", e);
134+
return false;
135+
}
136+
}
137+
138+
private static Map<String, String> parseRedirect(String request) {
139+
logger.trace("parseRedirect");
140+
Map<String, String> result = new HashMap<>();
141+
String[] redirect = request.split("&");
142+
143+
for (String s : redirect) {
144+
String[] res = s.split("=");
145+
result.put(res[0], res[1]);
146+
}
147+
return result;
148+
}
149+
150+
private static String generateQuery(Document request, String destination, String certPath, String certPass, String alias, String relayState) {
151+
logger.trace("generateQuery");
152+
try {
153+
String samlRequestParameter = Encoding.delfateAndEncodeXmlParameter(Encoding.documentToString(request));
154+
String relayStateParameter = URLEncoder.encode(relayState, StandardCharsets.UTF_8.name());
155+
Hash hash = Keys.isBase64(certPath) ? Hash.getHash(certPass.toUpperCase().trim()) : Hash.getHash(Keys.getHash(certPath, alias, certPass));
156+
157+
String sigAlgParameter = URLEncoder.encode(Hash.getSigAlg(hash), StandardCharsets.UTF_8.name());
158+
String query = MessageFormat.format("SAMLRequest={0}&RelayState={1}&SigAlg={2}", samlRequestParameter, relayStateParameter, sigAlgParameter);
159+
String signatureParameter = URLEncoder.encode(signRequest_RedirectBinding(query, certPath, certPass, hash, alias), StandardCharsets.UTF_8.name());
160+
161+
query += MessageFormat.format("&Signature={0}", signatureParameter);
162+
163+
logger.debug(MessageFormat.format("generateQuery - query: {0}", query));
164+
return MessageFormat.format("{0}?{1}", destination, query);
165+
} catch (Exception e) {
166+
logger.error("generateQuery", e);
167+
return "";
168+
}
169+
170+
}
171+
172+
private static String signRequest_RedirectBinding(String query, String path, String password, Hash hash, String alias) {
173+
logger.trace("signRequest_RedirectBinding");
174+
RSADigestSigner signer = new RSADigestSigner(Hash.getDigest(hash));
175+
byte[] inputText = query.getBytes(StandardCharsets.UTF_8);
176+
try (InputStream inputStream = new ByteArrayInputStream(inputText)) {
177+
setUpSigner(signer, inputStream, Keys.loadPrivateKey(path, alias, password), true);
178+
byte[] outputBytes = signer.generateSignature();
179+
return Base64.toBase64String(outputBytes);
180+
} catch (Exception e) {
181+
logger.error("signRequest_RedirectBinding", e);
182+
return "";
183+
}
184+
}
185+
186+
private static void setUpSigner(Signer signer, InputStream input, AsymmetricKeyParameter asymmetricKeyParameter,
187+
boolean toSign) {
188+
logger.trace("setUpSigner");
189+
byte[] buffer = new byte[8192];
190+
int n;
191+
try {
192+
signer.init(toSign, asymmetricKeyParameter);
193+
while ((n = input.read(buffer)) > 0) {
194+
signer.update(buffer, 0, n);
195+
}
196+
} catch (Exception e) {
197+
logger.error("setUpSigner", e);
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)