Skip to content

Commit 73beee3

Browse files
committed
Validate XML based on SAML Schema, time validations and num assertions
1 parent 92c6eed commit 73beee3

17 files changed

Lines changed: 3125 additions & 80 deletions

src/main/java/com/onelogin/saml/Response.java

Lines changed: 139 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import javax.xml.crypto.dsig.dom.DOMValidateContext;
1515

1616
import org.apache.commons.codec.binary.Base64;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
1719
import org.w3c.dom.Document;
1820
import org.w3c.dom.Element;
1921
import org.w3c.dom.NamedNodeMap;
@@ -26,13 +28,20 @@
2628

2729
public class Response {
2830

29-
private Document xmlDoc;
31+
/**
32+
* A DOMDocument class loaded from the SAML Response (Decrypted).
33+
*/
34+
private Document document;
35+
3036
private NodeList assertions;
3137
private Element rootElement;
3238
private final AccountSettings accountSettings;
3339
private final Certificate certificate;
40+
private String response;
3441
private String currentUrl;
3542
private StringBuffer error;
43+
44+
private static final Logger log = LoggerFactory.getLogger(Response.class);
3645

3746
public Response(AccountSettings accountSettings) throws CertificateException {
3847
error = new StringBuffer();
@@ -42,26 +51,29 @@ public Response(AccountSettings accountSettings) throws CertificateException {
4251
}
4352

4453
public Response(AccountSettings accountSettings, String response) throws Exception {
45-
this(accountSettings);
54+
this(accountSettings);
4655
loadXmlFromBase64(response);
4756
}
4857

49-
public void loadXmlFromBase64(String response) throws Exception {
58+
public void loadXmlFromBase64(String responseStr) throws Exception {
5059
Base64 base64 = new Base64();
51-
byte[] decodedB = base64.decode(response);
52-
String decodedS = new String(decodedB);
53-
xmlDoc = Utils.loadXML(decodedS);
54-
System.out.println("xmlDoc [ "+xmlDoc.getDocumentElement()+" ]");
60+
byte[] decodedB = base64.decode(responseStr);
61+
this.response = new String(decodedB);
62+
this.document = Utils.loadXML(this.response);
63+
if(this.document == null){
64+
65+
}
5566
}
5667

5768
// isValid() function should be called to make basic security checks to responses.
58-
public boolean isValid(){
69+
public boolean isValid(String... requestId){
5970
try{
6071

6172
// Security Checks
62-
rootElement = xmlDoc.getDocumentElement();
63-
assertions = xmlDoc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Assertion");
64-
xmlDoc.getDocumentElement().normalize();
73+
rootElement = document.getDocumentElement();
74+
rootElement.normalize();
75+
assertions = document.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Assertion");
76+
6577

6678
// Check SAML version
6779
if (!rootElement.getAttribute("Version").equals("2.0")) {
@@ -75,14 +87,41 @@ public boolean isValid(){
7587

7688
checkStatus();
7789

78-
if (assertions == null || assertions.getLength() != 1) {
90+
if (!this.validateNumAssertions()) {
7991
throw new Exception("SAML Response must contain 1 Assertion.");
8092
}
8193

82-
NodeList nodes = xmlDoc.getElementsByTagNameNS("*", "Signature");
83-
if (nodes == null || nodes.getLength() == 0) {
84-
throw new Exception("Can't find signature in Document.");
94+
NodeList nodes = document.getElementsByTagName("Signature");
95+
ArrayList<String> signedElements = new ArrayList<String>();
96+
for (int i = 0; i < nodes.getLength(); i++) {
97+
signedElements.add(nodes.item(i).getParentNode().getLocalName());
98+
}
99+
if (!signedElements.isEmpty()) {
100+
if(!this.validateSignedElements(signedElements)){
101+
throw new Exception("Found an unexpected Signature Element. SAML Response rejected");
102+
}
103+
}
104+
105+
Document res = Utils.validateXML(this.document, "saml-schema-protocol-2.0.xsd");
106+
107+
if(!(res instanceof Document)){
108+
throw new Exception("Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd");
85109
}
110+
111+
if (rootElement.hasAttribute("InResponseTo")) {
112+
String responseInResponseTo = document.getDocumentElement().getAttribute("InResponseTo");
113+
if(requestId.length > 0 && responseInResponseTo.compareTo(requestId[0]) != 0){
114+
throw new Exception("The InResponseTo of the Response: "+ responseInResponseTo + ", does not match the ID of the AuthNRequest sent by the SP: "+ requestId[0]);
115+
}
116+
}
117+
118+
// Validate Asserion timestamps
119+
if (!this.validateTimestamps()) {
120+
throw new Exception("Timing issues (please check your clock settings)");
121+
}
122+
123+
// ------------ working validations until here!
124+
//TODO: more validations
86125

87126
// Check destination
88127
String destinationUrl = rootElement.getAttribute("Destination");
@@ -93,7 +132,7 @@ public boolean isValid(){
93132
}
94133

95134
// Check Audience
96-
NodeList nodeAudience = xmlDoc.getElementsByTagNameNS("*", "Audience");
135+
NodeList nodeAudience = document.getElementsByTagNameNS("*", "Audience");
97136
String audienceUrl = nodeAudience.item(0).getChildNodes().item(0).getNodeValue();
98137
if (audienceUrl != null) {
99138
if(!audienceUrl.equals(currentUrl)){
@@ -102,7 +141,7 @@ public boolean isValid(){
102141
}
103142

104143
// Check SubjectConfirmation, at least one SubjectConfirmation must be valid
105-
NodeList nodeSubConf = xmlDoc.getElementsByTagNameNS("*", "SubjectConfirmation");
144+
NodeList nodeSubConf = document.getElementsByTagNameNS("*", "SubjectConfirmation");
106145
boolean validSubjectConfirmation = true;
107146
for(int i = 0; i < nodeSubConf.getLength(); i++){
108147
Node method = nodeSubConf.item(i).getAttributes().getNamedItem("Method");
@@ -112,30 +151,12 @@ public boolean isValid(){
112151
NodeList childs = nodeSubConf.item(i).getChildNodes();
113152
for(int c = 0; c < childs.getLength(); c++){
114153
if(childs.item(c).getLocalName().equals("SubjectConfirmationData")){
115-
Node inResponseTo = childs.item(c).getAttributes().getNamedItem("InResponseTo");
116-
// if(inResponseTo != null && !inResponseTo.getNodeValue().equals("ID of the AuthNRequest")){
117-
// validSubjectConfirmation = false;
118-
// }
154+
119155
Node recipient = childs.item(c).getAttributes().getNamedItem("Recipient");
120156
if(recipient != null && !recipient.getNodeValue().equals(currentUrl)){
121157
validSubjectConfirmation = false;
122158
}
123-
Node notOnOrAfter = childs.item(c).getAttributes().getNamedItem("NotOnOrAfter");
124-
if(notOnOrAfter != null){
125-
final Calendar notOnOrAfterDate = javax.xml.bind.DatatypeConverter.parseDateTime(notOnOrAfter.getNodeValue());
126-
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
127-
if(notOnOrAfterDate.before(now)){
128-
validSubjectConfirmation = false;
129-
}
130-
}
131-
Node notBefore = childs.item(c).getAttributes().getNamedItem("NotBefore");
132-
if(notBefore != null){
133-
final Calendar notBeforeDate = javax.xml.bind.DatatypeConverter.parseDateTime(notBefore.getNodeValue());
134-
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
135-
if(notBeforeDate.before(now)){
136-
validSubjectConfirmation = false;
137-
}
138-
}
159+
139160
}
140161
}
141162
}
@@ -165,7 +186,7 @@ public boolean isValid(){
165186
}
166187

167188
public String getNameId() throws Exception {
168-
NodeList nodes = xmlDoc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "NameID");
189+
NodeList nodes = document.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "NameID");
169190
if (nodes.getLength() == 0) {
170191
throw new Exception("No name id found in Document.");
171192
}
@@ -182,7 +203,7 @@ public String getAttribute(String name) {
182203

183204
public HashMap getAttributes() {
184205
HashMap<String, ArrayList> attributes = new HashMap<String, ArrayList>();
185-
NodeList nodes = xmlDoc.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Attribute");
206+
NodeList nodes = document.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Attribute");
186207

187208
if (nodes.getLength() != 0) {
188209
for (int i = 0; i < nodes.getLength(); i++) {
@@ -208,7 +229,7 @@ public HashMap getAttributes() {
208229
* @throws $statusExceptionMsg If status is not success
209230
*/
210231
public Map<String, String> checkStatus() throws Exception{
211-
Map<String, String> status = Utils.getStatus(xmlDoc);
232+
Map<String, String> status = Utils.getStatus(document);
212233
if(status.containsKey("code") && !status.get("code").equals(Constants.STATUS_SUCCESS) ){
213234
String statusExceptionMsg = "The status code of the Response was not Success, was " +
214235
status.get("code").substring(status.get("code").lastIndexOf(':') + 1);
@@ -221,6 +242,83 @@ public Map<String, String> checkStatus() throws Exception{
221242
return status;
222243

223244
}
245+
246+
/**
247+
* Verifies that the document only contains a single Assertion (encrypted or not).
248+
*
249+
* @return true if the document passes.
250+
*/
251+
public boolean validateNumAssertions(){
252+
NodeList assertionNodes = this.document.getElementsByTagNameNS("urn:oasis:names:tc:SAML:2.0:assertion", "Assertion");
253+
if(assertionNodes != null && assertionNodes.getLength() == 1)
254+
return true;
255+
return false;
256+
}
257+
258+
/**
259+
* Verifies that the document has the expected signed nodes.
260+
*
261+
* @return true if is valid
262+
*/
263+
public boolean validateSignedElements(ArrayList<String> signedElements){
264+
if(signedElements.size() > 2){
265+
return false;
266+
}
267+
Map<String, Integer> occurrences = new HashMap<String, Integer>();
268+
for(String e:signedElements){
269+
if(occurrences.containsKey(e)){
270+
occurrences.put(e, occurrences.get(e).intValue() + 1);
271+
}else{
272+
occurrences.put(e, 1);
273+
}
274+
}
275+
276+
if((occurrences.containsKey("Response") && occurrences.get("Response") > 1) ||
277+
(occurrences.containsKey("Assertion") && occurrences.get("Assertion") > 1) ||
278+
!occurrences.containsKey("Response") && !occurrences.containsKey("Assertion")
279+
) {
280+
return false;
281+
}
282+
return true;
283+
}
284+
285+
/**
286+
* Verifies that the document is still valid according Conditions Element.
287+
*
288+
* @return true if still valid
289+
*/
290+
public boolean validateTimestamps()
291+
{
292+
NodeList timestampNodes = document.getElementsByTagNameNS("*", "Conditions");
293+
if (timestampNodes.getLength() != 0) {
294+
for (int i = 0; i < timestampNodes.getLength(); i++) {
295+
NamedNodeMap attrName = timestampNodes.item(i).getAttributes();
296+
Node nbAttribute = attrName.getNamedItem("NotBefore");
297+
Node naAttribute = attrName.getNamedItem("NotOnOrAfter");
298+
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
299+
log.debug("now :"+ now.get(Calendar.HOUR_OF_DAY) + ":" + now.get(Calendar.MINUTE)+ ":" + now.get(Calendar.SECOND));
300+
// validate NotOnOrAfter using UTC
301+
if(naAttribute != null){
302+
final Calendar notOnOrAfterDate = javax.xml.bind.DatatypeConverter.parseDateTime(naAttribute.getNodeValue());
303+
log.debug("notOnOrAfterDate :"+ notOnOrAfterDate.get(Calendar.HOUR_OF_DAY) + ":" + notOnOrAfterDate.get(Calendar.MINUTE)+ ":" + notOnOrAfterDate.get(Calendar.SECOND));
304+
if(now.equals(notOnOrAfterDate) || now.after(notOnOrAfterDate)){
305+
return false;
306+
}
307+
}
308+
// validate NotBefore using UTC
309+
if(nbAttribute != null){
310+
final Calendar notBeforeDate = javax.xml.bind.DatatypeConverter.parseDateTime(nbAttribute.getNodeValue());
311+
log.debug("notBeforeDate :"+ notBeforeDate.get(Calendar.HOUR_OF_DAY) + ":" + notBeforeDate.get(Calendar.MINUTE)+ ":" + notBeforeDate.get(Calendar.SECOND));
312+
if(now.before(notBeforeDate)){
313+
return false;
314+
}
315+
}
316+
}
317+
}
318+
return true;
319+
}
320+
321+
224322

225323
private boolean setIdAttributeExists() {
226324
for (Method method : Element.class.getDeclaredMethods()) {
@@ -244,6 +342,6 @@ public String getError() {
244342
return error.toString();
245343
return "";
246344
}
247-
345+
248346

249347
}

0 commit comments

Comments
 (0)