rough draft of SAML 2 Authn Request handler
authordmorr <dmorr@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Mon, 19 Mar 2007 16:04:15 +0000 (16:04 +0000)
committerdmorr <dmorr@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Mon, 19 Mar 2007 16:04:15 +0000 (16:04 +0000)
git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@2164 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/shibboleth/idp/profile/saml2/AuthenticationRequest.java

index 1b302d9..c7755ad 100644 (file)
  */
 package edu.internet2.middleware.shibboleth.idp.profile.saml2;
 
+
 import java.io.InputStream;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLConnection;
+import java.util.LinkedList;
 import java.util.List;
 
-
+import javax.servlet.RequestDispatcher;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 
 import edu.internet2.middleware.shibboleth.common.profile.ProfileHandler;
+import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
+import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyManager;
+import edu.internet2.middleware.shibboleth.common.relyingparty.saml2.SSOConfiguration;
 import edu.internet2.middleware.shibboleth.idp.authn.AuthenticationManager;
-
-import javolution.util.FastList;
+import edu.internet2.middleware.shibboleth.idp.authn.LoginContext;
+import edu.internet2.middleware.shibboleth.idp.authn.Saml2LoginContext;
 
 import org.apache.log4j.Logger;
-
+import org.joda.time.DateTime;
 import org.opensaml.Configuration;
 import org.opensaml.DefaultBootstrap;
-import org.opensaml.common.xml.ParserPoolManager;
+import org.opensaml.saml2.core.Assertion;
+import org.opensaml.saml2.core.AuthnContext;
 import org.opensaml.saml2.core.AuthnContextClassRef;
 import org.opensaml.saml2.core.AuthnContextDeclRef;
 import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration;
@@ -46,10 +55,21 @@ import org.opensaml.saml2.core.IDPEntry;
 import org.opensaml.saml2.core.IDPList;
 import org.opensaml.saml2.core.LogoutRequest;
 import org.opensaml.saml2.core.RequestedAuthnContext;
+import org.opensaml.saml2.core.Response;
 import org.opensaml.saml2.core.Scoping;
+import org.opensaml.saml2.core.Status;
+import org.opensaml.saml2.core.SamlCode;
+import org.opensaml.saml2.core.StatusMessage;
+import org.opensaml.saml2.core.Subject;
+import org.opensaml.saml2.core.SubjectConfirmation;
+import org.opensaml.saml2.metadata.AssertionConsumerService;
+import org.opensaml.saml2.metadata.MetadataProvider;
+import org.opensaml.saml2.metadata.provider.ProviderException;
 import org.opensaml.xml.ConfigurationException;
 import org.opensaml.xml.io.Unmarshaller;
 import org.opensaml.xml.io.UnmarshallingException;
+import org.opensaml.xml.parse.ParserPool;
+import org.opensaml.xml.XMLObjectBuilder;
 import org.opensaml.xml.parse.XMLParserException;
 
 import org.w3c.dom.Document;
@@ -61,109 +81,601 @@ import org.xml.sax.InputSource;
 /**
  * SAML 2.0 Authentication Request profile handler
  */
-public class AuthenticationRequest implements ProfileHandler {
+public class AuthenticationRequest extends AbstractProfileHandler {
+    
+    private static final Logger log = Logger.getLogger(AuthenticationRequest.class.getName());
+    
+    /** SAML 2.0 protocol URI. */
+    public static final String SAML20_PROTOCOL_URI = "urn:oasis:names:tc:SAML:2.0:protocol";
+    
+    /** The RelyingPartyManager. */
+    protected RelyingPartyManager rpManager;
     
-    private static final Logger log =
-            Logger.getLogger(AuthenticationRequest.class.getName());
+    /** Backing store for artifacts. This must be shared between ShibbolethSSO and AttributeQuery. */
+    protected ArtifactMap artifactMap;
     
+    /** The path to the IdP's AuthenticationManager servlet */
+    protected String authnMgrURL;
     
     /** AuthenticationManager to be used */
-    private AuthenticationManager authnMgr;
+    protected AuthenticationManager authnMgr;
+    
+    /** ArtifactFactory used to make artifacts. */
+    protected SAMLArtifactFactory artifactFactory;
+    
+    /** A pool of XML parsers. */
+    protected ParserPool parserPool;
+    
+    /** Builder for Assertion elements. */
+    protected XMLObjectBuilder assertionBuilder;
+    
+    /** Builder for AuthnStatement elements. */
+    protected XMLObjectBuilder authnStatementBuilder;
+    
+    /** Builder for AuthnContext elements. */
+    protected XMLObjectBuilder authnContextBuilder;
+    
+    /** Builder for AuthnContextClassRef elements. */
+    protected XMLObjectBuilder authnContextClassRefBuilder;
+    
+    /** Builder for AuthnContextDeclRef elements. */
+    protected XMLObjectBuilder authnContextDeclRefBuilder;
+    
+    /** Builder for Response elements. */
+    protected XMLObjectBuilder responseBuilder;
+    
+    /** Builder for Issuer elements. */
+    protected XMLObjectBuilder issuerBuilder;
+    
+    /** Builder for Status elements. */
+    protected XMLObjectBuilder statusBuilder;
+    
+    /** Builder for StatusCode elements. */
+    protected XMLObjectBuilder statusCodeBuilder;
+    
+    /** Builder for StatusMessage elements. */
+    protected XMLObjectBuilder statusMessageBuilder;
+    
+    
+    /**
+     * Constructor.
+     */
+    public AuthenticationRequest() {
+       
+       parserPool = new ParserPool();
+       artifactFactory = new SAMLArtifactFactory();
+       
+       assertionBuilder            = getBuilderFactory().getBuilder(Assertion.DEFAULT_ELEMENT_NAME);
+       authnStatementBuilder       = getBuilderFactory().getBuilder(AuthnStatment.DEFULT_ELEMENT_NAME);
+       authnContextBuilder         = getBuilderFactory().getBuilder(AuthnContext.DEFAULT_ELEMENT_NAME);
+       authnContextClassRefBuilder = getBuilderFactory().getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
+       authnContextDeclRefBuilder  = getBuilderFactory().getBuilder(AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
+       responseBuilder             = getBuilderFactory().getBuilder(Response.DEFAULT_ELEMENT_NAME);
+       issuerBuilder               = getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
+       statusBuilder               = getBuilderFactory().getBuilder(Status.DEFAILT_ELEMENT_NAME);
+       statusCodeBuilder           = getBuilderFactory().getBuilder(StatusCode.DEFAULT_ELEMENT_NAME);
+       statusMessageBuilder        = getBuilderFactory().getBuilder(StatusMessage.DEFAULT_ELEMENT_NAME);
+    }
+    
+    
+    /**
+     * Set the Authentication Mananger.
+     *
+     * @param authnManager The IdP's AuthenticationManager.
+     */
+    public void setAuthenticationManager(AuthenticationManager authnManager) {
+       this.authnMgr = authnMgr;
+    }
+    
+    
+    /**
+     * Set the RelyingPartyManager.
+     *
+     * @param rpManager The IdP's RelyingParyManager.
+     */
+    public void setRelyingPartyManager(RelyingPartyManager rpManager) {
+       this.rpManager = rpManager;
+    }
+    
+    
+    /**
+     * Set the ArtifactMap.
+     *
+     * @param artifactMap The IdP's ArtifactMap.
+     */
+    public void setArtifactMap(ArtifactMap artifactMap) {
+       this.artifactMap = artifactMap;
+    }
     
     
     /** {@inheritDoc} */
     public boolean processRequest(ServletRequest request, ServletResponse response) throws ServletException {
-        
+       
+       // Only http servlets are supported for now.
+       if (!(request instanceof HttpServletRequest)) {
+           log.error("Received a non-HTTP request from " + request.getRemoteHost());
+           throw new ServletException("Received a non-HTTP request");
+       }
+       
+       HttpServletRequest httpReq = (HttpServletRequest)request;
+       HttpServletResponse httpResp = (HttpServletResponse)response;
+       HttpSession httpSession = httpReq.getSession();
+
+       AuthnRequest authnRequest;
+       try {
+           authnRequest = decodeMessage(request); // this will need to change to accomodate the factory
+       } catch (BindingException ex) {
+           log.error("Unable to decode SAML 2 authentication request", ex);
+           throw new ServletException("Error decoding SAML 2 authentication request", ex);
+       }
+       
+       Issuer issuer = authnRequest.getIssuer();
+       String providerId = authnRequest.getIssuer().getSPProvidedID();
+       RelyingPartyConfiguration relyingParty = rpManager.getRelyingPartyConfiguration(providerId);
+       SSOConfiguration ssoConfig = relyingParty.getProfileConfigurations().get(SSOConfiguration.PROFILE_ID);
+       SPSSODescriptor spDescriptor;
+       
+       // if the user hasn't been authenticated, validate the AuthnRequest and
+       // redirect to AuthenticationManager to authenticate them.
+       // otherwise, generate an AuthenticationStatement.
+       if (!hasUserAuthenticated()) {
+                   
+           StatusCode failureStatus;
+           
+           if (!verifyAuthnRequest(authnRequest, failureStatus)) {
+               Response response = buildResponse(authnRequest.getID(), 
+                   relyingParty.getProviderID(), failureStatus);
+               // XXX: TODO: send response;
+           }
+           authenticateUser(authnRequest, httpSession, httpReq, httpResp);
+       }
+       
+       
+       Saml2LoginContext loginCtx = getLoginContext(httpSession);
+       if (!loginCtx.getAuthenticationOK()) {
+           // if authentication failed, send the appropriate SAML error message.
+           String failureMessage = loginCtx.getAuthenticationFailureMessage();
+           Status failureStatus = getStatus(StatusCode.RESPONDER_URI, StatusCode.AUTHN_FAILED_URI, failureMessage);
+           Response response = buildResponse(authnRequest.getID(), relyingParty.getProviderID(), failureStatus);
+           // XXX: TODO: send the response.
+           
+           return true;
+       }
+       
+       // the user successfully authenticated. built an authentication assertion.
+       Response response = buildResponse(authnRequest.getID(), relyingParty.getProviderID(),
+           buildStatus(StatusCode.SUCCESS_URI, null, null));
+
+       // build assertion
+       // add assertion to response
+       // send response
+       
+       return true;
+    }
     
-//        // Check if we are in scope to handle this AuthnRequest
-//        // XXX: How do we get the current IdP's relying party uri (if we're in multiple feds?)
-//        boolean scopeOK = this.checkScope(authnRequest, "");
-//        if (!scopeOK) {
-//            log.error("AuthnRequest contains a Scoping element which "
-//                    + "does not contain a providerID registered with this IdP.");
-//        }
+    
+    /**
+     * Check if the user has already been authenticated.
+     *
+     * @param httpSession the user's HttpSession.
+     *
+     * @return <code>true</code> if the user has been authenticated. otherwise <code>false</code>
+     */
+    protected boolean hasUserAuthenticated(final HttpSession httpSession) {
        
+       // if the user has authenticated, their session will have a logincontext
        
-        return false;
+       Object o = httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
+       return (o == null);
     }
-
+    
     
     /**
-     * Check if an {@link AuthnRequest} contains a {@link Scoping} element.
-     * If so, check if the specified IdP is in the {@link IDPList} element.
-     * If no Scoping element is present, this method returns <code>true</code>.
+     * Check if the user has already been authenticated.
+     * If so, return the LoginContext. If not, redirect the user to the
+     * AuthenticationManager.
      *
-     * @param authnRequest The {@link AuthnRequest} element to check.
-     * @param providerId The IdP's ProviderID
+     * @param authnRequest The SAML 2 AuthnRequest.
+     * @param httpSession The user's HttpSession.
+     * @param request The user's HttpServletRequest.
+     * @param response The user's HttpServletResponse.
      *
-     * @return <code>true</code>if idp is in the IDPList, otherwise <code>false</code>
+     * @return A LoginContext for the authenticated user.
+     *
+     * @throws SerlvetException on error.
      */
-    private boolean checkScope(final AuthnRequest authnRequest, String providerId) {
-        
-        List<String> idpEntries = new FastList<String>();
-        
-        if (authnRequest == null) {
-            return (false);
-        }
-        
-        if (providerId == null) {
-            return (false);
-        }
-        
-        Scoping scoping = authnRequest.getScoping();
-        if (scoping == null) {
-            return (true);
-        }
-        
-        // process all of the explicitly listed idp provider ids
-        IDPList idpList = scoping.getIDPList();
-        if (idpList == null) {
-            return (true);
-        }
-        
-        List<IDPEntry> explicitIDPEntries = idpList.getIDPEntrys();
-        if (explicitIDPEntries != null) {
-            for (IDPEntry entry : explicitIDPEntries) {
-                String s = entry.getProviderID();
-                if (s != null) {
-                    idpEntries.add(s);
-                }
-            }
-        }
-        
-        
-        // If the IDPList is incomplete, retrieve the complete list
-        // and add the entries to idpEntries.
-        GetComplete getComplete = idpList.getGetComplete();
-        IDPList referencedIdPs = this.getCompleteIDPList(getComplete);
-        if (referencedIdPs != null) {
-            List<IDPEntry> referencedIDPEntries = referencedIdPs.getIDPEntrys();
-            if (referencedIDPEntries != null) {
-                for (IDPEntry entry : referencedIDPEntries) {
-                    String s = entry.getProviderID();
-                    if (s != null) {
-                        idpEntries.add(s);
+    protected void authenticateUser(final AuthnRequest authnRequest, final HttpSession httpSession,
+           final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
+       
+       // Forward the request to the AuthenticationManager.
+       // When the AuthenticationManager is done it will
+       // forward the request back to this servlet.
+       
+       loginCtx = new Saml2LoginContext(authnRequest);
+       loginCtx.setProfileHandlerURL(httpReq.getPathInfo());
+       httpSession.setAttribute(LoginContext.LOGIN_CONTEXT_KEY, loginCtx);
+       try {
+           RequestDispatcher dispatcher = request.getRequestDispatcher(authnMgrURL);
+           dispatcher.forward(request, response);
+       } catch (IOException ex) {
+           log.error("Error forwarding SAML 2 AuthnRequest " + authnRequest.getID() + " to AuthenticationManager", ex);
+           throw new ServletException("Error forwarding SAML 2 AuthnRequest " 
+               + authnRequest.getID() + " to AuthenticationManager", ex);
+       }
+    }
+    
+    
+    /**
+     * Build an AuthenticationStatement and add it to the Response.
+     *
+     */
+    protected void setAuthenticationStatement(Response response, final LoginContext loginCtx,
+       final AuthnRequest authnRequest, final SSOConfig ssoConfig, String issuer, String audience) throws ServletException {
+       
+       if (ssoConfig.getEncryptAssertion()) {
+           // encrypt the assertion
+       }
+       
+       Assertion assertion = (Assertion)assertionBuilder.buildObject(Assertion.DEFAULT_ELEMENT_NAME);
+       assertion.setID(getIdGenerator().generateIdentifier());
+       assertion.setVersion(SAML_VERSION);
+       assertion.setIssueInstant(new DateTime());
+       assertion.setConditions(authnRequest.getConditions());
+       assertion.setSubject(authnRequest.getSubject());
+       
+       
+       Issuer i = (Issuer)issuerBuilder.buildObject(Issuer.DEFAULT_ELEMENT_NAME);
+       i.setValue(issuer);
+       assertion.setIssuer(i);
+       
+       // Build the AuthnCtx. We need to determine if the user was authenticated
+       // with an AuthnContextClassRef or a AuthnContextDeclRef
+       AuthnContext authnCtx = buildAuthnCtx(authnRequest, loginCtx.getAuthenticationMethod());
+       if (authnCtx == null) {
+           log.error("Error respond to SAML 2 AuthnRequest " + authnRequest.getID()
+               + " : Unable to determine authentication method");
+       }
+       
+       
+       AuthnStatement stmt = (AuthnStatement)authnStatementBuilder.buildObject(AuthnStatment.DEFAULT_ELEMENT_NAME);
+       stmt.setAuthnInstant(loginCtx.getAuthenticationInstant());
+       stmt.setAuthnContext(authnCtx);
+       
+       // add the AuthnStatement to the Assertion
+       List<AuthnStatement> authnStatements = assertion.getAuthnStatements();
+       authnStatements.add(stmt);
+       
+       
+       // add the Assertion to the Response
+       List<Assertion> assertions = response.getAssertions();
+       assertions.add(assertion);
+    }
+    
+    
+    /**
+     * Create the AuthnContex object.
+     *
+     * To do this, we have to walk the AuthnRequest's RequestedAuthnContext object and compare
+     * any values we find to what's set in the loginContext.
+     *
+     * @param requestedAuthnCtx The RequestedAuthnContext from the Authentication Request.
+     * @param authnMethod The authentication method that was used.
+     *
+     * @return An AuthnCtx object on success or <code>null</code> on failure.
+     */
+    protected AuthnContext buildAuthnCtx(final RequestedAuthnContext requestedAuthnCtx, String authnMethod) {
+       
+       // this method assumes that only one URI will match.
+       
+       AuthnContext authnCtx = (AuthnCtx)authnContextBuilder.buildObject(AuthnContext.DEFAULT_ELEMENT_NAME);
+       String authnMethod = loginCtx.getAuthenticationMethod();
+       
+       List<AuthnContextClassRef> authnClasses = ctx.getAuthnContextClassRefs();
+        List<AuthnContextDeclRef> authnDeclRefs = ctx.getAuthnContextDeclRefs();
+    
+        if (authnClasses != null) {
+            for (AuthnContextClassRef classRef : authnClasses) {
+                if (classRef != null) {
+                    String s = classRef.getAuthnContextClassRef();
+                    if (s != null && authnMethod.equals(s)) {
+                        AuthnContextClassRef classRef
+                           = (AuthnContextClassRef)authnContextClassRefBuilder.buildObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
+                       authnCtx.setAuthnContextClassRef(classRef);
+                       return authnCtx;
                     }
                 }
             }
         }
-        
-        
-        // iterate over all the IDPEntries we've gathered, 
-        // and check if we're in scope.
-        boolean found = false;
-        for (String requestProviderId : idpEntries) {
-            if (providerId.equals(requestProviderId)) {
-                found = true;
-                log.debug("Found Scoping match for IdP: (" 
-                            + providerId + ")");
-                break;
+    
+       // if no AuthnContextClassRef's matched, try the DeclRefs
+        if (authnDeclRefs != null) {
+            for (AuthnContextDeclRef declRef : authnDeclRefs) {
+                if (declRef != null) {
+                    String s = declRef.getAuthnContextDeclRef();
+                    if (s != null && authnMethod.equals((s))) {
+                       AuthnContextDeclRef declRef
+                          = (AuthnContextDeclRef)authnContextDeclRefBuilder.buildObject(AuthnContextDeclRef.DEFAULT_ELEMENT_NAME);
+                       authnCtx.setAuthnContextDeclRef(declRef);
+                       return authnCtx;
+                   }
+                }
             }
         }
-        
-        return (found);
+       
+       // no matches were found.
+       return null;
+    }
+    
+    
+    /**
+     * Get the User's LoginContext.
+     *
+     * @param httpSession The user's HttpSession.
+     *
+     * @return The user's LoginContext.
+     *
+     * @throws ServletException On error.
+     */
+    protected LoginContext getLoginContext(final HttpSession httpSession) throws ServletException {
+       
+       Object o = httpSession.getAttribute(LoginContext.LOGIN_CONTEXT_KEY);
+       if (o == null) {
+           log.error("User's session does not contain a LoginContext object.");
+           throw new ServletException("User's session does not contain a LoginContext object.");
+       }
+       
+       if (! (o instanceof LoginContext)) {
+           log.error("Invalid login context object -- object is not an instance of LoginContext.");
+           throw new ServletException("Invalid login context object.");
+       }
+       
+       return (LoginContext)o;;
+    }
+    
+    
+    /**
+     * Verify the AuthnRequest is well-formed.
+     *
+     * On error, this method returns <code>false</code> and sets failureStatus.
+     *
+     * @param authnRequest The user's SAML 2 AuthnRequest.
+     * @param failureStatus On failure, this will be populated with appropriate status.
+     * 
+     * @return <code>true</code> if the message is well-formed. 
+     */
+    protected boolean verifyAuthnRequest(final AuthnRequest authnRequest, Status failureStatus) {
+       
+       // The Web Browser SSO profile requires that the Issuer element is present.
+       Issuer issuer = authnRequest.getIssuer();
+       if (issuer == null) {
+           log.error("Malformed SAML 2 AuthnReq - missing Issuer element.");
+           failureStatus = buildStatus(StatusCode.REQUESTER_URI, null,
+               "SAML 2 AuthnRequest " + authnRequest.getID() + " is malformed: It lacks an Issuer.");
+           return false;
+       }
+       
+       // Check if we are in scope to handle this AuthnRequest
+       // XXX: confirm that SPProviderID is the field we want in the issuer
+       if (!checkScope(authnRequest, issuer.getSPProvidedID(), failureStatus)) {
+           return false;
+       }
+       
+       // XXX: run signature checks on authnRequest
+       
+       // verify that the AssertionConsumerService url is valid.
+       AssertionConsumerService acsEndpoint = getAndVerifyACSEndpoint(authnRequest,
+           relyingParty.getRelyingPartyID(), rpManager.getMetadataProvider(), failureStatus);
+       if (acsEndpoint == null) {
+           return false;
+       }
+       
+       Subject subject = getAndVerifySubject(authnRequest, failureStatus);
+       if (subject == null) {
+           return false;
+       }
+       
+       // check for nameID constraints.
     }
+    
+    
+    /**
+     * Get and verify the Subject element.
+     *
+     * On error, this method will return <code>null</code> and set failureStatus.
+     *
+     * @param authnRequest The SAML 2 AuthnRequest.
+     * @param failureStatus On failure, this will be populated with appropriate status.
+     *
+     * @return A Subject element, if one was present, otherwise <code>null</code>
+     */
+    protected Subject getAndVerifySubject(final AuthnRequest authnRequest, Status failureStatus) {
+       
+       Subject subject = authnRequest.getSubject();
+       
+       if (subject == null) {
+           failureStatus = buildStatus(StatusCode.REQUESTER_URI, null,
+                   "SAML 2 AuthnRequest " + authnRequest.getID() + " is malformed: It does not contain a Subject.");
+           return null;
+       }
+       
+       // The Web Browser SSO profile disallows SubjectConfirmation methods
+       // in the requested subject.
+       List<SubjectConfirmation> confMethods = subject.getSubjectConfirmations();
+       if (confMethods != null || confMethods.length > 0) {
+           log.error("SAML 2 AuthnRequest " + authnRequest.getID()
+               + " is malformed: It contains SubjectConfirmation elements.");
+           failureStauts = buildStatus(StatusCode.REQUESTER_URI, null,
+               "SAML 2 AuthnRequest " + authnRequest.getID() + " is malformed: It contains SubjectConfirmation elements.");
+           return null;
+       }
+       
+       return subject;
+    }
+    
+    
+    /**
+     * Return the endpoint URL and protocol binding to use for the AuthnRequest.
+     *
+     * On error, this method will set failureStatus to point 
+     * to a failure status object and return <code>null</code>.
+     *
+     * @param authnRequest The SAML 2 AuthnRequest.
+     * @param providerId The SP's providerId.
+     * @param metadata The appropriate Metadata.
+     * @param failureStatus A failure status element. Will be populated on error.
+     *
+     * @return The AssertionConsumerService for the endpoint, or <code>null</code> on error.
+     *
+     * @throws ServletException On error.
+     */
+    protected AssertionConsumerService getAndVerifyACSEndpoint(final AuthnRequest authnRequest,
+           String providerId, final MetadataProvider metadata, Status failureStatus) {
+       
+       // Either the AssertionConsumerServiceIndex must be present
+       // or AssertionConsumerServiceURL must be present.
+       
+       Integer idx = authnRequest.getAssertionConsumerServiceIndex();
+       String acsURL = authnRequest.getAssertionConsumerServiceURL();
+       
+       if (idx != null && acsURL != null) {
+           log.error("SAML 2 AuthnRequest " + authnRequest.getID()
+               + " is malformed: It contains both an AssertionConsumerServiceIndex and an AssertionConsumerServiceURL");
+           failureStatus = buildStatus(StatusCode.REQUESTER_URI, null, "SAML 2 AuthnRequest " + authnRequest.getID()
+               + " is malformed: It contains both an AssertionConsumerServiceIndex and an AssertionConsumerServiceURL");
+           return null;
+       }
+       
+       SPSSODescriptor spDescriptor;
+       List<AssertionConsumerService> acsList;
+       try {
+           spDescriptor =
+                   metadata.getEntityDescriptor(providerId).getSPSSODescriptor(SAML20_PROTOCOL_URI);
+       } catch (MetadataProviderException ex) {
+           log.error("Unable retrieve SPSSODescriptor metadata for providerId "
+               + providerId + " while processing SAML 2 AuthnRequest " + authnRequest.getID(), ex);
+           failureStatus = buildStatus(StatusCode.RESPONDER_URI, null, "Unable to locate metadata for " + providerId);
+           return null;
+       }
+       
+       acsList = spDescriptor.getAssertionConsumerServices();
+       
+       // if the ACS index is specified, retrieve it from the metadata
+       if (idx != null) {
+           
+           int i = idx.intValue();
+           
+           // if the index is out of range, return an appropriate error.
+           if (i > acsList.length) {
+               log.error("Illegal AssertionConsumerIndex specicifed ("
+                   + i + ") in SAML 2 AuthnRequest " + authnRequest.getID());
+               
+               failureStatus = buildStatus(StatusCode.REQUESTER_URI, null, 
+                       "Illegal AssertionConsumerIndex specicifed (" + i 
+                       + ") in SAML 2 AuthnRequest " + authnRequest.getID());
+               
+               return null;
+           }
+           
+           return acsList.get(i);
+       }
+       
+       // if the ACS endpoint is specified, validate it against the metadata
+       String protocolBinding = authnRequest.getProtocolBinding();
+       for (AssertionConumerService acs : acsList) {
+           if (acsURL.equals(acs.getLocation())) {
+               if (protocolBinding != null) {
+                   if (protocolBinding.equals(acs.getBinding())) {
+                       return acs;
+                   }
+               }
+           }
+       }
+       
+       log.error("Error processing SAML 2 AuthnRequest message " + authnRequest.getID()
+           + ": Unable to validate AssertionConsumerServiceURL against metadata: "
+           + acsURL + " for binding " + protocolBinding);
+       
+       failureStatus = buildStatus(statusCodeBuilder.REQUESTER_URI, null,
+               "Unable to validate AssertionConsumerService against metadata");
 
+       return null;
+    }
+    
+    
+    /**
+     * Check if an {@link AuthnRequest} contains a {@link Scoping} element.
+     * If so, check if the specified IdP is in the {@link IDPList} element.
+     * If no Scoping element is present, this method returns <code>true</code>.
+     *
+     * If this method returns <code>false</code>, the caller 
+     * should check if <code>failureStatus</code> has been set.
+     *
+     * @param authnRequest The {@link AuthnRequest} element to check.
+     * @param providerId The IdP's ProviderID
+     * @param failureStatus Set to a failure code on error.
+     *
+     * @return <code>true</code>if idp is in the IDPList, otherwise <code>false</code>
+     */
+    protected boolean checkScope(final AuthnRequest authnRequest, String providerId, Status failureStatus) {
+       
+       List<String> idpEntries = new LinkedList<String>();
+               
+       Scoping scoping = authnRequest.getScoping();
+       if (scoping == null) {
+           return true;
+       }
+       
+       // process all of the explicitly listed idp provider ids
+       IDPList idpList = scoping.getIDPList();
+       if (idpList == null) {
+           return true;
+       }
+       
+       List<IDPEntry> explicitIDPEntries = idpList.getIDPEntrys();
+       if (explicitIDPEntries != null) {
+           for (IDPEntry entry : explicitIDPEntries) {
+               String s = entry.getProviderID();
+               if (s != null) {
+                   idpEntries.add(s);
+               }
+           }
+       }
+       
+       
+       // If the IDPList is incomplete, retrieve the complete list
+       // and add the entries to idpEntries.
+       GetComplete getComplete = idpList.getGetComplete();
+       IDPList referencedIdPs = getCompleteIDPList(getComplete);
+       if (referencedIdPs != null) {
+           List<IDPEntry> referencedIDPEntries = referencedIdPs.getIDPEntrys();
+           if (referencedIDPEntries != null) {
+               for (IDPEntry entry : referencedIDPEntries) {
+                   String s = entry.getProviderID();
+                   if (s != null) {
+                       idpEntries.add(s);
+                   }
+               }
+           }
+       }
+       
+       
+       // iterate over all the IDPEntries we've gathered,
+       // and check if we're in scope.
+       for (String requestProviderId : idpEntries) {
+           if (providerId.equals(requestProviderId)) {
+               found = true;
+               log.debug("Found Scoping match for IdP: (" + providerId + ")");
+               return true;
+           }
+       }
+       
+       log.error("SAML 2 AuthnRequest " + authnRequest.getID() + " contains a Scoping element which "
+               + "does not contain a providerID registered with this IdP.");
+       
+       failureStatus = buildStatus(StatusCode.RESPONDER_URI, StatusCode.NO_SUPPORTED_IDP_URI, null);
+       return false;
+    }
+    
     
     /**
      * Retrieve an incomplete IDPlist.
@@ -174,56 +686,115 @@ public class AuthenticationRequest implements ProfileHandler {
      *
      * @return an {@link IDPList} or <code>null</code> if the uri can't be dereferenced.
      */
-    private IDPList getCompleteIDPList(GetComplete getComplete) {
-        
-        // XXX: enhance this method to cache the url and last-modified-header
-        
-        if (getComplete == null) {
-            return (null);
-        }
-        
-        String uri = getComplete.getGetComplete();
-        if (uri != null) {
-            return (null);
-        }
-        
-        
-        IDPList idpList = null;
-        InputStream istream = null;
-        
-        try {
-            URL url = new URL(uri);
-            URLConnection conn = url.openConnection();
-            istream = conn.getInputStream();
-            
-            // convert the raw data into an XML object
-            DefaultBootstrap.bootstrap();
-            ParserPoolManager parserMgr = ParserPoolManager.getInstance();
-            Document doc = parserMgr.parse(new InputSource(istream));
-            Element docElement = doc.getDocumentElement();
-            Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(docElement);
-            idpList = (IDPList) unmarshaller.unmarshall(docElement);
-            
-        } catch (MalformedURLException ex) {
-            log.error("Unable to retrieve GetComplete IDPList. Unsupported URI: " + uri);
-        } catch (IOException ex) {
-            log.error("IO Error while retreieving GetComplete IDPList from " + uri, ex);
-        } catch (ConfigurationException ex) {
-            log.error("Internal OpenSAML error while parsing GetComplete IDPList from " + uri, ex);
-        } catch (XMLParserException ex) {
-            log.error("Internal OpenSAML error while parsing GetComplete IDPList from " + uri, ex);
-        } catch (UnmarshallingException ex) {
-            log.error("Internal OpenSAML error while unmarshalling GetComplete IDPList from " + uri, ex);
-        } finally {
-            if (istream != null) {
-                try {
-                    istream.close();
-                } catch (IOException ex) {
-                    // nothing to do here.
-                }
-            }
-        }
-        
-        return idpList;
+    protected IDPList getCompleteIDPList(final GetComplete getComplete) {
+       
+       // XXX: enhance this method to cache the url and last-modified-header
+       
+       if (getComplete == null) {
+           return null;
+       }
+       
+       String uri = getComplete.getGetComplete();
+       if (uri != null) {
+           return null;
+       }
+       
+       IDPList idpList = null;
+       InputStream istream = null;
+       
+       try {
+           URL url = new URL(uri);
+           URLConnection conn = url.openConnection();
+           istream = conn.getInputStream();
+           
+           // convert the raw data into an XML object
+           Document doc = parserPool.parse(istream);
+           Element docElement = doc.getDocumentElement();
+           Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(docElement);
+           idpList = (IDPList) unmarshaller.unmarshall(docElement);
+           
+       } catch (MalformedURLException ex) {
+           log.error("Unable to retrieve GetComplete IDPList. Unsupported URI: " + uri, ex);
+       } catch (IOException ex) {
+           log.error("IO Error while retreieving GetComplete IDPList from " + uri, ex);
+       } catch (ConfigurationException ex) {
+           log.error("Internal OpenSAML error while parsing GetComplete IDPList from " + uri, ex);
+       } catch (XMLParserException ex) {
+           log.error("Internal OpenSAML error while parsing GetComplete IDPList from " + uri, ex);
+       } catch (UnmarshallingException ex) {
+           log.error("Internal OpenSAML error while unmarshalling GetComplete IDPList from " + uri, ex);
+       } finally {
+           
+           if (istream != null) {
+               try {
+                   istream.close();
+               } catch (IOException ex) {
+                   // nothing to do here.
+               }
+           }
+       }
+       
+       return idpList;
+    }
+    
+    
+    /**
+     * Build a SAML 2 Response element with basic fields populated.
+     *
+     * Failure handlers can send the returned response element to the RP. 
+     * Success handlers should add the assertions before sending it.
+     *
+     * @param inResponseTo The ID of the request this is in response to.
+     * @param issuer The URI of the RP issuing the response.
+     * @param status The response's status code.
+     *
+     * @return The populated response object. 
+     */
+    protected Response buildResponse(String inResponseTo, String issuer, final Status status) {
+       
+       Response response = (Response)responseBuilder.buildObject(Response.DEFAULT_ELEMENT_NAME);
+       
+       Issuer i = (Issuer)issuerBuilder.buildObject(Issuer.DEFAULT_ELEMENT_NAME);
+       i.setValue(issuer);
+       
+       response.setVersion(SAML_VERSION);
+       response.setId(getIdGenerator().generateIdentifier());
+       response.setInResponseto(inResponseTo);
+       response.setIssueInstance(new DateTime());
+       response.setIssuer(i);
+       response.setStatus(status);
+       
+       return response;
+    }
+    
+    
+    /**
+     * Build a status message, with an optional second-level failure message.
+     *
+     * @param topLevelCode The top-level status code. Should be from saml-core-2.0-os, sec. 3.2.2.2
+     * @param secondLevelCode An optional second-level failure code. Should be from saml-core-2.0-is, sec 3.2.2.2.
+     *                 If null, no second-level Status element will be set.
+     * @param secondLevelFailureMessage An optional second-level failure message.
+     * @return a Status object.
+     */
+    protected Status buildStatus(String topLevelCode,
+           String secondLevelCode, String secondLevelFailureMessage) {
+       
+       Status status = (Status)statusBuilder.buildObject(Status.DEFAULT_ELEMENT_NAME);
+       StatusCode statusCode = (StatusCode)statusCodeBuilder.buildObject(StatusCode.DEFAULT_ELEMENT_NAME);
+       
+       statusCode.setValue(topLevelCode);
+       if (secondLevelCode != null) {
+           StatusCode secondLevelStatusCode = (StatusCode)statusCodeBuilder.buildObject(StatusCode.DEFAULT_ELEMENT_NAME);
+           secondLevelStatusCode.setValue(secondLevelCode);
+           statusCode.setStatusCode(secondLevelStatusCode);
+       }
+       
+       if (secondLevelFailureMessage != null) {
+           StatusMessage msg = (StatusMessage)statusMessageBuilder.buildObject(StatusMessage.DEFAULT_ELEMENT_NAME);
+           msg.setMessage(secondLevelFailureMessage);
+       }
+       
+       return status;
     }
 }