Removed println.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / E_AuthSSOHandler.java
index 550e2d5..6fea7d7 100644 (file)
 package edu.internet2.middleware.shibboleth.idp.provider;
 
 import java.io.IOException;
+import java.net.URLEncoder;
+import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Vector;
 
 import javax.servlet.ServletException;
@@ -48,37 +52,55 @@ import org.opensaml.SAMLRequest;
 import org.opensaml.SAMLResponse;
 import org.opensaml.SAMLStatement;
 import org.opensaml.SAMLSubject;
-import org.w3c.dom.Document;
+import org.opensaml.artifact.Artifact;
 import org.w3c.dom.Element;
 
-import sun.misc.BASE64Decoder;
+import edu.internet2.middleware.shibboleth.aa.AAException;
+import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
+import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
 import edu.internet2.middleware.shibboleth.common.RelyingParty;
 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
+import edu.internet2.middleware.shibboleth.metadata.Endpoint;
+import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
+import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
 
 /**
+ * <code>ProtocolHandler</code> implementation that responds to SSO flows as specified in "E-Authentication Interface
+ * Specifications for the SAML Artifact Profile ".
+ * 
  * @author Walter Hoehn
  */
-public class E_AuthSSOHandler extends BaseHandler implements IdPProtocolHandler {
+public class E_AuthSSOHandler extends SSOHandler implements IdPProtocolHandler {
 
        private static Logger log = Logger.getLogger(E_AuthSSOHandler.class.getName());
-       private final String name = "EAuth";
-       private final String eAuthPortal = "http://eauth.firstgov.gov/service/select";
-       private final String eAuthFed = "urn:mace:shibboleth:eAuthFed";
+       private String eAuthPortal = "http://eauth.firstgov.gov/service/select";
+       private String eAuthError = "http://eauth.firstgov.gov/service/error";
        private String csid;
 
-       // TODO validate that the target wants artifact, since it is required for this profile
-       // TODO validate that we aren't using signatures
-       // TODO validate that we are using the right nameIdentifier format
-       // TODO more robust attribute values before we ship
        /**
         * Required DOM-based constructor.
         */
        public E_AuthSSOHandler(Element config) throws ShibbolethConfigurationException {
 
                super(config);
+               csid = config.getAttribute("csid");
+               if (csid == null || csid.equals("")) {
+                       log.error("(csid) attribute is required for the " + getHandlerName() + "protocol handler.");
+                       throw new ShibbolethConfigurationException("Unable to initialize protocol handler.");
+               }
+
+               String portal = config.getAttribute("eAuthPortal");
+               if (portal != null && !portal.equals("")) {
+                       eAuthPortal = portal;
+               }
+
+               String error = config.getAttribute("eAuthError");
+               if (error != null && !error.equals("")) {
+                       eAuthError = portal;
+               }
        }
 
        /*
@@ -98,7 +120,7 @@ public class E_AuthSSOHandler extends BaseHandler implements IdPProtocolHandler
                        SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
 
                // Sanity check
-               if (request != null) {
+               if (samlRequest != null) {
                        log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
                        throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
                }
@@ -126,20 +148,84 @@ public class E_AuthSSOHandler extends BaseHandler implements IdPProtocolHandler
                                                        + request.getQueryString().replaceAll("(^sessionreset=1&?|&?sessionreset=1)", "") : ""));
                        return null;
                }
+               // Sanity check
+               try {
+                       validateEngineData(request);
+               } catch (InvalidClientDataException e) {
+                       throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
+               }
+
+               // Get the authN info
+               String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
+                               .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
+               if ((username == null) || (username.equals(""))) {
+                       log.error("Unable to authenticate remote user.");
+                       throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+               }
+               AuthNPrincipal principal = new AuthNPrincipal(username);
+
+               // Select the appropriate Relying Party configuration for the request
+               String remoteProviderId = request.getParameter("aaid");
+               log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
+               RelyingParty relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
+
+               if (relyingParty == null || relyingParty.isLegacyProvider()) {
+                       log.error("Unable to identify appropriate relying party configuration.");
+                       eAuthError(response, 30, remoteProviderId, csid);
+                       return null;
+               }
+
+               // Lookup the provider in the metadata
+               EntityDescriptor entity = support.lookup(relyingParty.getProviderId());
+               if (entity == null) {
+                       log.error("No metadata found for EAuth provider.");
+                       eAuthError(response, 30, remoteProviderId, csid);
+                       return null;
+               }
+               SPSSODescriptor role = entity.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
+               if (role == null) {
+                       log.error("Inappropriate metadata for EAuth provider.");
+                       eAuthError(response, 30, remoteProviderId, csid);
+                       return null;
+               }
+
+               // The EAuth profile requires metadata, since the assertion consumer is not supplied as a request parameter
+               // Pull the consumer URL from the metadata
+               Iterator endpoints = role.getAssertionConsumerServiceManager().getEndpoints();
+               if (endpoints == null || !endpoints.hasNext()) {
+                       log.error("Inappropriate metadata for provider: no roles specified.");
+                       eAuthError(response, 30, remoteProviderId, csid);
+                       return null;
+               }
+               String consumerURL = ((Endpoint) endpoints.next()).getLocation();
+               log.debug("Assertion Consumer URL provider: " + consumerURL);
 
+               // Create SAML Name Identifier & Subject
+               SAMLNameIdentifier nameId;
                try {
-                       IdPProtocolSupport.validateEngineData(request);
-               } catch (InvalidClientDataException e1) {
-                       // TODO Auto-generated catch block
+                       // TODO verify that the nameId is the right format here and error if not
+                       nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(), principal,
+                                       relyingParty, relyingParty.getIdentityProvider());
+               } catch (NameIdentifierMappingException e) {
+                       log.error("Error converting principal to SAML Name Identifier: " + e);
+                       eAuthError(response, 60, remoteProviderId, csid);
+                       return null;
                }
 
-               // TODO figure this out
-               RelyingParty relyingParty = null;
-               SAMLNameIdentifier nameId = null;
-               String authenticationMethod = null;
-               Date authTime = null;
+               String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
+               SAMLSubject authNSubject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
+
+               // Determine AuthN method
+               String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
+               if (authenticationMethod == null || authenticationMethod.equals("")) {
+                       authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
+                       log.debug("User was authenticated via the default method for this relying party (" + authenticationMethod
+                                       + ").");
+               } else {
+                       log.debug("User was authenticated via the method (" + authenticationMethod + ").");
+               }
 
-               Document doc = org.opensaml.XML.parserPool.newDocument();
+               // Generate SAML audiences
                ArrayList audiences = new ArrayList();
                if (relyingParty.getProviderId() != null) {
                        audiences.add(relyingParty.getProviderId());
@@ -147,93 +233,149 @@ public class E_AuthSSOHandler extends BaseHandler implements IdPProtocolHandler
                if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
                        audiences.add(relyingParty.getName());
                }
-               String issuer = relyingParty.getIdentityProvider().getProviderId();
                Vector conditions = new Vector(1);
                if (audiences != null && audiences.size() > 0) {
                        conditions.add(new SAMLAudienceRestrictionCondition(audiences));
                }
 
-               // TODO need to pull this out into the generic artifact handling
-               String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
-               SAMLSubject subject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
-               // TODO pull from authN system? or make configurable
-               ArrayList attributes = new ArrayList();
-               attributes.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
-                               0, Arrays.asList(new String[]{"2"})));
+               String issuer = relyingParty.getIdentityProvider().getProviderId();
 
-               // TODO Hack Alert!!!
-               // Pull attributes from AA
-               String hackFullName = null;
-               if (nameId.getName().startsWith("uid=tomcat")) {
-                       hackFullName = "Tomcat Test User";
-               } else if (nameId.getName().startsWith("uid=nfaut")) {
-                       hackFullName = "Nathan Faut";
-               } else if (nameId.getName().startsWith("uid=wassa")) {
-                       hackFullName = "Walter F. Hoehn, Jr.";
-               } else if (nameId.getName().startsWith("uid=mtebo")) {
-                       hackFullName = "Matt Tebo";
-               } else if (nameId.getName().startsWith("uid=dblanchard")) {
-                       hackFullName = "Deb Blanchard";
-               } else if (nameId.getName().startsWith("uid=rweiser")) {
-                       hackFullName = "Russ Weiser";
-               } else if (nameId.getName().startsWith("uid=scarmody")) {
-                       hackFullName = "Steven Carmody";
-               }
-               attributes.add(new SAMLAttribute("commonName", "http://eauthentication.gsa.gov/federated/attribute", null, 0,
-                               Arrays.asList(new String[]{hackFullName})));
-               attributes.add(new SAMLAttribute("csid", "http://eauthentication.gsa.gov/federated/attribute", null, 0, Arrays
-                               .asList(new String[]{csid})));
+               log.info("Resolving attributes.");
+               List attributes = null;
                try {
-                       SAMLStatement[] statements = {
-                                       new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request.getRemoteAddr(),
-                                                       null, null), new SAMLAttributeStatement((SAMLSubject) subject.clone(), attributes)};
-                       SAMLAssertion[] assertions = {new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
-                                       System.currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements))};
-                       if (log.isDebugEnabled()) {
-                               log.debug("Dumping generated SAML Assertions:"
-                                               + System.getProperty("line.separator")
-                                               + new String(new BASE64Decoder().decodeBuffer(new String(assertions[0].toBase64(), "ASCII")),
-                                                               "UTF8"));
-                       }
+                       attributes = Arrays.asList(support.getReleaseAttributes(principal, relyingParty.getProviderId(), null));
+               } catch (AAException e1) {
+                       log.error("Error resolving attributes: " + e1);
+                       eAuthError(response, 90, remoteProviderId, csid);
+                       return null;
+               }
+               log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
+
+               // Bail if we didn't get any attributes
+               if (attributes == null || attributes.size() < 1) {
+                       log.error("Attribute resolver did not return any attributes. "
+                                       + " The E-Authentication profile's minimum attribute requirements were not met.");
+                       eAuthError(response, 60, remoteProviderId, csid);
                        return null;
-               } catch (CloneNotSupportedException e) { // TODO handle return null; } }
 
+                       // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
+                       // assertion
+               } else {
+                       try {
+                               attributes = repackageForEauth(attributes);
+                       } catch (SAMLException e) {
+                               eAuthError(response, 90, remoteProviderId, csid);
+                               return null;
+                       }
+
+                       // Put all attributes into an assertion
+                       try {
+                               // TODO provide a way to override authN time
+                               SAMLStatement attrStatement = new SAMLAttributeStatement((SAMLSubject) authNSubject.clone(), attributes);
+                               SAMLStatement[] statements = {
+                                               new SAMLAuthenticationStatement(authNSubject, authenticationMethod, new Date(System
+                                                               .currentTimeMillis()), request.getRemoteAddr(), null, null), attrStatement};
+                               SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
+                                               System.currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
+                               if (log.isDebugEnabled()) {
+                                       log.debug("Dumping generated SAML Assertion:" + System.getProperty("line.separator")
+                                                       + assertion.toString());
+                               }
+
+                               // Redirect to agency application
+                               try {
+                                       respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
+                                                       relyingParty);
+                                       return null;
+                               } catch (SAMLException e) {
+                                       eAuthError(response, 90, remoteProviderId, csid);
+                                       return null;
+                               }
+
+                       } catch (CloneNotSupportedException e) {
+                               log.error("An error was encountered while generating assertion: " + e);
+                               eAuthError(response, 90, remoteProviderId, csid);
+                               return null;
+                       }
                }
+       }
 
-               return null;
+       private void respondWithArtifact(HttpServletResponse response, IdPProtocolSupport support, String acceptanceURL,
+                       Principal principal, SAMLAssertion assertion, SAMLNameIdentifier nameId, SPSSODescriptor descriptor,
+                       RelyingParty relyingParty) throws SAMLException, IOException {
+
+               // Create artifacts for each assertion
+               ArrayList artifacts = new ArrayList();
+
+               artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
+
+               String target = relyingParty.getDefaultTarget();
+               if (target == null || target.equals("")) {
+                       log.error("No default target found.  Relying Party elements corresponding to "
+                                       + "E-Authentication providers must have a (defaultTarget) attribute specified.");
+                       throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+               }
+
+               // Assemble the query string
+               StringBuffer destination = new StringBuffer(acceptanceURL);
+               destination.append("?TARGET=");
+               destination.append(URLEncoder.encode(target, "UTF-8"));
+               Iterator iterator = artifacts.iterator();
+               StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
+
+               // Construct the artifact query parameter
+               while (iterator.hasNext()) {
+                       Artifact artifact = (Artifact) iterator.next();
+                       artifactBuffer.append("(" + artifact.encode() + ")");
+                       destination.append("&SAMLart=");
+                       destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
+               }
+
+               log.debug("Redirecting to (" + destination.toString() + ").");
+               response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
+               support.getTransactionLog().info(
+                               "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to E-Authentication provider ("
+                                               + relyingParty.getProviderId() + ") on behalf of principal ("
+                                               + principal.getName() + "). Name Identifier: (" + nameId.getName()
+                                               + "). Name Identifier Format: (" + nameId.getFormat() + ").");
 
        }
 
-       /*
-        * EAuthProfileHandler(String csid) throws ShibbolethConfigurationException { if (csid == null) { throw new
-        * ShibbolethConfigurationException( "EAuth support is enabled, but no (csid) parameter has been configured."); }
-        * this.csid = csid; }
-        */
+       private List repackageForEauth(List attributes) throws SAMLException {
 
-       /*
-        * String getRemoteProviderId(HttpServletRequest req) { return req.getParameter("aaid"); }
-        */
+               ArrayList  writeable = new ArrayList(attributes); 
+               // Bail if we didn't get a commonName, because it is required by the profile
+               SAMLAttribute commonName = getAttribute("commonName", writeable);
+               if (commonName == null) {
+                       log.error("The attribute resolver did not return a (commonName) attribute, "
+                                       + " which is required for the E-Authentication profile.");
+                       throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+               } else {
+                       // This namespace is required by the eAuth profile
+                       commonName.setNamespace("http://eauthentication.gsa.gov/federated/attribute");
+                       // TODO Maybe the resolver should set this
+               }
+               writeable.add(new SAMLAttribute("csid", "http://eauthentication.gsa.gov/federated/attribute", null, 0, Arrays
+                               .asList(new String[]{csid})));
+               // TODO pull from authN system? or make configurable
+               writeable.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
+                               0, Arrays.asList(new String[]{"2"})));
+               return writeable;
+       }
 
-       /*
-        * String getSAMLTargetParameter(HttpServletRequest request, RelyingParty relyingParty, Provider provider) {
-        * ProviderRole[] roles = provider.getRoles(); if (roles.length == 0) { log.error("Inappropriate metadata for EAuth
-        * provider."); return null; } for (int i = 0; roles.length > i; i++) { if (roles[i] instanceof SPProviderRole) {
-        * return ((SPProviderRole) roles[i]).getTarget(); } } log.error("Inappropriate metadata for EAuth provider.");
-        * return null; }
-        */
+       private SAMLAttribute getAttribute(String name, List attributes) {
 
-       /*
-        * internet2.middleware.shibboleth.hs.HSRelyingParty) String getAcceptanceURL(HttpServletRequest request,
-        * HSRelyingParty relyingParty, Provider provider) throws InvalidClientDataException { //The EAuth profile requires
-        * metadata, since the assertion consumer is not supplied as a request parameter if (provider == null) {
-        * log.error("Unkown requesting service provider (" + relyingParty.getProviderId() + ")."); throw new
-        * InvalidClientDataException("Unkown requesting service provider."); } //Pull the consumer URL from the metadata
-        * ProviderRole[] roles = provider.getRoles(); if (roles.length == 0) { log.info("Inappropriate metadata for
-        * provider: no roles specified."); throw new InvalidClientDataException("Invalid metadata for requesting service
-        * provider."); } for (int i = 0; roles.length > i; i++) { if (roles[i] instanceof SPProviderRole) { Endpoint[]
-        * endpoints = ((SPProviderRole) roles[i]).getAssertionConsumerServiceURLs(); if (endpoints.length > 0) { return
-        * endpoints[0].getLocation(); } } } log.info("Inappropriate metadata for provider: no roles specified."); throw new
-        * InvalidClientDataException("Invalid metadata for requesting service provider."); }
-        */
+               Iterator iterator = attributes.iterator();
+               while (iterator.hasNext()) {
+                       SAMLAttribute attribute = (SAMLAttribute) iterator.next();
+                       if (attribute.getName().equals(name)) { return attribute; }
+               }
+               return null;
+       }
 
+       private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
+
+               log.info("Redirecting to E-Authentication error page.");
+               response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);
+       }
 }
\ No newline at end of file