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