encode the failure response for saml 2 authnreq
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / profile / saml2 / AbstractAuthenticationRequest.java
1 /*
2  * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package edu.internet2.middleware.shibboleth.idp.profile.saml2;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.net.URLConnection;
24 import java.util.LinkedList;
25 import java.util.List;
26
27 import javax.servlet.RequestDispatcher;
28 import javax.servlet.ServletResponse;
29 import javax.servlet.http.HttpServletRequest;
30 import javax.servlet.http.HttpServletResponse;
31 import javax.servlet.http.HttpSession;
32
33 import org.apache.log4j.Logger;
34 import org.joda.time.DateTime;
35 import org.opensaml.Configuration;
36 import org.opensaml.common.SAMLObjectBuilder;
37 import org.opensaml.common.binding.BindingException;
38 import org.opensaml.common.binding.encoding.MessageEncoder;
39 import org.opensaml.saml2.core.Assertion;
40 import org.opensaml.saml2.core.AuthnContext;
41 import org.opensaml.saml2.core.AuthnContextClassRef;
42 import org.opensaml.saml2.core.AuthnContextDeclRef;
43 import org.opensaml.saml2.core.AuthnRequest;
44 import org.opensaml.saml2.core.AuthnStatement;
45 import org.opensaml.saml2.core.GetComplete;
46 import org.opensaml.saml2.core.IDPEntry;
47 import org.opensaml.saml2.core.IDPList;
48 import org.opensaml.saml2.core.Issuer;
49 import org.opensaml.saml2.core.RequestedAuthnContext;
50 import org.opensaml.saml2.core.Response;
51 import org.opensaml.saml2.core.Scoping;
52 import org.opensaml.saml2.core.Status;
53 import org.opensaml.saml2.core.StatusCode;
54 import org.opensaml.saml2.core.Subject;
55 import org.opensaml.saml2.core.SubjectConfirmation;
56 import org.opensaml.saml2.metadata.AssertionConsumerService;
57 import org.opensaml.saml2.metadata.SPSSODescriptor;
58 import org.opensaml.saml2.metadata.provider.MetadataProvider;
59 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
60 import org.opensaml.xml.io.Unmarshaller;
61 import org.opensaml.xml.io.UnmarshallingException;
62 import org.opensaml.xml.parse.BasicParserPool;
63 import org.opensaml.xml.parse.ParserPool;
64 import org.opensaml.xml.parse.XMLParserException;
65 import org.w3c.dom.Document;
66 import org.w3c.dom.Element;
67
68
69 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
70 import edu.internet2.middleware.shibboleth.common.profile.ProfileResponse;
71 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
72 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.AbstractSAML2ProfileConfiguration;
73 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.SSOConfiguration;
74 import edu.internet2.middleware.shibboleth.idp.authn.AuthenticationManager;
75 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
76 import edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext;
77 import javax.servlet.ServletException;
78
79
80 /**
81  * Abstract SAML 2.0 Authentication Request profile handler.
82  */
83 public abstract class AbstractAuthenticationRequest extends AbstractSAML2ProfileHandler {
84     
85     /** Class logger. */
86     private static final Logger log = Logger.getLogger(AbstractAuthenticationRequest.class);
87     
88     /** Key in an HttpSession for the AssertionConsumerService object. */
89     protected static final String ACS_SESSION_KEY = "AssertionConsumerService";
90     
91     /** Key in an HttpSession for the RelyingParty. */
92     protected static final String RPCONFIG_SESSION_KEY = "RelyingPartyConfiguration";
93     
94     /** Key in an HttpSession for the SSOConfiguration. */
95     protected static final String SSOCONFIG_SESSION_KEY = "SSOConfiguration";
96     
97     /** Key in an HttpSession for the SPSSODescriptor. */
98     protected static final String SPSSODESC_SESSION_KEY = "SPSSODescriptor";
99     
100     /** Key in an HttpSession for the AuthnRequest. */
101     protected static final String AUTHNREQUEST_SESSION_KEY = "AuthnRequest";
102     
103     /** Key in an HttpSession for the Issuer. */
104     protected static final String ISSUER_SESSION_KEY = "Issuer";
105     
106     
107     /** The path to the IdP's AuthenticationManager servlet */
108     protected String authnMgrURL;
109     
110     /** AuthenticationManager to be used */
111     protected AuthenticationManager authnMgr;
112     
113     /** A pool of XML parsers. */
114     protected ParserPool parserPool;
115     
116     /** Builder for AuthnStatements. */
117     protected SAMLObjectBuilder<AuthnStatement> authnStatementBuilder;
118     
119     /** Builder for AuthnContexts. */
120     protected SAMLObjectBuilder<AuthnContext> authnContextBuilder;
121     
122     /** Builder for AuthnContextDeclRef's */
123     protected SAMLObjectBuilder<AuthnContextDeclRef> authnContextDeclRefBuilder;
124     
125     /** Builder for AuthnContextClassRef's. */
126     protected SAMLObjectBuilder<AuthnContextClassRef> authnContextClassRefBuilder;
127     
128     /**
129      * Constructor.
130      */
131     public AbstractAuthenticationRequest() {
132         
133         parserPool = new BasicParserPool();
134         authnStatementBuilder = (SAMLObjectBuilder<AuthnStatement>) getBuilderFactory().getBuilder(AuthnStatement.DEFAULT_ELEMENT_NAME);
135         authnContextBuilder = (SAMLObjectBuilder<AuthnContext>) getBuilderFactory().getBuilder(AuthnContext.DEFAULT_ELEMENT_NAME);
136         authnContextDeclRefBuilder = (SAMLObjectBuilder<AuthnContextDeclRef>) getBuilderFactory().getBuilder(AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
137         authnContextClassRefBuilder = (SAMLObjectBuilder<AuthnContextClassRef>) getBuilderFactory().getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
138     }
139     
140     /**
141      * Set the Authentication Mananger.
142      *
143      * @param authnManager
144      *            The IdP's AuthenticationManager.
145      */
146     public void setAuthenticationManager(AuthenticationManager authnManager) {
147         this.authnMgr = authnMgr;
148     }
149     
150     /**
151      * Evaluate a SAML 2 AuthenticationRequest message.
152      *
153      * @param authnRequest
154      *            A SAML 2 AuthenticationRequest
155      * @param issuer
156      *            The issuer of the authnRequest.
157      * @param session
158      *            The HttpSession of the request.
159      * @param relyingParty
160      *            The RelyingPartyConfiguration for the request.
161      * @param ssoConfig
162      *            The SSOConfiguration for the request.
163      * @param spDescriptor
164      *            The SPSSODescriptor for the request.
165      *
166      * @return A Response containing a failure message or a AuthenticationStmt.
167      *
168      * @throws ProfileException
169      *             On Error.
170      */
171     protected Response evaluateRequest(final AuthnRequest authnRequest,
172             String issuer, final HttpSession session,
173             final RelyingPartyConfiguration relyingParty,
174             final SSOConfiguration ssoConfig, final SPSSODescriptor spDescriptor)
175             throws ProfileException {
176         
177         Response samlResponse;
178         
179         // check if the authentication was successful.
180         Saml2LoginContext loginCtx = getLoginContext(session);
181         if (!loginCtx.getAuthenticationOK()) {
182             // if authentication failed, send the appropriate SAML error message.
183             String failureMessage = loginCtx.getAuthenticationFailureMessage();
184             Status failureStatus = buildStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI, failureMessage);
185             samlResponse = buildResponse(authnRequest.getID(), new DateTime(), relyingParty.getProviderId(),
186                     failureStatus);
187             
188             return samlResponse;
189         }
190         
191         // the user successfully authenticated.
192         // build an authentication assertion.
193         samlResponse = buildResponse(authnRequest.getID(), new DateTime(),
194                 relyingParty.getProviderId(), buildStatus(StatusCode.SUCCESS_URI, null, null));
195         
196         DateTime now = new DateTime();
197         Assertion assertion = buildAssertion(now, relyingParty,
198                 (AbstractSAML2ProfileConfiguration) relyingParty.getProfileConfigurations().get(SSOConfiguration.PROFILE_ID));
199         assertion.setSubject(authnRequest.getSubject());
200         setAuthenticationStatement(assertion, loginCtx, authnRequest);
201         
202         samlResponse.getAssertions().add(assertion);
203         
204         // retrieve the AssertionConsumerService endpoint (we parsed it in
205         // verifyAuthnRequest()
206         AssertionConsumerService acsEndpoint = getACSEndpointFromSession(session);
207         
208         
209         
210         return samlResponse;
211     }
212     
213     /**
214      * Build a SAML 2 Response element with basic fields populated.
215      *
216      * Failure handlers can send the returned response element to the RP.
217      * Success handlers should add the assertions before sending it.
218      *
219      * @param inResponseTo
220      *            The ID of the request this is in response to.
221      * @param issueInstant
222      *            The timestamp of this response.
223      * @param issuer
224      *            The URI of the RP issuing the response.
225      * @param status
226      *            The response's status code.
227      *
228      * @return The populated Response object.
229      */
230     protected Response buildResponse(String inResponseTo,
231             final DateTime issueInstant, String issuer, final Status status) {
232         
233         Response response = getResponseBuilder().buildObject();
234         
235         Issuer i = getIssuerBuilder().buildObject();
236         i.setValue(issuer);
237         
238         response.setVersion(SAML_VERSION);
239         response.setID(getIdGenerator().generateIdentifier());
240         response.setInResponseTo(inResponseTo);
241         response.setIssueInstant(issueInstant);
242         response.setIssuer(i);
243         response.setStatus(status);
244         
245         return response;
246     }
247     
248     /**
249      * Check if the user has already been authenticated.
250      *
251      * @param httpSession
252      *            the user's HttpSession.
253      *
254      * @return <code>true</code> if the user has been authenticated. otherwise
255      *         <code>false</code>
256      */
257     protected boolean hasUserAuthenticated(final HttpSession httpSession) {
258         
259         // if the user has authenticated, their session will have a LoginContext
260         
261         Object o = httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
262         return (o != null && o instanceof LoginContext);
263     }
264     
265     /**
266      * Store a user's AuthnRequest and Issuer in the session.
267      *
268      * @param authnRequest
269      *            A SAML 2 AuthnRequest.
270      * @param issuer
271      *            The issuer of the AuthnRequest.
272      * @param session
273      *            The HttpSession in which the data should be stored.
274      * @param relyingParty
275      *            The RelyingPartyConfiguration for the issuer.
276      * @param ssoConfig
277      *            The SSOConfiguration for the relyingParty
278      * @param spDescriptor
279      *            The SPSSODescriptor for the ssoConfig.
280      */
281     protected void storeRequestData(final HttpSession session,
282             final AuthnRequest authnRequest, String issuer,
283             final RelyingPartyConfiguration relyingParty,
284             final SSOConfiguration ssoConfig, final SPSSODescriptor spDescriptor) {
285         
286         if (session == null) {
287             return;
288         }
289         
290         session.setAttribute(AUTHNREQUEST_SESSION_KEY, authnRequest);
291         session.setAttribute(ISSUER_SESSION_KEY, issuer);
292         session.setAttribute(RPCONFIG_SESSION_KEY, relyingParty);
293         session.setAttribute(SSOCONFIG_SESSION_KEY, ssoConfig);
294         session.setAttribute(SPSSODESC_SESSION_KEY, spDescriptor);
295     }
296     
297     /**
298      * Retrieve the AuthnRequest and Issuer from a session.
299      *
300      * @param session
301      *            The HttpSession in which the data was stored.
302      * @param authnRequest
303      *            Will be populated with the AuthnRequest.
304      * @param issuer
305      *            Will be populated with the ssuer of the AuthnRequest.
306      * @param relyingParty
307      *            Will be populated with the RelyingPartyConfiguration for the
308      *            issuer.
309      * @param ssoConfig
310      *            Will be populated with the SSOConfiguration for the
311      *            relyingParty
312      * @param spDescriptor
313      *            Will be populated with the SPSSODescriptor for the ssoConfig.
314      */
315     protected void retrieveRequestData(final HttpSession session,
316             AuthnRequest authnRequest, String issuer,
317             RelyingPartyConfiguration relyingParty, SSOConfiguration ssoConfig,
318             SPSSODescriptor spDescriptor) {
319         
320         if (session == null) {
321             authnRequest = null;
322             issuer = null;
323         }
324         
325         authnRequest = (AuthnRequest) session
326                 .getAttribute(AUTHNREQUEST_SESSION_KEY);
327         issuer = (String) session.getAttribute(ISSUER_SESSION_KEY);
328         relyingParty = (RelyingPartyConfiguration) session
329                 .getAttribute(RPCONFIG_SESSION_KEY);
330         ssoConfig = (SSOConfiguration) session
331                 .getAttribute(SSOCONFIG_SESSION_KEY);
332         spDescriptor = (SPSSODescriptor) session
333                 .getAttribute(SPSSODESC_SESSION_KEY);
334         
335         session.removeAttribute(AUTHNREQUEST_SESSION_KEY);
336         session.removeAttribute(ISSUER_SESSION_KEY);
337         session.removeAttribute(RPCONFIG_SESSION_KEY);
338         session.removeAttribute(SSOCONFIG_SESSION_KEY);
339         session.removeAttribute(SPSSODESC_SESSION_KEY);
340     }
341     
342     /**
343      * Check if the user has already been authenticated. If so, return the
344      * LoginContext. If not, redirect the user to the AuthenticationManager.
345      *
346      * @param authnRequest
347      *            The SAML 2 AuthnRequest.
348      * @param httpSession
349      *            The user's HttpSession.
350      * @param request
351      *            The user's HttpServletRequest.
352      * @param response
353      *            The user's HttpServletResponse.
354      *
355      * @throws ProfileException
356      *             on error.
357      */
358     protected void authenticateUser(final AuthnRequest authnRequest,
359             final HttpSession httpSession, final HttpServletRequest request,
360             final HttpServletResponse response) throws ProfileException {
361         
362         // Forward the request to the AuthenticationManager.
363         // When the AuthenticationManager is done it will
364         // forward the request back to this servlet.
365         
366         Saml2LoginContext loginCtx = new Saml2LoginContext(authnRequest);
367         loginCtx.setProfileHandlerURL(request.getRequestURI());
368         httpSession.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginCtx);
369         try {
370             RequestDispatcher dispatcher = request
371                     .getRequestDispatcher(authnMgrURL);
372             dispatcher.forward(request, response);
373         } catch (IOException ex) {
374             log.error("Error forwarding SAML 2 AuthnRequest "
375                     + authnRequest.getID() + " to AuthenticationManager", ex);
376             throw new ProfileException("Error forwarding SAML 2 AuthnRequest "
377                     + authnRequest.getID() + " to AuthenticationManager", ex);
378         } catch (ServletException ex) {
379             log.error("Error forwarding SAML 2 AuthnRequest "
380                     + authnRequest.getID() + " to AuthenticationManager", ex);
381             throw new ProfileException("Error forwarding SAML 2 AuthnRequest "
382                     + authnRequest.getID() + " to AuthenticationManager", ex);
383         }
384     }
385     
386     /**
387      * Build an AuthnStatement and add it to an Assertion.
388      *
389      * @param assertion An empty SAML 2 Assertion object.
390      * @param loginContext The processed login context for the AuthnRequest.
391      * @param authnRequest The AuthnRequest to which this is in response.
392      *
393      * @throws ProfileException On error.
394      */
395     protected void setAuthenticationStatement(Assertion assertion,
396             final Saml2LoginContext loginContext,
397             final AuthnRequest authnRequest) throws ProfileException {
398         
399         // Build the AuthnCtx.
400         // We need to determine if the user was authenticated
401         // with an AuthnContextClassRef or a AuthnContextDeclRef
402         AuthnContext authnCtx = buildAuthnCtx(authnRequest.getRequestedAuthnContext(), loginContext);
403         if (authnCtx == null) {
404             log.error("Error respond to SAML 2 AuthnRequest "
405                     + authnRequest.getID()
406                     + " : Unable to determine authentication method");
407         }
408         
409         AuthnStatement stmt = authnStatementBuilder.buildObject();
410         stmt.setAuthnInstant(loginContext.getAuthenticationInstant());
411         stmt.setAuthnContext(authnCtx);
412         
413         // add the AuthnStatement to the Assertion
414         List<AuthnStatement> authnStatements = assertion.getAuthnStatements();
415         authnStatements.add(stmt);
416     }
417     
418     /**
419      * Create the AuthnContex object.
420      *
421      * To do this, we have to walk the AuthnRequest's RequestedAuthnContext
422      * object and compare any values we find to what's set in the loginContext.
423      *
424      * @param requestedAuthnCtx
425      *            The RequestedAuthnContext from the Authentication Request.
426      * @param loginContext
427      *            The processed LoginContext (it must contain the authn method).
428      *
429      * @return An AuthnCtx object on success or <code>null</code> on failure.
430      */
431     protected AuthnContext buildAuthnCtx(
432             final RequestedAuthnContext requestedAuthnCtx,
433             final Saml2LoginContext loginContext) {
434         
435         // this method assumes that only one URI will match.
436         
437         AuthnContext authnCtx = authnContextBuilder.buildObject();
438         String authnMethod = loginContext.getAuthenticationMethod();
439         
440         List<AuthnContextClassRef> authnClasses = requestedAuthnCtx
441                 .getAuthnContextClassRefs();
442         List<AuthnContextDeclRef> authnDeclRefs = requestedAuthnCtx
443                 .getAuthnContextDeclRefs();
444         
445         if (authnClasses != null) {
446             for (AuthnContextClassRef classRef : authnClasses) {
447                 if (classRef != null) {
448                     String s = classRef.getAuthnContextClassRef();
449                     if (s != null && authnMethod.equals(s)) {
450                         AuthnContextClassRef ref = authnContextClassRefBuilder
451                                 .buildObject();
452                         authnCtx.setAuthnContextClassRef(ref);
453                         return authnCtx;
454                     }
455                 }
456             }
457         }
458         
459         // if no AuthnContextClassRef's matched, try the DeclRefs
460         if (authnDeclRefs != null) {
461             for (AuthnContextDeclRef declRef : authnDeclRefs) {
462                 if (declRef != null) {
463                     String s = declRef.getAuthnContextDeclRef();
464                     if (s != null && authnMethod.equals((s))) {
465                         AuthnContextDeclRef ref = authnContextDeclRefBuilder
466                                 .buildObject();
467                         authnCtx.setAuthnContextDeclRef(ref);
468                         return authnCtx;
469                     }
470                 }
471             }
472         }
473         
474         // no matches were found.
475         return null;
476     }
477     
478     /**
479      * Get the user's LoginContext.
480      *
481      * @param httpSession
482      *            The user's HttpSession.
483      *
484      * @return The user's LoginContext.
485      *
486      * @throws ProfileException
487      *             On error.
488      */
489     protected Saml2LoginContext getLoginContext(final HttpSession httpSession)
490             throws ProfileException {
491         
492         Object o = httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
493         if (o == null) {
494             log.error("User's session does not contain a LoginContext object.");
495             throw new ProfileException(
496                     "User's session does not contain a LoginContext object.");
497         }
498         
499         if (!(o instanceof Saml2LoginContext)) {
500             log
501                     .error("Invalid login context object -- object is not an instance of Saml2LoginContext.");
502             throw new ProfileException("Invalid login context object.");
503         }
504         
505         Saml2LoginContext ctx = (Saml2LoginContext) o;
506         
507         httpSession.removeAttribute(LoginContext.LOGIN_CONTEXT_KEY);
508         
509         return ctx;
510     }
511     
512     /**
513      * Verify the AuthnRequest is well-formed.
514      *
515      * @param authnRequest
516      *            The user's SAML 2 AuthnRequest.
517      * @param issuer
518      *            The Issuer of the AuthnRequest.
519      * @param relyingParty
520      *            The relying party configuration for the request's originator.
521      * @param session
522      *            The user's HttpSession.
523      *
524      * @throws AuthenticationRequestException
525      *             on error.
526      */
527     protected void verifyAuthnRequest(final AuthnRequest authnRequest,
528             String issuer, final RelyingPartyConfiguration relyingParty,
529             final HttpSession session) throws AuthenticationRequestException {
530         
531         Status failureStatus;
532         
533         // Check if we are in scope to handle this AuthnRequest
534         checkScope(authnRequest, issuer);
535         
536         // verify that the AssertionConsumerService url is valid.
537         AssertionConsumerService acsEndpoint = getAndVerifyACSEndpoint(
538                 authnRequest, relyingParty.getRelyingPartyId(),
539                 getRelyingPartyConfigurationManager().getMetadataProvider());
540         session.setAttribute(ACS_SESSION_KEY, acsEndpoint);
541         
542         // check for nameID constraints.
543         Subject subject = getAndVerifySubject(authnRequest);
544     }
545     
546     /**
547      * Get and verify the Subject element.
548      *
549      * @param authnRequest
550      *            The SAML 2 AuthnRequest.
551      *
552      * @return A Subject element.
553      *
554      * @throws AuthenticationRequestException
555      *             on error.
556      */
557     protected Subject getAndVerifySubject(final AuthnRequest authnRequest)
558             throws AuthenticationRequestException {
559         
560         Status failureStatus;
561         
562         Subject subject = authnRequest.getSubject();
563         
564         if (subject == null) {
565             failureStatus = buildStatus(StatusCode.REQUESTER_URI, null,
566                     "SAML 2 AuthnRequest " + authnRequest.getID()
567                     + " is malformed: It does not contain a Subject.");
568             throw new AuthenticationRequestException(
569                     "AuthnRequest lacks a Subject", failureStatus);
570         }
571         
572         // The Web Browser SSO profile disallows SubjectConfirmation
573         // methods in the requested subject.
574         List<SubjectConfirmation> confMethods = subject
575                 .getSubjectConfirmations();
576         if (confMethods != null || confMethods.size() > 0) {
577             log
578                     .error("SAML 2 AuthnRequest "
579                     + authnRequest.getID()
580                     + " is malformed: It contains SubjectConfirmation elements.");
581             failureStatus = buildStatus(
582                     StatusCode.REQUESTER_URI,
583                     null,
584                     "SAML 2 AuthnRequest "
585                     + authnRequest.getID()
586                     + " is malformed: It contains SubjectConfirmation elements.");
587             throw new AuthenticationRequestException(
588                     "AuthnRequest contains SubjectConfirmation elements",
589                     failureStatus);
590         }
591         
592         return subject;
593     }
594     
595     /**
596      * Return the endpoint URL and protocol binding to use for the AuthnRequest.
597      *
598      * @param authnRequest
599      *            The SAML 2 AuthnRequest.
600      * @param providerId
601      *            The SP's providerId.
602      * @param metadata
603      *            The appropriate Metadata.
604      *
605      * @return The AssertionConsumerService for the endpoint, or
606      *         <code>null</code> on error.
607      *
608      * @throws AuthenticationRequestException
609      *             On error.
610      */
611     protected AssertionConsumerService getAndVerifyACSEndpoint(
612             final AuthnRequest authnRequest, String providerId,
613             final MetadataProvider metadata)
614             throws AuthenticationRequestException {
615         
616         Status failureStatus;
617         
618         // Either the AssertionConsumerServiceIndex must be present
619         // or AssertionConsumerServiceURL must be present.
620         
621         Integer idx = authnRequest.getAssertionConsumerServiceIndex();
622         String acsURL = authnRequest.getAssertionConsumerServiceURL();
623         
624         if (idx != null && acsURL != null) {
625             log
626                     .error("SAML 2 AuthnRequest "
627                     + authnRequest.getID()
628                     + " is malformed: It contains both an AssertionConsumerServiceIndex and an AssertionConsumerServiceURL");
629             failureStatus = buildStatus(
630                     StatusCode.REQUESTER_URI,
631                     null,
632                     "SAML 2 AuthnRequest "
633                     + authnRequest.getID()
634                     + " is malformed: It contains both an AssertionConsumerServiceIndex and an AssertionConsumerServiceURL");
635             throw new AuthenticationRequestException("Malformed AuthnRequest",
636                     failureStatus);
637         }
638         
639         SPSSODescriptor spDescriptor;
640         try {
641             spDescriptor = metadata.getEntityDescriptor(providerId)
642                     .getSPSSODescriptor(SAML20_PROTOCOL_URI);
643         } catch (MetadataProviderException ex) {
644             log.error(
645                     "Unable retrieve SPSSODescriptor metadata for providerId "
646                     + providerId
647                     + " while processing SAML 2 AuthnRequest "
648                     + authnRequest.getID(), ex);
649             failureStatus = buildStatus(StatusCode.RESPONDER_URI, null,
650                     "Unable to locate metadata for " + providerId);
651             throw new AuthenticationRequestException(
652                     "Unable to locate metadata", ex, failureStatus);
653         }
654         
655         List<AssertionConsumerService> acsList = spDescriptor
656                 .getAssertionConsumerServices();
657         
658         // if the ACS index is specified, retrieve it from the metadata
659         if (idx != null) {
660             
661             int i = idx.intValue();
662             
663             // if the index is out of range, return an appropriate error.
664             if (i > acsList.size()) {
665                 log.error("Illegal AssertionConsumerIndex specicifed (" + i
666                         + ") in SAML 2 AuthnRequest " + authnRequest.getID());
667                 
668                 failureStatus = buildStatus(StatusCode.REQUESTER_URI, null,
669                         "Illegal AssertionConsumerIndex specicifed (" + i
670                         + ") in SAML 2 AuthnRequest "
671                         + authnRequest.getID());
672                 
673                 throw new AuthenticationRequestException(
674                         "Illegal AssertionConsumerIndex in AuthnRequest",
675                         failureStatus);
676             }
677             
678             return acsList.get(i);
679         }
680         
681         // if the ACS endpoint is specified, validate it against the metadata
682         String protocolBinding = authnRequest.getProtocolBinding();
683         for (AssertionConsumerService acs : acsList) {
684             if (acsURL.equals(acs.getLocation())) {
685                 if (protocolBinding != null) {
686                     if (protocolBinding.equals(acs.getBinding())) {
687                         return acs;
688                     }
689                 }
690             }
691         }
692         
693         log
694                 .error("Error processing SAML 2 AuthnRequest message "
695                 + authnRequest.getID()
696                 + ": Unable to validate AssertionConsumerServiceURL against metadata: "
697                 + acsURL + " for binding " + protocolBinding);
698         
699         failureStatus = buildStatus(StatusCode.REQUESTER_URI, null,
700                 "Unable to validate AssertionConsumerService against metadata.");
701         
702         throw new AuthenticationRequestException(
703                 "SAML 2 AuthenticationRequest: Unable to validate AssertionConsumerService against Metadata",
704                 failureStatus);
705     }
706     
707     /**
708      * Retrieve a parsed AssertionConsumerService endpoint from the user's
709      * session.
710      *
711      * @param session
712      *            The user's HttpSession.
713      *
714      * @return An AssertionConsumerServiceEndpoint object.
715      *
716      * @throws ProfileException
717      *             On error.
718      */
719     protected AssertionConsumerService getACSEndpointFromSession(
720             final HttpSession session) throws ProfileException {
721         
722         Object o = session.getAttribute(ACS_SESSION_KEY);
723         if (o == null) {
724             log
725                     .error("User's session does not contain an AssertionConsumerService object.");
726             throw new ProfileException(
727                     "User's session does not contain an AssertionConsumerService object.");
728         }
729         
730         if (!(o instanceof AssertionConsumerService)) {
731             log
732                     .error("Invalid session data -- object is not an instance of AssertionConsumerService.");
733             throw new ProfileException(
734                     "Invalid session data -- object is not an instance of AssertionConsumerService.");
735         }
736         
737         AssertionConsumerService endpoint = (AssertionConsumerService) o;
738         
739         session.removeAttribute(ACS_SESSION_KEY);
740         
741         return endpoint;
742     }
743     
744     /**
745      * Check if an {@link AuthnRequest} contains a {@link Scoping} element. If
746      * so, check if the specified IdP is in the {@link IDPList} element. If no
747      * Scoping element is present, this method returns <code>true</code>.
748      *
749      * @param authnRequest
750      *            The {@link AuthnRequest} element to check.
751      * @param providerId
752      *            The IdP's ProviderID.
753      *
754      * @throws AuthenticationRequestException
755      *             on error.
756      */
757     protected void checkScope(final AuthnRequest authnRequest, String providerId)
758             throws AuthenticationRequestException {
759         
760         Status failureStatus;
761         
762         List<String> idpEntries = new LinkedList<String>();
763         
764         Scoping scoping = authnRequest.getScoping();
765         if (scoping == null) {
766             return;
767         }
768         
769         // process all of the explicitly listed idp provider ids
770         IDPList idpList = scoping.getIDPList();
771         if (idpList == null) {
772             return;
773         }
774         
775         List<IDPEntry> explicitIDPEntries = idpList.getIDPEntrys();
776         if (explicitIDPEntries != null) {
777             for (IDPEntry entry : explicitIDPEntries) {
778                 String s = entry.getProviderID();
779                 if (s != null) {
780                     idpEntries.add(s);
781                 }
782             }
783         }
784         
785         // If the IDPList is incomplete, retrieve the complete list
786         // and add the entries to idpEntries.
787         GetComplete getComplete = idpList.getGetComplete();
788         IDPList referencedIdPs = getCompleteIDPList(getComplete);
789         if (referencedIdPs != null) {
790             List<IDPEntry> referencedIDPEntries = referencedIdPs.getIDPEntrys();
791             if (referencedIDPEntries != null) {
792                 for (IDPEntry entry : referencedIDPEntries) {
793                     String s = entry.getProviderID();
794                     if (s != null) {
795                         idpEntries.add(s);
796                     }
797                 }
798             }
799         }
800         
801         // iterate over all the IDPEntries we've gathered,
802         // and check if we're in scope.
803         for (String requestProviderId : idpEntries) {
804             if (providerId.equals(requestProviderId)) {
805                 log.debug("Found Scoping match for IdP: (" + providerId + ")");
806                 return;
807             }
808         }
809         
810         log.error("SAML 2 AuthnRequest " + authnRequest.getID()
811                 + " contains a Scoping element which "
812                 + "does not contain a providerID registered with this IdP.");
813         
814         failureStatus = buildStatus(StatusCode.RESPONDER_URI,
815                 StatusCode.NO_SUPPORTED_IDP_URI, null);
816         throw new AuthenticationRequestException(
817                 "Unrecognized providerID in Scoping element", failureStatus);
818     }
819     
820     /**
821      * Retrieve an incomplete IDPlist.
822      *
823      * This only handles URL-based <GetComplete/> references.
824      *
825      * @param getComplete
826      *            The (possibly <code>null</code>) &lt;GetComplete/&gt;
827      *            element
828      *
829      * @return an {@link IDPList} or <code>null</code> if the uri can't be
830      *         dereferenced.
831      */
832     protected IDPList getCompleteIDPList(final GetComplete getComplete) {
833         
834         // XXX: enhance this method to cache the url and last-modified-header
835         
836         if (getComplete == null) {
837             return null;
838         }
839         
840         String uri = getComplete.getGetComplete();
841         if (uri != null) {
842             return null;
843         }
844         
845         IDPList idpList = null;
846         InputStream istream = null;
847         
848         try {
849             URL url = new URL(uri);
850             URLConnection conn = url.openConnection();
851             istream = conn.getInputStream();
852             
853             // convert the raw data into an XML object
854             Document doc = parserPool.parse(istream);
855             Element docElement = doc.getDocumentElement();
856             Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory()
857                     .getUnmarshaller(docElement);
858             idpList = (IDPList) unmarshaller.unmarshall(docElement);
859             
860         } catch (MalformedURLException ex) {
861             log.error(
862                     "Unable to retrieve GetComplete IDPList. Unsupported URI: "
863                     + uri, ex);
864         } catch (IOException ex) {
865             log.error("IO Error while retreieving GetComplete IDPList from "
866                     + uri, ex);
867         } catch (XMLParserException ex) {
868             log.error(
869                     "Internal OpenSAML error while parsing GetComplete IDPList from "
870                     + uri, ex);
871         } catch (UnmarshallingException ex) {
872             log.error(
873                     "Internal OpenSAML error while unmarshalling GetComplete IDPList from "
874                     + uri, ex);
875         } finally {
876             
877             if (istream != null) {
878                 try {
879                     istream.close();
880                 } catch (IOException ex) {
881                     // pass
882                 }
883             }
884         }
885         
886         return idpList;
887     }
888     
889     protected void encodeResponse(String binding, final ProfileResponse<ServletResponse> response,
890             final Response samlResponse,
891             final RelyingPartyConfiguration relyingParty,
892             final SSOConfiguration ssoConfig, final SPSSODescriptor spDescriptor) throws ProfileException {
893         
894         MessageEncoder<ServletResponse> encoder = getMessageEncoderFactory().getMessageEncoder(binding);
895         if (encoder == null) {
896             log.error("SAML 2 Authentication Request: No MessageEncoder registered for " + binding);
897             throw new ProfileException("SAML 2 Authentication Request: No MessageEncoder registered for " + binding);
898         }
899         
900         encoder.setResponse(response.getRawResponse());
901         encoder.setIssuer(relyingParty.getProviderId());
902         encoder.setMetadataProvider(getRelyingPartyConfigurationManager().getMetadataProvider());
903         encoder.setRelyingPartyRole(spDescriptor);
904         encoder.setSigningCredential(relyingParty.getDefaultSigningCredential());
905         encoder.setSamlMessage(samlResponse);
906         encoder.setRelyingPartyEndpoint(spDescriptor.getDefaultAssertionConsumerService());
907         
908         try {
909             encoder.encode();
910         } catch (BindingException ex) {
911             log.error("Unable to encode response the relying party: " + relyingParty.getRelyingPartyId(), ex);
912             throw new ProfileException("Unable to encode response the relying party: "
913                     + relyingParty.getRelyingPartyId(), ex);
914         }
915         
916     }
917     
918 }