1414import javax .xml .crypto .dsig .dom .DOMValidateContext ;
1515
1616import org .apache .commons .codec .binary .Base64 ;
17+ import org .slf4j .Logger ;
18+ import org .slf4j .LoggerFactory ;
1719import org .w3c .dom .Document ;
1820import org .w3c .dom .Element ;
1921import org .w3c .dom .NamedNodeMap ;
2628
2729public 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