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