Update to use new profile handler code.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / profile / saml2 / AuthenticationRequest.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.ServletException;
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.opensaml.Configuration;
35 import org.opensaml.common.binding.ArtifactMap;
36 import org.opensaml.common.binding.BindingException;
37 import org.opensaml.common.binding.SAMLArtifactFactory;
38 import org.opensaml.saml2.core.Assertion;
39 import org.opensaml.saml2.core.Audience;
40 import org.opensaml.saml2.core.AudienceRestriction;
41 import org.opensaml.saml2.core.AuthnContext;
42 import org.opensaml.saml2.core.AuthnContextClassRef;
43 import org.opensaml.saml2.core.AuthnContextDeclRef;
44 import org.opensaml.saml2.core.AuthnRequest;
45 import org.opensaml.saml2.core.AuthnStatement;
46 import org.opensaml.saml2.core.GetComplete;
47 import org.opensaml.saml2.core.IDPEntry;
48 import org.opensaml.saml2.core.IDPList;
49 import org.opensaml.saml2.core.Issuer;
50 import org.opensaml.saml2.core.RequestedAuthnContext;
51 import org.opensaml.saml2.core.Response;
52 import org.opensaml.saml2.core.Scoping;
53 import org.opensaml.saml2.core.Status;
54 import org.opensaml.saml2.core.StatusCode;
55 import org.opensaml.saml2.core.Subject;
56 import org.opensaml.saml2.core.SubjectConfirmation;
57 import org.opensaml.saml2.metadata.AssertionConsumerService;
58 import org.opensaml.saml2.metadata.SPSSODescriptor;
59 import org.opensaml.saml2.metadata.provider.MetadataProvider;
60 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
61 import org.opensaml.xml.ConfigurationException;
62 import org.opensaml.xml.XMLObjectBuilder;
63 import org.opensaml.xml.io.Unmarshaller;
64 import org.opensaml.xml.io.UnmarshallingException;
65 import org.opensaml.xml.parse.BasicParserPool;
66 import org.opensaml.xml.parse.ParserPool;
67 import org.opensaml.xml.parse.XMLParserException;
68 import org.w3c.dom.Document;
69 import org.w3c.dom.Element;
70
71 import edu.internet2.middleware.shibboleth.common.profile.ProfileRequest;
72 import edu.internet2.middleware.shibboleth.common.profile.ProfileResponse;
73 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
74 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfigurationManager;
75 import edu.internet2.middleware.shibboleth.common.relyingparty.saml2.SSOConfiguration;
76 import edu.internet2.middleware.shibboleth.idp.authn.AuthenticationManager;
77 import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
78 import edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext;
79
80 /**
81  * SAML 2.0 Authentication Request profile handler
82  */
83 public class AuthenticationRequest extends AbstractSAML2ProfileHandler {
84
85     private static final Logger log = Logger.getLogger(AuthenticationRequest.class.getName());
86
87     /** SAML 2.0 protocol URI. */
88     public static final String SAML20_PROTOCOL_URI = "urn:oasis:names:tc:SAML:2.0:protocol";
89
90     /** The RelyingPartyManager. */
91     protected RelyingPartyConfigurationManager rpManager;
92
93     /**
94      * Backing store for artifacts. This must be shared between ShibbolethSSO and AttributeQuery.
95      */
96     protected ArtifactMap artifactMap;
97
98     /** The path to the IdP's AuthenticationManager servlet */
99     protected String authnMgrURL;
100
101     /** AuthenticationManager to be used */
102     protected AuthenticationManager authnMgr;
103
104     /** ArtifactFactory used to make artifacts. */
105     protected SAMLArtifactFactory artifactFactory;
106
107     /** A pool of XML parsers. */
108     protected ParserPool parserPool;
109
110     /** Builder for Assertion elements. */
111     protected XMLObjectBuilder assertionBuilder;
112
113     /** Builder for AuthnStatement elements. */
114     protected XMLObjectBuilder authnStatementBuilder;
115
116     /** Builder for AuthnContext elements. */
117     protected XMLObjectBuilder authnContextBuilder;
118
119     /** Builder for AuthnContextClassRef elements. */
120     protected XMLObjectBuilder authnContextClassRefBuilder;
121
122     /** Builder for AuthnContextDeclRef elements. */
123     protected XMLObjectBuilder authnContextDeclRefBuilder;
124
125     /** Builder for AudienceRestriction conditions. */
126     protected XMLObjectBuilder audienceRestrictionBuilder;
127
128     /** Builder for Audience elemenets. */
129     protected XMLObjectBuilder audienceBuilder;
130
131     /**
132      * Constructor.
133      */
134     public AuthenticationRequest() {
135
136         parserPool = new BasicParserPool();
137         artifactFactory = new SAMLArtifactFactory();
138
139         assertionBuilder = getBuilderFactory().getBuilder(Assertion.DEFAULT_ELEMENT_NAME);
140         authnStatementBuilder = getBuilderFactory().getBuilder(AuthnStatment.DEFULT_ELEMENT_NAME);
141         authnContextBuilder = getBuilderFactory().getBuilder(AuthnContext.DEFAULT_ELEMENT_NAME);
142         authnContextClassRefBuilder = getBuilderFactory().getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
143         authnContextDeclRefBuilder = getBuilderFactory().getBuilder(AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
144         audienceRestrictionBuilder = getBuilderFactory().getBuilder(AudienceRestriction.DEFAULT_ELEMENT_NAME);
145         audienceBuilder = getBuilderFactory().getBuilder(Audience.DEFAULT_ELEMENT_NAME);
146     }
147
148     /**
149      * Set the Authentication Mananger.
150      * 
151      * @param authnManager The IdP's AuthenticationManager.
152      */
153     public void setAuthenticationManager(AuthenticationManager authnManager) {
154         this.authnMgr = authnMgr;
155     }
156
157     /**
158      * Set the RelyingPartyManager.
159      * 
160      * @param rpManager The IdP's RelyingParyManager.
161      */
162     public void setRelyingPartyManager(RelyingPartyConfigurationManager rpManager) {
163         this.rpManager = rpManager;
164     }
165
166     /**
167      * Set the ArtifactMap.
168      * 
169      * @param artifactMap The IdP's ArtifactMap.
170      */
171     public void setArtifactMap(ArtifactMap artifactMap) {
172         this.artifactMap = artifactMap;
173     }
174
175     /** {@inheritDoc} */
176     public boolean processRequest(ProfileRequest request, ProfileResponse response) throws ServletException {
177
178         // Only http servlets are supported for now.
179         if (!(request.getRequest() instanceof HttpServletRequest)) {
180             log.error("Received a non-HTTP request from " + request.getRequest().getRemoteHost());
181             throw new ServletException("Received a non-HTTP request");
182         }
183
184         HttpServletRequest httpReq = (HttpServletRequest) request.getRequest();
185         HttpServletResponse httpResp = (HttpServletResponse) response.getResponse();
186         HttpSession httpSession = httpReq.getSession();
187
188         AuthnRequest authnRequest;
189         try {
190             // this will need to change
191             authnRequest = (org.opensaml.saml2.core.AuthnRequest) decodeMessage(request.getMessageDecoder(), request
192                     .getRequest());
193             // to accomodate the factory
194         } catch (BindingException ex) {
195             log.error("Unable to decode SAML 2 authentication request", ex);
196             throw new ServletException("Error decoding SAML 2 authentication request", ex);
197         }
198
199         Issuer issuer = authnRequest.getIssuer();
200         String providerId = authnRequest.getIssuer().getSPProvidedID();
201         RelyingPartyConfiguration relyingParty = rpManager.getRelyingPartyConfiguration(providerId);
202         SSOConfiguration ssoConfig = relyingParty.getProfileConfigurations().get(SSOConfiguration.PROFILE_ID);
203         SPSSODescriptor spDescriptor;
204
205         try {
206
207             // If the user hasn't been authenticated, validate the AuthnRequest
208             // and
209             // redirect to AuthenticationManager to authenticate them.
210             // Otherwise, the user has been authenticated, so generate an
211             // AuthenticationStatement.
212             if (!hasUserAuthenticated()) {
213                 verifyAuthnRequest(authnRequest);
214                 authenticateUser(authnRequest, httpSession, httpReq, httpResp);
215             }
216
217             // the user has been authenticated.
218             // check if the authentication was successful.
219
220             Saml2LoginContext loginCtx = getLoginContext(httpSession);
221             if (!loginCtx.getAuthenticationOK()) {
222                 // if authentication failed, send the appropriate SAML error
223                 // message.
224                 String failureMessage = loginCtx.getAuthenticationFailureMessage();
225                 Status failureStatus = getStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI, failureMessage);
226                 Response response = buildResponse(authnRequest.getID(), relyingParty.getProviderID(), failureStatus);
227
228                 // XXX: TODO: send the response.
229
230                 return true;
231             }
232
233             // the user successfully authenticated. build an authentication
234             // assertion.
235             Response response = buildResponse(authnRequest.getID(), relyingParty.getProviderID(), buildStatus(
236                     StatusCode.SUCCESS_URI, null, null));
237
238             // XXX: don't blindly copy conditions.
239             Assertion assertion = buildAssertion(authnRequest.getSubject(), authnRequest.getConditions(),
240                     new String[] { relyingParty.getRelyingPartyID() });
241             setAuthenticationStatement(assertion, loginCtx, authnRequest);
242
243             response.getAssertions().add(assertion);
244
245             // XXX: send the assertion
246
247         } catch (AuthenticationRequestException ex) {
248
249             StatusCode errorStatus = ex.getStatusCode();
250             if (errorStatus == null) {
251                 // if no explicit status code was set, assume the error was in
252                 // the message.
253                 errorStatus = buildStatus(StatusCode.REQUESTER_URI, null, null);
254                 Response response = buildResponse(authnRequest.getID(), relyingParty.getProviderID(), failureStatus);
255                 // XXX: TODO: send the response.
256             }
257
258         }
259
260         // build assertion
261         // add assertion to response
262         // send response
263
264         return true;
265     }
266
267     /**
268      * Check if the user has already been authenticated.
269      * 
270      * @param httpSession the user's HttpSession.
271      * 
272      * @return <code>true</code> if the user has been authenticated. otherwise <code>false</code>
273      */
274     protected boolean hasUserAuthenticated(final HttpSession httpSession) {
275
276         // if the user has authenticated, their session will have a logincontext
277
278         Object o = httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
279         return (o == null);
280     }
281
282     /**
283      * Check if the user has already been authenticated. If so, return the LoginContext. If not, redirect the user to
284      * the AuthenticationManager.
285      * 
286      * @param authnRequest The SAML 2 AuthnRequest.
287      * @param httpSession The user's HttpSession.
288      * @param request The user's HttpServletRequest.
289      * @param response The user's HttpServletResponse.
290      * 
291      * @return A LoginContext for the authenticated user.
292      * 
293      * @throws SerlvetException on error.
294      */
295     protected void authenticateUser(final AuthnRequest authnRequest, final HttpSession httpSession,
296             final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
297
298         // Forward the request to the AuthenticationManager.
299         // When the AuthenticationManager is done it will
300         // forward the request back to this servlet.
301
302         loginCtx = new Saml2LoginContext(authnRequest);
303         loginCtx.setProfileHandlerURL(httpReq.getPathInfo());
304         httpSession.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginCtx);
305         try {
306             RequestDispatcher dispatcher = request.getRequestDispatcher(authnMgrURL);
307             dispatcher.forward(request, response);
308         } catch (IOException ex) {
309             log.error("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID() + " to AuthenticationManager", ex);
310             throw new ServletException("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID()
311                     + " to AuthenticationManager", ex);
312         }
313     }
314
315     /**
316      * Build an AuthnStatement and add it to a Response.
317      * 
318      * @param response The Response to which the AuthnStatement will be added.
319      * @param loginCtx The LoginContext of the sucessfully authenticated user.
320      * @param authnRequest The AuthnRequest that prompted this message.
321      * @param ssoConfig The SSOConfiguration for the RP to which we are addressing this message.
322      * @param issuer The IdP's identifier.
323      * @param audiences An array of URIs restricting the audience of this assertion.
324      */
325     protected void setAuthenticationStatement(Assertion assertion, final LoginContext loginCtx,
326             final AuthnRequest authnRequest) throws ServletException {
327
328         // Build the AuthnCtx. We need to determine if the user was
329         // authenticated
330         // with an AuthnContextClassRef or a AuthnContextDeclRef
331         AuthnContext authnCtx = buildAuthnCtx(authnRequest, loginCtx.getAuthenticationMethod());
332         if (authnCtx == null) {
333             log.error("Error respond to SAML 2 AuthnRequest " + authnRequest.getID()
334                     + " : Unable to determine authentication method");
335         }
336
337         AuthnStatement stmt = (AuthnStatement) authnStatementBuilder.buildObject(AuthnStatment.DEFAULT_ELEMENT_NAME);
338         stmt.setAuthnInstant(loginCtx.getAuthenticationInstant());
339         stmt.setAuthnContext(authnCtx);
340
341         // add the AuthnStatement to the Assertion
342         List<AuthnStatement> authnStatements = assertion.getAuthnStatements();
343         authnStatements.add(stmt);
344     }
345
346     /**
347      * Create the AuthnContex object.
348      * 
349      * To do this, we have to walk the AuthnRequest's RequestedAuthnContext object and compare any values we find to
350      * what's set in the loginContext.
351      * 
352      * @param requestedAuthnCtx The RequestedAuthnContext from the Authentication Request.
353      * @param authnMethod The authentication method that was used.
354      * 
355      * @return An AuthnCtx object on success or <code>null</code> on failure.
356      */
357     protected AuthnContext buildAuthnCtx(final RequestedAuthnContext requestedAuthnCtx, String authnMethod) {
358
359         // this method assumes that only one URI will match.
360
361         AuthnContext authnCtx = (AuthnCtx) authnContextBuilder.buildObject(AuthnContext.DEFAULT_ELEMENT_NAME);
362         String authnMethod = loginCtx.getAuthenticationMethod();
363
364         List<AuthnContextClassRef> authnClasses = ctx.getAuthnContextClassRefs();
365         List<AuthnContextDeclRef> authnDeclRefs = ctx.getAuthnContextDeclRefs();
366
367         if (authnClasses != null) {
368             for (AuthnContextClassRef classRef : authnClasses) {
369                 if (classRef != null) {
370                     String s = classRef.getAuthnContextClassRef();
371                     if (s != null && authnMethod.equals(s)) {
372                         AuthnContextClassRef classRef = (AuthnContextClassRef) authnContextClassRefBuilder
373                                 .buildObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
374                         authnCtx.setAuthnContextClassRef(classRef);
375                         return authnCtx;
376                     }
377                 }
378             }
379         }
380
381         // if no AuthnContextClassRef's matched, try the DeclRefs
382         if (authnDeclRefs != null) {
383             for (AuthnContextDeclRef declRef : authnDeclRefs) {
384                 if (declRef != null) {
385                     String s = declRef.getAuthnContextDeclRef();
386                     if (s != null && authnMethod.equals((s))) {
387                         AuthnContextDeclRef declRef = (AuthnContextDeclRef) authnContextDeclRefBuilder
388                                 .buildObject(AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
389                         authnCtx.setAuthnContextDeclRef(declRef);
390                         return authnCtx;
391                     }
392                 }
393             }
394         }
395
396         // no matches were found.
397         return null;
398     }
399
400     /**
401      * Get the User's LoginContext.
402      * 
403      * @param httpSession The user's HttpSession.
404      * 
405      * @return The user's LoginContext.
406      * 
407      * @throws ServletException On error.
408      */
409     protected LoginContext getLoginContext(final HttpSession httpSession) throws ServletException {
410
411         Object o = httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
412         if (o == null) {
413             log.error("User's session does not contain a LoginContext object.");
414             throw new ServletException("User's session does not contain a LoginContext object.");
415         }
416
417         if (!(o instanceof LoginContext)) {
418             log.error("Invalid login context object -- object is not an instance of LoginContext.");
419             throw new ServletException("Invalid login context object.");
420         }
421
422         return (LoginContext) o;;
423     }
424
425     /**
426      * Verify the AuthnRequest is well-formed.
427      * 
428      * @param authnRequest The user's SAML 2 AuthnRequest.
429      * 
430      * @throws AuthenticationRequestException on error.
431      */
432     protected void verifyAuthnRequest(final AuthnRequest authnRequest) throws AuthenticationRequestException {
433
434         Status failureStatus;
435
436         // The Web Browser SSO profile requires that the Issuer element is
437         // present.
438         Issuer issuer = authnRequest.getIssuer();
439         if (issuer == null) {
440             log.error("Malformed SAML 2 AuthnReq - missing Issuer element.");
441             failureStatus = buildStatus(StatusCode.REQUESTER_URI, null, "SAML 2 AuthnRequest " + authnRequest.getID()
442                     + " is malformed: It lacks an Issuer.");
443             throw new AuthenticationRequestException("AuthnRequest lacks an Issuer", failureStatus);
444         }
445
446         // Check if we are in scope to handle this AuthnRequest
447         // XXX: confirm that SPProviderID is the field we want in the issuer
448         if (!checkScope(authnRequest, issuer.getSPProvidedID())) {
449             return false;
450         }
451
452         // XXX: run signature checks on authnRequest
453
454         // verify that the AssertionConsumerService url is valid.
455         AssertionConsumerService acsEndpoint = getAndVerifyACSEndpoint(authnRequest, relyingParty.getRelyingPartyID(),
456                 rpManager.getMetadataProvider());
457
458         Subject subject = getAndVerifySubject(authnRequest, failureStatus);
459
460         // check for nameID constraints.
461     }
462
463     /**
464      * Get and verify the Subject element.
465      * 
466      * @param authnRequest The SAML 2 AuthnRequest.
467      * 
468      * @return A Subject element.
469      * 
470      * @throws AuthenticationRequestException on error.
471      */
472     protected Subject getAndVerifySubject(final AuthnRequest authnRequest) throws AuthenticationRequestException {
473
474         Status failureStatus;
475
476         Subject subject = authnRequest.getSubject();
477
478         if (subject == null) {
479             failureStatus = buildStatus(StatusCode.REQUESTER_URI, null, "SAML 2 AuthnRequest " + authnRequest.getID()
480                     + " is malformed: It does not contain a Subject.");
481             throw new AuthenticationRequestException("AuthnRequest lacks a Subject", failureStatus);
482         }
483
484         // The Web Browser SSO profile disallows SubjectConfirmation
485         // methods in the requested subject.
486         List<SubjectConfirmation> confMethods = subject.getSubjectConfirmations();
487         if (confMethods != null || confMethods.length > 0) {
488             log.error("SAML 2 AuthnRequest " + authnRequest.getID()
489                     + " is malformed: It contains SubjectConfirmation elements.");
490             failureStauts = buildStatus(StatusCode.REQUESTER_URI, null, "SAML 2 AuthnRequest " + authnRequest.getID()
491                     + " is malformed: It contains SubjectConfirmation elements.");
492             throw new AuthenticationRequestException("AuthnRequest contains SubjectConfirmation elements",
493                     failureStatus);
494         }
495
496         return subject;
497     }
498
499     /**
500      * Return the endpoint URL and protocol binding to use for the AuthnRequest.
501      * 
502      * @param authnRequest The SAML 2 AuthnRequest.
503      * @param providerId The SP's providerId.
504      * @param metadata The appropriate Metadata.
505      * 
506      * @return The AssertionConsumerService for the endpoint, or <code>null</code> on error.
507      * 
508      * @throws AuthenticationRequestException On error.
509      */
510     protected AssertionConsumerService getAndVerifyACSEndpoint(final AuthnRequest authnRequest, String providerId,
511             final MetadataProvider metadata) throws AuthenticationRequestException {
512
513         Status failureStatus;
514
515         // Either the AssertionConsumerServiceIndex must be present
516         // or AssertionConsumerServiceURL must be present.
517
518         Integer idx = authnRequest.getAssertionConsumerServiceIndex();
519         String acsURL = authnRequest.getAssertionConsumerServiceURL();
520
521         if (idx != null && acsURL != null) {
522             log
523                     .error("SAML 2 AuthnRequest "
524                             + authnRequest.getID()
525                             + " is malformed: It contains both an AssertionConsumerServiceIndex and an AssertionConsumerServiceURL");
526             failureStatus = buildStatus(
527                     StatusCode.REQUESTER_URI,
528                     null,
529                     "SAML 2 AuthnRequest "
530                             + authnRequest.getID()
531                             + " is malformed: It contains both an AssertionConsumerServiceIndex and an AssertionConsumerServiceURL");
532             throw new AuthenticationRequestException("Malformed AuthnRequest", failureStatus);
533         }
534
535         SPSSODescriptor spDescriptor;
536         try {
537             spDescriptor = metadata.getEntityDescriptor(providerId).getSPSSODescriptor(SAML20_PROTOCOL_URI);
538         } catch (MetadataProviderException ex) {
539             log.error("Unable retrieve SPSSODescriptor metadata for providerId " + providerId
540                     + " while processing SAML 2 AuthnRequest " + authnRequest.getID(), ex);
541             failureStatus = buildStatus(StatusCode.RESPONDER_URI, null, "Unable to locate metadata for " + providerId);
542             throw new AuthenticationRequestException("Unable to locate metadata", ex, failureStatus);
543         }
544
545         List<AssertionConsumerService> acsList = spDescriptor.getAssertionConsumerServices();
546
547         // if the ACS index is specified, retrieve it from the metadata
548         if (idx != null) {
549
550             int i = idx.intValue();
551
552             // if the index is out of range, return an appropriate error.
553             if (i > acsList.length) {
554                 log.error("Illegal AssertionConsumerIndex specicifed (" + i + ") in SAML 2 AuthnRequest "
555                         + authnRequest.getID());
556
557                 failureStatus = buildStatus(StatusCode.REQUESTER_URI, null,
558                         "Illegal AssertionConsumerIndex specicifed (" + i + ") in SAML 2 AuthnRequest "
559                                 + authnRequest.getID());
560
561                 throw new AuthenticationRequestException("Illegal AssertionConsumerIndex in AuthnRequest",
562                         failureStatus);
563             }
564
565             return acsList.get(i);
566         }
567
568         // if the ACS endpoint is specified, validate it against the metadata
569         String protocolBinding = authnRequest.getProtocolBinding();
570         for (AssertionConumerService acs : acsList) {
571             if (acsURL.equals(acs.getLocation())) {
572                 if (protocolBinding != null) {
573                     if (protocolBinding.equals(acs.getBinding())) {
574                         return acs;
575                     }
576                 }
577             }
578         }
579
580         log.error("Error processing SAML 2 AuthnRequest message " + authnRequest.getID()
581                 + ": Unable to validate AssertionConsumerServiceURL against metadata: " + acsURL + " for binding "
582                 + protocolBinding);
583
584         failureStatus = buildStatus(statusCodeBuilder.REQUESTER_URI, null,
585                 "Unable to validate AssertionConsumerService against metadata.");
586
587         throw new AuthenticationRequestException("Unabel to validate AssertionConsumerService against Metadata",
588                 failureStatus);
589     }
590
591     /**
592      * Check if an {@link AuthnRequest} contains a {@link Scoping} element. If so, check if the specified IdP is in the
593      * {@link IDPList} element. If no Scoping element is present, this method returns <code>true</code>.
594      * 
595      * @param authnRequest The {@link AuthnRequest} element to check.
596      * @param providerId The IdP's ProviderID.
597      * 
598      * @throws AuthenticationRequestException on error.
599      */
600     protected void checkScope(final AuthnRequest authnRequest, String providerId) throws AuthenticationRequestException {
601
602         Status failureStatus;
603
604         List<String> idpEntries = new LinkedList<String>();
605
606         Scoping scoping = authnRequest.getScoping();
607         if (scoping == null) {
608             return true;
609         }
610
611         // process all of the explicitly listed idp provider ids
612         IDPList idpList = scoping.getIDPList();
613         if (idpList == null) {
614             return;
615         }
616
617         List<IDPEntry> explicitIDPEntries = idpList.getIDPEntrys();
618         if (explicitIDPEntries != null) {
619             for (IDPEntry entry : explicitIDPEntries) {
620                 String s = entry.getProviderID();
621                 if (s != null) {
622                     idpEntries.add(s);
623                 }
624             }
625         }
626
627         // If the IDPList is incomplete, retrieve the complete list
628         // and add the entries to idpEntries.
629         GetComplete getComplete = idpList.getGetComplete();
630         IDPList referencedIdPs = getCompleteIDPList(getComplete);
631         if (referencedIdPs != null) {
632             List<IDPEntry> referencedIDPEntries = referencedIdPs.getIDPEntrys();
633             if (referencedIDPEntries != null) {
634                 for (IDPEntry entry : referencedIDPEntries) {
635                     String s = entry.getProviderID();
636                     if (s != null) {
637                         idpEntries.add(s);
638                     }
639                 }
640             }
641         }
642
643         // iterate over all the IDPEntries we've gathered,
644         // and check if we're in scope.
645         for (String requestProviderId : idpEntries) {
646             if (providerId.equals(requestProviderId)) {
647                 found = true;
648                 log.debug("Found Scoping match for IdP: (" + providerId + ")");
649                 return;
650             }
651         }
652
653         log.error("SAML 2 AuthnRequest " + authnRequest.getID() + " contains a Scoping element which "
654                 + "does not contain a providerID registered with this IdP.");
655
656         failureStatus = buildStatus(StatusCode.RESPONDER_URI, StatusCode.NO_SUPPORTED_IDP_URI, null);
657         throw new AuthenticationRequestException("Unrecognized providerID in Scoping element", failureStatus);
658     }
659
660     /**
661      * Retrieve an incomplete IDPlist.
662      * 
663      * This only handles URL-based <GetComplete/> references.
664      * 
665      * @param getComplete The (possibly <code>null</code>) &lt;GetComplete/&gt; element
666      * 
667      * @return an {@link IDPList} or <code>null</code> if the uri can't be dereferenced.
668      */
669     protected IDPList getCompleteIDPList(final GetComplete getComplete) {
670
671         // XXX: enhance this method to cache the url and last-modified-header
672
673         if (getComplete == null) {
674             return null;
675         }
676
677         String uri = getComplete.getGetComplete();
678         if (uri != null) {
679             return null;
680         }
681
682         IDPList idpList = null;
683         InputStream istream = null;
684
685         try {
686             URL url = new URL(uri);
687             URLConnection conn = url.openConnection();
688             istream = conn.getInputStream();
689
690             // convert the raw data into an XML object
691             Document doc = parserPool.parse(istream);
692             Element docElement = doc.getDocumentElement();
693             Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(docElement);
694             idpList = (IDPList) unmarshaller.unmarshall(docElement);
695
696         } catch (MalformedURLException ex) {
697             log.error("Unable to retrieve GetComplete IDPList. Unsupported URI: " + uri, ex);
698         } catch (IOException ex) {
699             log.error("IO Error while retreieving GetComplete IDPList from " + uri, ex);
700         } catch (ConfigurationException ex) {
701             log.error("Internal OpenSAML error while parsing GetComplete IDPList from " + uri, ex);
702         } catch (XMLParserException ex) {
703             log.error("Internal OpenSAML error while parsing GetComplete IDPList from " + uri, ex);
704         } catch (UnmarshallingException ex) {
705             log.error("Internal OpenSAML error while unmarshalling GetComplete IDPList from " + uri, ex);
706         } finally {
707
708             if (istream != null) {
709                 try {
710                     istream.close();
711                 } catch (IOException ex) {
712                     // pass
713                 }
714             }
715         }
716
717         return idpList;
718     }
719 }