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