001/* 002 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. 003 * 004 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved 005 * 006 * The contents of this file are subject to the terms 007 * of the Common Development and Distribution License 008 * (the License). You may not use this file except in 009 * compliance with the License. 010 * 011 * You can obtain a copy of the License at 012 * https://opensso.dev.java.net/public/CDDLv1.0.html or 013 * opensso/legal/CDDLv1.0.txt 014 * See the License for the specific language governing 015 * permission and limitations under the License. 016 * 017 * When distributing Covered Code, include this CDDL 018 * Header Notice in each file and include the License file 019 * at opensso/legal/CDDLv1.0.txt. 020 * If applicable, add the following below the CDDL Header, 021 * with the fields enclosed by brackets [] replaced by 022 * your own identifying information: 023 * "Portions Copyrighted [year] [name of copyright owner]" 024 * 025 * $Id: Response.java,v 1.3 2009/02/13 04:05:10 bina Exp $ 026 * 027 * Portions Copyrighted 2014-2016 ForgeRock AS. 028 */ 029package com.sun.identity.saml.protocol; 030 031import static org.forgerock.openam.utils.Time.*; 032 033import com.sun.identity.common.SystemConfigurationUtil; 034import com.sun.identity.shared.xml.XMLUtils; 035 036import com.sun.identity.shared.DateUtils; 037 038import com.sun.identity.saml.assertion.Assertion; 039 040import com.sun.identity.saml.common.SAMLConstants; 041import com.sun.identity.saml.common.SAMLException; 042import com.sun.identity.saml.common.SAMLRequesterException; 043import com.sun.identity.saml.common.SAMLRequestVersionTooHighException; 044import com.sun.identity.saml.common.SAMLRequestVersionTooLowException; 045import com.sun.identity.saml.common.SAMLResponderException; 046import com.sun.identity.saml.common.SAMLUtils; 047import com.sun.identity.saml.common.SAMLVersionMismatchException; 048 049import com.sun.identity.saml.xmlsig.XMLSignatureManager; 050 051import java.io.ByteArrayOutputStream; 052import java.io.InputStream; 053 054import java.text.ParseException; 055 056import java.util.ArrayList; 057import java.util.Collections; 058import java.util.Date; 059import java.util.Iterator; 060import java.util.List; 061 062import org.w3c.dom.Document; 063import org.w3c.dom.Element; 064import org.w3c.dom.Node; 065import org.w3c.dom.NodeList; 066 067/** 068 * This <code>Response</code> class represents a Response XML document. 069 * The schema of Response is defined as the following: 070 * 071 * @supported.all.api 072 */ 073public class Response extends AbstractResponse { 074 075 protected Status status = null; 076 protected List assertions = Collections.EMPTY_LIST; 077 protected String xmlString = null; 078 protected String signatureString = null; 079 protected String issuer = null; 080 081 // Response ID attribute name 082 private static final String RESPONSE_ID_ATTRIBUTE = "ResponseID"; 083 084 /** default constructor */ 085 protected Response() {} 086 087 /** 088 * Return whether the signature on the object is valid or not. 089 * @return true if the signature on the object is valid; false otherwise. 090 */ 091 public boolean isSignatureValid() { 092 if (signed & ! validationDone) { 093 valid = SAMLUtils.checkSignatureValid( 094 xmlString, RESPONSE_ID_ATTRIBUTE, issuer); 095 096 validationDone = true; 097 } 098 return valid; 099 } 100 101 /** 102 * Method that signs the Response. 103 * 104 * @exception SAMLException if could not sign the Response. 105 */ 106 public void signXML() throws SAMLException { 107 if (signed) { 108 if (SAMLUtils.debug.messageEnabled()) { 109 SAMLUtils.debug.message("Response.signXML: the response is " 110 + "already signed."); 111 } 112 throw new SAMLException( 113 SAMLUtils.bundle.getString("alreadySigned")); 114 } 115 String certAlias = 116 SystemConfigurationUtil.getProperty( 117 "com.sun.identity.saml.xmlsig.certalias"); 118 if (certAlias == null) { 119 if (SAMLUtils.debug.messageEnabled()) { 120 SAMLUtils.debug.message("Response.signXML: couldn't obtain " 121 + "this site's cert alias."); 122 } 123 throw new SAMLResponderException( 124 SAMLUtils.bundle.getString("cannotFindCertAlias")); 125 } 126 XMLSignatureManager manager = XMLSignatureManager.getInstance(); 127 if ((majorVersion == 1) && (minorVersion == 0)) { 128 SAMLUtils.debug.message("Request.signXML: sign with version 1.0"); 129 signatureString = manager.signXML(this.toString(true, true), 130 certAlias); 131 // this block is used for later return of signature element by 132 // getSignature() method 133 signature = 134 XMLUtils.toDOMDocument(signatureString, SAMLUtils.debug) 135 .getDocumentElement(); 136 } else { 137 Document doc = XMLUtils.toDOMDocument(this.toString(true, true), 138 SAMLUtils.debug); 139 // sign with SAML 1.1 spec & include cert in KeyInfo 140 signature = manager.signXML(doc, certAlias, null, 141 RESPONSE_ID_ATTRIBUTE, getResponseID(), true, null); 142 signatureString = XMLUtils.print(signature); 143 } 144 signed = true; 145 xmlString = this.toString(true, true); 146 } 147 148 private void buildResponse(String responseID, 149 String inResponseTo, 150 Status status, 151 String recipient, 152 List contents) throws SAMLException 153 { 154 if ((responseID == null) || (responseID.length() == 0)) { 155 // generate one 156 this.responseID = SAMLUtils.generateID(); 157 if (this.responseID == null) { 158 throw new SAMLRequesterException( 159 SAMLUtils.bundle.getString("errorGenerateID")); 160 } 161 } else { 162 this.responseID = responseID; 163 } 164 165 this.inResponseTo = inResponseTo; 166 167 this.recipient = recipient; 168 169 issueInstant = newDate(); 170 171 if (status == null) { 172 SAMLUtils.debug.message("Response: missing <Status>."); 173 throw new SAMLRequesterException( 174 SAMLUtils.bundle.getString("missingElement")); 175 } 176 this.status = status; 177 178 if ((contents != null) && 179 (contents != Collections.EMPTY_LIST)) { 180 int length = contents.size(); 181 for (int i = 0; i < length; i++) { 182 Object temp = contents.get(i); 183 if (!(temp instanceof Assertion)) { 184 if (SAMLUtils.debug.messageEnabled()) { 185 SAMLUtils.debug.message("Response: Wrong input " 186 + "for Assertion."); 187 } 188 throw new SAMLRequesterException( 189 SAMLUtils.bundle.getString("wrongInput")); 190 } 191 } 192 assertions = contents; 193 } 194 } 195 196 /** 197 * This constructor shall only be used at the server side to construct 198 * a Response object. 199 * NOTE: The content here is just the body for the Response. The 200 * constructor will add the unique <code>ResponseID</code>, 201 * <code>MajorVersion</code>, etc. to form a complete Response object. 202 * 203 * @param responseID If it's null, the constructor will create one. 204 * @param inResponseTo the <code>RequestID</code> that this response is 205 * corresponding. It could be null or empty string "". 206 * @param status The status of the response. 207 * @param contents A List of Assertions that are the content of the 208 * Response. It could be null when there is no Assertion. 209 * @throws SAMLException if error occurs. 210 */ 211 public Response(String responseID, 212 String inResponseTo, 213 Status status, 214 List contents) throws SAMLException 215 { 216 buildResponse(responseID, inResponseTo, status, null, contents); 217 } 218 219 /** 220 * This constructor shall only be used at the server side to construct 221 * a Response object. 222 * NOTE: The content here is just the body for the Response. The 223 * constructor will add the unique <code>ResponseID</code>, 224 * <code>MajorVersion</code>, etc. to form a complete Response object. 225 * 226 * @param responseID If it's null, the constructor will create one. 227 * @param inResponseTo the <code>RequestID</code> that this response is 228 * corresponding. It could be null or empty string "". 229 * @param status The status of the response. 230 * @param recipient The intended recipient of the response. It could be 231 * null or empty string since it's optional. 232 * @param contents A List of Assertions that are the content of the 233 * Response. It could be null when there is no Assertion. 234 * @throws SAMLException if error occurs. 235 */ 236 public Response(String responseID, 237 String inResponseTo, 238 Status status, 239 String recipient, 240 List contents) throws SAMLException 241 { 242 buildResponse(responseID, inResponseTo, status, recipient, contents); 243 } 244 245 /** 246 * This constructor shall only be used at the server side to construct 247 * a Response object. 248 * NOTE: The content here is just the body for the Response. The 249 * constructor will add the unique <code>ResponseID</code>, 250 * <code>MajorVersion</code>, etc. to form a complete Response object. 251 * 252 * @param responseID If it's null, the constructor will create one. 253 * @param status The status of the response. 254 * @param recipient The intended recipient of the response. It could be 255 * null or empty string since it's optional. 256 * @param contents A List of Assertions that are the content of the 257 * Response. It could be null when there is no Assertion. 258 * @throws SAMLException if error occurs. 259 */ 260 public Response(String responseID, 261 Status status, 262 String recipient, 263 List contents) throws SAMLException 264 { 265 buildResponse(responseID, null, status, recipient, contents); 266 } 267 268 /** 269 * This constructor shall only be used at the server side to construct 270 * a Response object. 271 * NOTE: The content here is just the body for the Response. The 272 * constructor will add the unique <code>ResponseID</code>, 273 * <code>MajorVersion</code>, etc. to form a complete Response object. 274 * 275 * @param responseID If it's null, the constructor will create one. 276 * @param status The status of the response. 277 * @param contents A List of Assertions that are the content of the 278 * Response. It could be null when there is no Assertion. 279 * @throws SAMLException if error occurs. 280 */ 281 public Response(String responseID, 282 Status status, 283 List contents) throws SAMLException 284 { 285 buildResponse(responseID, null, status, null, contents); 286 } 287 288 /** 289 * Returns Response object based on the XML document received from server. 290 * This method is used primarily at the client side. The schema of the XML 291 * document is describe above. 292 * 293 * @param xml The Response XML document String. 294 * NOTE: this is a complete SAML response XML string with 295 * <code>ResponseID</code>, <code>MajorVersion</code>, etc. 296 * @return Response object based on the XML document received from server. 297 * @exception SAMLException if XML parsing failed 298 */ 299 public static Response parseXML(String xml) throws SAMLException { 300 // parse the xml string 301 Document doc = XMLUtils.toDOMDocument(xml, SAMLUtils.debug); 302 Element root = doc.getDocumentElement(); 303 304 return new Response(root); 305 } 306 307 /** 308 * Returns Response object based on the XML document received from server. 309 * This method is used primarily at the client side. The schema of the XML 310 * document is describe above. 311 * 312 * @param is The Response XML <code>InputStream</code>. 313 * NOTE: The <code>InputStream</code> contains a complete 314 * SAML response with 315 * <code>ResponseID</code>, <code>MajorVersion</code>, etc. 316 * @return Response object based on the XML document received from server. 317 * @exception SAMLException if XML parsing failed 318 */ 319 public static Response parseXML(InputStream is) throws SAMLException { 320 Document doc = XMLUtils.toDOMDocument(is, SAMLUtils.debug); 321 Element root = doc.getDocumentElement(); 322 323 return new Response(root); 324 } 325 326 /** 327 * Constructor. 328 * 329 * @param root <code>Response</code> element 330 * @throws SAMLException if error occurs. 331 */ 332 public Response(Element root) throws SAMLException { 333 // Make sure this is a Response 334 if (root == null) { 335 SAMLUtils.debug.message("Response(Element): null input."); 336 throw new SAMLRequesterException( 337 SAMLUtils.bundle.getString("nullInput")); 338 } 339 String tag = null; 340 if (((tag = root.getLocalName()) == null) || 341 (!tag.equals("Response"))) { 342 SAMLUtils.debug.message("Response(Element): wrong input."); 343 throw new SAMLRequesterException( 344 SAMLUtils.bundle.getString("wrongInput")); 345 } 346 347 List signs = XMLUtils.getElementsByTagNameNS1(root, 348 SAMLConstants.XMLSIG_NAMESPACE_URI, 349 SAMLConstants.XMLSIG_ELEMENT_NAME); 350 int signsSize = signs.size(); 351 if (signsSize == 1) { 352 xmlString = XMLUtils.print(root); 353 signed = true; 354 } else if (signsSize != 0) { 355 if (SAMLUtils.debug.messageEnabled()) { 356 SAMLUtils.debug.message("Response(Element): included more than" 357 + " one Signature element."); 358 } 359 throw new SAMLRequesterException( 360 SAMLUtils.bundle.getString("moreElement")); 361 } 362 363 // Attribute ResponseID 364 responseID = root.getAttribute("ResponseID"); 365 if ((responseID == null) || (responseID.length() == 0)) { 366 if (SAMLUtils.debug.messageEnabled()) { 367 SAMLUtils.debug.message("Response.parseXML: " 368 + "Reponse doesn't have ResponseID."); 369 } 370 throw new SAMLRequesterException( 371 SAMLUtils.bundle.getString("missingAttribute")); 372 } 373 374 // Attribute InResponseTo 375 if (root.hasAttribute("InResponseTo")) { 376 inResponseTo = root.getAttribute("InResponseTo"); 377 } 378 379 // Attribute MajorVersion 380 parseMajorVersion(root.getAttribute("MajorVersion")); 381 382 parseMinorVersion(root.getAttribute("MinorVersion")); 383 384 if (root.hasAttribute("Recipient")) { 385 recipient = root.getAttribute("Recipient"); 386 } 387 388 // Attribute IssueInstant 389 String instantString = root.getAttribute("IssueInstant"); 390 if ((instantString == null) || (instantString.length() == 0)) { 391 SAMLUtils.debug.message("Response(Element): missing IssueInstant"); 392 throw new SAMLRequesterException( 393 SAMLUtils.bundle.getString("missingAttribute")); 394 } else { 395 try { 396 issueInstant = DateUtils.stringToDate(instantString); 397 } catch (ParseException e) { 398 SAMLUtils.debug.message( 399 "Resposne(Element): could not parse IssueInstant", e); 400 throw new SAMLRequesterException(SAMLUtils.bundle.getString( 401 "wrongInput")); 402 } 403 } 404 405 NodeList nl = root.getChildNodes(); 406 Node child; 407 String childName; 408 int length = nl.getLength(); 409 for (int i = 0; i < length; i++) { 410 child = nl.item(i); 411 if ((childName = child.getLocalName()) != null) { 412 if (childName.equals("Signature")) { 413 signature = (Element) child; 414 } else if (childName.equals("Status")) { 415 if (status != null) { 416 if (SAMLUtils.debug.messageEnabled()) { 417 SAMLUtils.debug.message("Response: included more" 418 + " than one <Status>"); 419 } 420 throw new SAMLRequesterException( 421 SAMLUtils.bundle.getString("moreElement")); 422 } 423 status = new Status((Element) child); 424 } else if (childName.equals("Assertion")) { 425 if (assertions == Collections.EMPTY_LIST) { 426 assertions = new ArrayList(); 427 } 428 Element canoEle = SAMLUtils.getCanonicalElement(child); 429 if (canoEle == null) { 430 throw new SAMLRequesterException( 431 SAMLUtils.bundle.getString("errorCanonical")); 432 } 433 434 Assertion oneAssertion= new Assertion(canoEle); 435 issuer = oneAssertion.getIssuer(); 436 assertions.add(oneAssertion); 437 } else { 438 if (SAMLUtils.debug.messageEnabled()) { 439 SAMLUtils.debug.message("Response: included wrong " 440 + "element:" + childName); 441 } 442 throw new SAMLRequesterException( 443 SAMLUtils.bundle.getString("wrongInput")); 444 } 445 } // end if childName != null 446 } // end for loop 447 448 if (status == null) { 449 SAMLUtils.debug.message("Response: missing element <Status>."); 450 throw new SAMLRequesterException( 451 SAMLUtils.bundle.getString("oneElement")); 452 } 453 } 454 455 /** 456 * Parse the input and set the majorVersion accordingly. 457 * @param majorVer a String representing the MajorVersion to be set. 458 * @exception SAMLException when the version mismatchs. 459 */ 460 private void parseMajorVersion(String majorVer) throws SAMLException { 461 try { 462 majorVersion = Integer.parseInt(majorVer); 463 } catch (NumberFormatException e) { 464 if (SAMLUtils.debug.messageEnabled()) { 465 SAMLUtils.debug.message("Response(Element): invalid " 466 + "MajorVersion", e); 467 } 468 throw new SAMLRequesterException( 469 SAMLUtils.bundle.getString("wrongInput")); 470 } 471 472 if (majorVersion != SAMLConstants.PROTOCOL_MAJOR_VERSION) { 473 if (majorVersion > SAMLConstants.PROTOCOL_MAJOR_VERSION) { 474 if (SAMLUtils.debug.messageEnabled()) { 475 SAMLUtils.debug.message("Response(Element):MajorVersion of" 476 + " the Response is too high."); 477 } 478 throw new SAMLVersionMismatchException( 479 SAMLUtils.bundle.getString("responseVersionTooHigh")); 480 } else { 481 if (SAMLUtils.debug.messageEnabled()) { 482 SAMLUtils.debug.message("Response(Element):MajorVersion of" 483 + " the Response is too low."); 484 } 485 throw new SAMLVersionMismatchException( 486 SAMLUtils.bundle.getString("responseVersionTooLow")); 487 } 488 } 489 } 490 491 /** 492 * Parse the input and set the minorVersion accordingly. 493 * @param minorVer a String representing the MinorVersion to be set. 494 * @exception SAMLException when the version mismatchs. 495 */ 496 private void parseMinorVersion(String minorVer) throws SAMLException { 497 try { 498 minorVersion = Integer.parseInt(minorVer); 499 } catch (NumberFormatException e) { 500 if (SAMLUtils.debug.messageEnabled()) { 501 SAMLUtils.debug.message("Response(Element): invalid " 502 + "MinorVersion", e); 503 } 504 throw new SAMLRequesterException( 505 SAMLUtils.bundle.getString("wrongInput")); 506 } 507 508 if (minorVersion > SAMLConstants.PROTOCOL_MINOR_VERSION_ONE) { 509 if (SAMLUtils.debug.messageEnabled()) { 510 SAMLUtils.debug.message("Response(Element): MinorVersion" 511 + " of the Response is too high."); 512 } 513 throw new SAMLRequestVersionTooHighException( 514 SAMLUtils.bundle.getString("responseVersionTooHigh")); 515 } else if (minorVersion < SAMLConstants.PROTOCOL_MINOR_VERSION_ZERO) { 516 if (SAMLUtils.debug.messageEnabled()) { 517 SAMLUtils.debug.message("Response(Element): MinorVersion" 518 + " of the Response is too low."); 519 } 520 throw new SAMLRequestVersionTooLowException( 521 SAMLUtils.bundle.getString("responseVersionTooLow")); 522 } 523 } 524 525 /** 526 * This method returns the set of Assertions that is the content of 527 * the response. 528 * @return The set of Assertions that is the content of the response. 529 * It could be Collections.EMPTY_LIST when there is no Assertion 530 * in the response. 531 */ 532 public List getAssertion() { 533 return assertions; 534 } 535 536 /** 537 * Add an assertion to the Response. 538 * @param assertion The assertion to be added. 539 * @return A boolean value: true if the operation is successful; 540 * false otherwise. 541 */ 542 public boolean addAssertion(Assertion assertion) { 543 if (signed) { 544 return false; 545 } 546 if (assertion == null) { 547 return false; 548 } 549 if ((assertions == null) || (assertions == Collections.EMPTY_LIST)) { 550 assertions = new ArrayList(); 551 } 552 assertions.add(assertion); 553 return true; 554 } 555 556 /** 557 * Gets the Status of the Response. 558 * @return The Status of the response. 559 */ 560 public Status getStatus() { 561 return status; 562 } 563 564 /** 565 * Set the Status of the Response. 566 * 567 * @param status The Status of the Response to be set. 568 * @return true if the operation is successful. 569 */ 570 public boolean setStatus(Status status) { 571 if (signed) { 572 return false; 573 } 574 if (status == null) { 575 return false; 576 } 577 this.status = status; 578 return true; 579 } 580 581 /** 582 * Set the signature for the Response. 583 * @param elem ds:Signature element 584 * @return A boolean value: true if the operation succeeds; false otherwise. 585 */ 586 public boolean setSignature(Element elem) { 587 signatureString = XMLUtils.print(elem); 588 return super.setSignature(elem); 589 } 590 591 /** 592 * This method translates the response to an XML document String based on 593 * the Response schema described above. 594 * @return An XML String representing the response. NOTE: this is a 595 * complete SAML response XML string with <code>ResponseID</code>, 596 * <code>MajorVersion</code>, etc. 597 */ 598 public String toString() { 599 return this.toString(true, true); 600 } 601 602 /** 603 * Creates a String representation of the 604 * <code><samlp:Response></code> element. 605 * 606 * @param includeNS Determines whether or not the namespace qualifier 607 * is prepended to the Element when converted 608 * @param declareNS Determines whether or not the namespace is declared 609 * within the Element. 610 * @return A string containing the valid XML for this element 611 */ 612 public String toString(boolean includeNS, boolean declareNS) { 613 return toString(includeNS, declareNS, false); 614 } 615 616 /** 617 * Creates a String representation of the 618 * <code><samlp:Response></code> element. 619 * 620 * @param includeNS Determines whether or not the namespace qualifier 621 * is prepended to the Element when converted 622 * @param declareNS Determines whether or not the namespace is declared 623 * within the Element. 624 * @param includeHeader Determines whether the output include the XML 625 * declaration header. 626 * @return A string containing the valid XML for this element 627 */ 628 public String toString(boolean includeNS, 629 boolean declareNS, 630 boolean includeHeader) { 631 if (signed && (xmlString != null)) { 632 return xmlString; 633 } 634 635 StringBuffer xml = new StringBuffer(300); 636 if (includeHeader) { 637 xml.append("<?xml version=\"1.0\" encoding=\""). 638 append(SAMLConstants.DEFAULT_ENCODING).append("\" ?>\n"); 639 } 640 String prefix = ""; 641 String uri = ""; 642 if (includeNS) { 643 prefix = SAMLConstants.PROTOCOL_PREFIX; 644 } 645 646 if (declareNS) { 647 uri = SAMLConstants.PROTOCOL_NAMESPACE_STRING; 648 } 649 650 String instantString = DateUtils.toUTCDateFormat(issueInstant); 651 652 xml.append("<").append(prefix).append("Response").append(uri). 653 append(" ResponseID=\"").append(responseID).append("\""); 654 if (inResponseTo != null) { 655 xml.append(" InResponseTo=\"").append(inResponseTo).append("\""); 656 } 657 xml.append(" MajorVersion=\"").append(majorVersion).append("\""). 658 append(" MinorVersion=\"").append(minorVersion).append("\""). 659 append(" IssueInstant=\"").append(instantString).append("\""); 660 if (recipient != null) { 661 xml.append(" Recipient=\"").append(XMLUtils.escapeSpecialCharacters(recipient)).append("\""); 662 } 663 xml.append(">\n"); 664 665 if (signed) { 666 if (signatureString != null) { 667 xml.append(signatureString); 668 } else if (signature != null) { 669 signatureString = XMLUtils.print(signature); 670 xml.append(signatureString); 671 } 672 } 673 674 xml.append(status.toString(includeNS, false)); 675 if ((assertions != null) && (assertions != Collections.EMPTY_LIST)) { 676 Iterator j = assertions.iterator(); 677 while (j.hasNext()) { 678 xml.append(((Assertion) j.next()).toString(true, true)); 679 } 680 } 681 682 xml.append("</").append(prefix).append("Response>\n"); 683 return xml.toString(); 684 } 685}