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;
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 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 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 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
+ // TODO profile-specific error handling
+
/**
* 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;
+ }
}
/*
+ request.getQueryString().replaceAll("(^sessionreset=1&?|&?sessionreset=1)", "") : ""));
return null;
}
-
+ // Sanity check
try {
validateEngineData(request);
- } catch (InvalidClientDataException e1) {
- // TODO Auto-generated catch block
+ } catch (InvalidClientDataException e) {
+ throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
}
- // TODO figure this out
- RelyingParty relyingParty = null;
- SAMLNameIdentifier nameId = null;
- String authenticationMethod = null;
- Date authTime = null;
+ // 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.");
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+ }
+
+ // Lookup the provider in the metadata
+ EntityDescriptor entity = support.lookup(relyingParty.getProviderId());
+ if (entity == null) {
+ log.error("No metadata found for EAuth provider.");
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+ }
+ SPSSODescriptor role = entity.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
+ if (role == null) {
+ log.error("Inappropriate metadata for EAuth provider.");
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+ }
+
+ // 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.getAttributeConsumingServices();
+ if (!endpoints.hasNext()) {
+ log.error("Inappropriate metadata for provider: no roles specified.");
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+ }
+ String consumerURL = ((Endpoint) endpoints.next()).getLocation();
+ log.debug("Assertion Consumer URL provider: " + consumerURL);
- Document doc = org.opensaml.XML.parserPool.newDocument();
+ // Create SAML Name Identifier & Subject
+ SAMLNameIdentifier nameId;
+ try {
+ nameId = support.getNameMapper().getNameIdentifierName(
+ "urn:oasis:names:tc:SAML:1.0:assertion#X509SubjectName", principal, relyingParty,
+ relyingParty.getIdentityProvider());
+ } catch (NameIdentifierMappingException e) {
+ log.error("Error converting principal to SAML Name Identifier: " + e);
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+ }
+
+ 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 + ").");
+ }
+
+ // Generate SAML audiences
ArrayList audiences = new ArrayList();
if (relyingParty.getProviderId() != null) {
audiences.add(relyingParty.getProviderId());
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")
- + assertions[0].toString());
+ attributes = Arrays.asList(support.getReleaseAttributes(principal, relyingParty.getProviderId(), null));
+ } catch (AAException e1) {
+ log.error("Error resolving attributes: " + e1);
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+ }
+ 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.");
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
+
+ // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
+ // assertion
+ } else {
+ repackageForEauth(attributes);
+
+ // 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
+ respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role, relyingParty);
+ return null;
+
+ } catch (CloneNotSupportedException e) {
+ log.error("An error was encountered while generating assertion: " + e);
+ throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
}
- return null;
- } catch (CloneNotSupportedException e) { // TODO handle 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.");
}
- return null;
+ // 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 + ")");
+ destination.append("&SAMLart=");
+ destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
+ }
- /*
- * 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; }
- */
+ 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.getIdentityProvider().getProviderId() + ") on behalf of principal ("
+ + principal.getName() + "). Name Identifier: (" + nameId.getName()
+ + "). Name Identifier Format: (" + nameId.getFormat() + ").");
- /*
- * String getRemoteProviderId(HttpServletRequest req) { return req.getParameter("aaid"); }
- */
+ }
- /*
- * 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 void repackageForEauth(List attributes) throws SAMLException {
- /*
- * 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."); }
- */
+ // Bail if we didn't get a commonName, because it is required by the profile
+ SAMLAttribute commonName = getAttribute("commonName", attributes);
+ 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
+ }
+ attributes.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
+ attributes.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
+ 0, Arrays.asList(new String[]{"2"})));
+ }
+ private SAMLAttribute getAttribute(String name, List attributes) {
+
+ Iterator iterator = attributes.iterator();
+ while (iterator.hasNext()) {
+ SAMLAttribute attribute = (SAMLAttribute) iterator.next();
+ if (attribute.getName().equals(name)) { return attribute; }
+ }
+ return null;
+ }
}
\ No newline at end of file