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>&lt;samlp:Response&gt;</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>&lt;samlp:Response&gt;</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}