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