Skip to content

Commit 82b2489

Browse files
committed
Merge pull request #10 from iguanajazz/master
security implementations, example webapp was included an some bugs was fixed.
2 parents 96cbb18 + 20e92b4 commit 82b2489

12 files changed

Lines changed: 599 additions & 69 deletions

File tree

README

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
This example was developed to show how Java Toolkit works.
2+
3+
The com-folder contains the files you'll copy into your Java application.
4+
The sample-folder has a minimal webapp with the files on com-folder inside java-folder and The files index.jsp and consume.jsp inside webapp-folder.
5+
index.jsp and consume.jsp are the ones that actually handle the SAML conversation.
6+
7+
The index.jsp file acts as an initiater for the SAML conversation, if it should be initiated by the application.
8+
9+
This is called Service Provider Initiated SAML. The service provider creates a SAML Authentication Request and sends it to the identity provider (IdP),
10+
We authenticate at the IdP and then a Response is sent to the Consumer Service Url configured on index.jsp.
11+
12+
In order to know where to redirect the user with the authentication request, we need to establish the user's identity provider affinity.
13+
This depends on your application. In this example, those validations are provided by consume.jsp, which is meant as a stub for you customization.
14+
15+
Dependencies are configured on file pom.xml, also a jetty plugin is configured to execute the example with command "mvn jetty:run"
16+
17+
18+
What needs to be configured
19+
----------------------------
20+
21+
In the example above, SAML settings are divided into two parts, the application specific (const_assertion_consumer_service_url, const_issuer, const_name_identifier_format)
22+
and the user/account specific (idp_sso_target_url, x509certificate). You'll need to add your own code here to identify the user or user origin (e.g. by subdomain, ip_address etc.).
23+
24+
The following information needs to be available on the account:
25+
26+
appSettings.setAssertionConsumerServiceUrl
27+
The URL at which the SAML assertion should be received. In this example "http://localhost:3000/saml/consume" would be correct.
28+
29+
appSettings.setIssuer
30+
The name of your application. Some identity providers might need this to establish the identity of the service provider requesting the login.
31+
32+
accSettings.setIdpSsoTargetUrl
33+
The URL to which the authentication request should be sent. This would be on the identity provider.
34+
35+
accountSettings.setCertificate
36+
The x509 certificate fingerprint.
37+
This is provided from the identity provider when setting up the relationship, for this version the certificate must be 1024-bit.

com/onelogin/saml/AuthRequest.java

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,29 +74,6 @@ public String getRequest(int format) throws XMLStreamException, IOException {
7474
return result;
7575
}
7676

77-
public static String getRidOfCRLF(String what) {
78-
String lf = "%0D";
79-
String cr = "%0A";
80-
String now = lf;
81-
int index = what.indexOf(now);
82-
83-
StringBuilder r = new StringBuilder();
84-
85-
while (index!=-1) {
86-
r.append(what.substring(0,index));
87-
what = what.substring(index+3,what.length());
88-
89-
if (now.equals(lf)) {
90-
now = cr;
91-
} else {
92-
now = lf;
93-
}
94-
95-
index = what.indexOf(now);
96-
}
97-
return r.toString();
98-
}
99-
10077
private String encodeSAMLRequest(byte[] pSAMLRequest) throws RuntimeException {
10178

10279
Base64 base64Encoder = new Base64();
@@ -112,7 +89,7 @@ private String encodeSAMLRequest(byte[] pSAMLRequest) throws RuntimeException {
11289

11390
String stream = new String(base64Encoder.encode(byteArray.toByteArray()));
11491

115-
return stream;
92+
return stream.trim();
11693
} catch (Exception e) {
11794
throw new RuntimeException(e);
11895
}

com/onelogin/saml/Response.java

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
package com.onelogin.saml;
22

3-
import com.onelogin.AccountSettings;
43
import java.io.ByteArrayInputStream;
54
import java.io.IOException;
65
import java.lang.reflect.Method;
76
import java.security.cert.CertificateException;
87
import java.security.cert.X509Certificate;
98
import java.util.ArrayList;
9+
import java.util.Calendar;
1010
import java.util.HashMap;
11+
import java.util.TimeZone;
12+
1113
import javax.xml.XMLConstants;
1214
import javax.xml.crypto.dsig.XMLSignature;
1315
import javax.xml.crypto.dsig.XMLSignatureFactory;
1416
import javax.xml.crypto.dsig.dom.DOMValidateContext;
1517
import javax.xml.parsers.DocumentBuilder;
1618
import javax.xml.parsers.DocumentBuilderFactory;
1719
import javax.xml.parsers.ParserConfigurationException;
20+
1821
import org.apache.commons.codec.binary.Base64;
1922
import org.w3c.dom.Document;
2023
import org.w3c.dom.Element;
@@ -23,72 +26,136 @@
2326
import org.w3c.dom.NodeList;
2427
import org.xml.sax.SAXException;
2528

29+
import com.onelogin.AccountSettings;
30+
2631
public class Response {
2732

2833
private Document xmlDoc;
29-
private Integer Assertions;
34+
private NodeList assertions;
3035
private Element rootElement;
3136
private final AccountSettings accountSettings;
3237
private final Certificate certificate;
38+
private String currentUrl;
3339

3440
public Response(AccountSettings accountSettings) throws CertificateException {
35-
this.accountSettings = accountSettings;
41+
this.accountSettings = accountSettings;
3642
certificate = new Certificate();
3743
certificate.loadCertificate(this.accountSettings.getCertificate());
3844
}
3945

40-
public void loadXml(String xml) throws ParserConfigurationException, SAXException, IOException {
41-
DocumentBuilderFactory fty = DocumentBuilderFactory.newInstance();
46+
public void loadXml(String xml) throws ParserConfigurationException, SAXException, IOException {
47+
DocumentBuilderFactory fty = DocumentBuilderFactory.newInstance();
4248
fty.setNamespaceAware(true);
4349
// XMLConstants with FEATURE_SECURE_PROCESSING prevents external document access. (XXE/XEE Possible Attacks).
4450
fty.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
45-
DocumentBuilder builder = fty.newDocumentBuilder();
46-
ByteArrayInputStream bais = new ByteArrayInputStream(xml.getBytes());
47-
xmlDoc = builder.parse(bais);
51+
DocumentBuilder builder = fty.newDocumentBuilder();
52+
ByteArrayInputStream bais = new ByteArrayInputStream(xml.getBytes());
53+
xmlDoc = builder.parse(bais);
4854
}
4955

50-
public void loadXmlFromBase64(String response) throws ParserConfigurationException, SAXException, IOException {
56+
public void loadXmlFromBase64(String response) throws ParserConfigurationException, SAXException, IOException {
5157
Base64 base64 = new Base64();
5258
byte[] decodedB = base64.decode(response);
5359
String decodedS = new String(decodedB);
5460
loadXml(decodedS);
5561
}
5662

5763
// isValid() function should be called to make basic security checks to responses.
58-
public boolean isValid() throws Exception {
64+
public boolean isValid() throws Exception {
5965
// Security Checks
60-
rootElement = xmlDoc.getDocumentElement();
61-
Assertions = xmlDoc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Assertion").getLength();
66+
rootElement = xmlDoc.getDocumentElement();
67+
assertions = xmlDoc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Assertion");
6268
xmlDoc.getDocumentElement().normalize();
6369

64-
if (Assertions > 1) {
65-
throw new Exception("SAML Response must contain 1 Assertion.");
70+
// Check SAML version
71+
String attName = rootElement.getAttribute("Version");
72+
if (!attName.equals("2.0")) {
73+
throw new Exception("Unsupported SAML Version.");
6674
}
67-
68-
String attName = rootElement.getAttribute("ID");
75+
76+
// Check ID in the response
77+
attName = rootElement.getAttribute("ID");
6978
if (attName.equals("")) {
7079
throw new Exception("Missing ID attribute on SAML Response.");
7180
}
72-
73-
attName = rootElement.getAttribute("Version");
74-
if (!attName.equals("2.0")) {
75-
throw new Exception("Unsupported SAML Version.");
81+
82+
if (assertions == null || assertions.getLength() != 1) {
83+
throw new Exception("SAML Response must contain 1 Assertion.");
7684
}
7785

78-
NodeList nodes = xmlDoc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
79-
86+
NodeList nodes = xmlDoc.getElementsByTagNameNS("*", "Signature");
8087
if (nodes == null || nodes.getLength() == 0) {
8188
throw new Exception("Can't find signature in Document.");
8289
}
8390

84-
if (setIdAttributeExists()) {
85-
tagIdAttributes(xmlDoc);
91+
// Check destination
92+
String destinationUrl = rootElement.getAttribute("Destination");
93+
if (destinationUrl != null) {
94+
if(!destinationUrl.equals(currentUrl)){
95+
throw new Exception("The response was received at " + currentUrl + " instead of " + destinationUrl);
96+
}
8697
}
87-
88-
X509Certificate cert = certificate.getX509Cert();
89-
DOMValidateContext ctx = new DOMValidateContext(cert.getPublicKey(), nodes.item(0));
90-
XMLSignatureFactory sigF = XMLSignatureFactory.getInstance("DOM");
91-
XMLSignature xmlSignature = sigF.unmarshalXMLSignature(ctx);
98+
99+
// Check Audience
100+
NodeList nodeAudience = xmlDoc.getElementsByTagNameNS("*", "Audience");
101+
String audienceUrl = nodeAudience.item(0).getChildNodes().item(0).getNodeValue();
102+
if (audienceUrl != null) {
103+
if(!audienceUrl.equals(currentUrl)){
104+
throw new Exception(audienceUrl + " is not a valid audience for this Response");
105+
}
106+
}
107+
108+
// Check SubjectConfirmation, at least one SubjectConfirmation must be valid
109+
NodeList nodeSubConf = xmlDoc.getElementsByTagNameNS("*", "SubjectConfirmation");
110+
boolean validSubjectConfirmation = true;
111+
for(int i = 0; i < nodeSubConf.getLength(); i++){
112+
Node method = nodeSubConf.item(i).getAttributes().getNamedItem("Method");
113+
if(method != null && !method.getNodeValue().equals("urn:oasis:names:tc:SAML:2.0:cm:bearer")){
114+
continue;
115+
}
116+
NodeList childs = nodeSubConf.item(i).getChildNodes();
117+
for(int c = 0; c < childs.getLength(); c++){
118+
if(childs.item(c).getLocalName().equals("SubjectConfirmationData")){
119+
Node inResponseTo = childs.item(c).getAttributes().getNamedItem("InResponseTo");
120+
// if(inResponseTo != null && !inResponseTo.getNodeValue().equals("ID of the AuthNRequest")){
121+
// validSubjectConfirmation = false;
122+
// }
123+
Node recipient = childs.item(c).getAttributes().getNamedItem("Recipient");
124+
if(recipient != null && !recipient.getNodeValue().equals(currentUrl)){
125+
validSubjectConfirmation = false;
126+
}
127+
Node notOnOrAfter = childs.item(c).getAttributes().getNamedItem("NotOnOrAfter");
128+
if(notOnOrAfter != null){
129+
final Calendar notOnOrAfterDate = javax.xml.bind.DatatypeConverter.parseDateTime(notOnOrAfter.getNodeValue());
130+
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
131+
if(notOnOrAfterDate.before(now)){
132+
validSubjectConfirmation = false;
133+
}
134+
}
135+
Node notBefore = childs.item(c).getAttributes().getNamedItem("NotBefore");
136+
if(notBefore != null){
137+
final Calendar notBeforeDate = javax.xml.bind.DatatypeConverter.parseDateTime(notBefore.getNodeValue());
138+
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
139+
if(notBeforeDate.before(now)){
140+
validSubjectConfirmation = false;
141+
}
142+
}
143+
}
144+
}
145+
}
146+
if (!validSubjectConfirmation) {
147+
throw new Exception("A valid SubjectConfirmation was not found on this Response");
148+
}
149+
150+
151+
// if (setIdAttributeExists()) {
152+
// tagIdAttributes(xmlDoc);
153+
// }
154+
155+
X509Certificate cert = certificate.getX509Cert();
156+
DOMValidateContext ctx = new DOMValidateContext(cert.getPublicKey(), nodes.item(0));
157+
XMLSignatureFactory sigF = XMLSignatureFactory.getInstance("DOM");
158+
XMLSignature xmlSignature = sigF.unmarshalXMLSignature(ctx);
92159

93160
return xmlSignature.validate(ctx);
94161
}
@@ -141,7 +208,10 @@ private boolean setIdAttributeExists() {
141208
}
142209

143210
private void tagIdAttributes(Document xmlDoc) {
144-
throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
211+
throw new UnsupportedOperationException("Not supported yet.");
145212
}
146213

214+
public void setDestinationUrl(String urld){
215+
currentUrl = urld;
216+
}
147217
}

sample/pom.xml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<groupId>com.onelogin</groupId>
5+
<artifactId>java-saml</artifactId>
6+
<packaging>war</packaging>
7+
<version>1.0-SNAPSHOT</version>
8+
<name>java-saml Sample Webapp</name>
9+
10+
<properties>
11+
<jettyVersion>9.1.0.RC2</jettyVersion>
12+
</properties>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>commons-codec</groupId>
17+
<artifactId>commons-codec</artifactId>
18+
<version>1.9</version>
19+
</dependency>
20+
<dependency>
21+
<groupId>org.eclipse.jetty</groupId>
22+
<artifactId>jetty-servlet</artifactId>
23+
<version>${jettyVersion}</version>
24+
</dependency>
25+
</dependencies>
26+
27+
<build>
28+
<finalName>java-saml</finalName>
29+
<plugins>
30+
<plugin>
31+
<groupId>org.apache.maven.plugins</groupId>
32+
<artifactId>maven-compiler-plugin</artifactId>
33+
<version>3.0</version>
34+
<configuration>
35+
<source>1.7</source>
36+
<target>1.7</target>
37+
</configuration>
38+
</plugin>
39+
<plugin>
40+
<groupId>org.eclipse.jetty</groupId>
41+
<artifactId>jetty-maven-plugin</artifactId>
42+
<version>${jettyVersion}</version>
43+
</plugin>
44+
</plugins>
45+
</build>
46+
</project>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.onelogin;
2+
3+
public class AccountSettings {
4+
private String certificate;
5+
private String idp_sso_target_url;
6+
7+
public String getCertificate() {
8+
return certificate;
9+
}
10+
public void setCertificate(String certificate) {
11+
this.certificate = certificate;
12+
}
13+
public String getIdp_sso_target_url() {
14+
return idp_sso_target_url;
15+
}
16+
public void setIdpSsoTargetUrl(String idp_sso_target_url) {
17+
this.idp_sso_target_url = idp_sso_target_url;
18+
}
19+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.onelogin;
2+
3+
public class AppSettings {
4+
private String assertionConsumerServiceUrl;
5+
private String issuer;
6+
7+
public String getAssertionConsumerServiceUrl() {
8+
return assertionConsumerServiceUrl;
9+
}
10+
public void setAssertionConsumerServiceUrl(String assertionConsumerServiceUrl) {
11+
this.assertionConsumerServiceUrl = assertionConsumerServiceUrl;
12+
}
13+
public String getIssuer() {
14+
return issuer;
15+
}
16+
public void setIssuer(String issuer) {
17+
this.issuer = issuer;
18+
}
19+
20+
}

0 commit comments

Comments
 (0)