2 * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3 * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4 * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5 * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6 * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7 * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8 * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
9 * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10 * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11 * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12 * products derived from this software without specific prior written permission. For written permission, please contact
13 * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14 * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15 * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16 * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18 * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19 * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 package edu.internet2.middleware.shibboleth.idp.provider;
28 import java.io.IOException;
29 import java.net.URLEncoder;
30 import java.security.Principal;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Date;
34 import java.util.Iterator;
35 import java.util.List;
37 import javax.servlet.ServletException;
38 import javax.servlet.http.Cookie;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
42 import org.apache.log4j.Logger;
43 import org.opensaml.SAMLAssertion;
44 import org.opensaml.SAMLAttribute;
45 import org.opensaml.SAMLAttributeStatement;
46 import org.opensaml.SAMLAuthenticationStatement;
47 import org.opensaml.SAMLException;
48 import org.opensaml.SAMLNameIdentifier;
49 import org.opensaml.SAMLRequest;
50 import org.opensaml.SAMLResponse;
51 import org.opensaml.SAMLStatement;
52 import org.opensaml.SAMLSubject;
53 import org.opensaml.artifact.Artifact;
54 import org.w3c.dom.Element;
56 import edu.internet2.middleware.shibboleth.aa.AAException;
57 import edu.internet2.middleware.shibboleth.common.LocalPrincipal;
58 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
59 import edu.internet2.middleware.shibboleth.common.RelyingParty;
60 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
61 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
62 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
63 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
64 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
65 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
66 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
69 * <code>ProtocolHandler</code> implementation that responds to SSO flows as specified in "E-Authentication Interface
70 * Specifications for the SAML Artifact Profile ".
72 * @author Walter Hoehn
74 public class E_AuthSSOHandler extends SSOHandler implements IdPProtocolHandler {
76 private static Logger log = Logger.getLogger(E_AuthSSOHandler.class.getName());
77 private String eAuthPortal = "http://eauth.firstgov.gov/service/select";
78 private String eAuthError = "http://eauth.firstgov.gov/service/error";
82 * Required DOM-based constructor.
84 public E_AuthSSOHandler(Element config) throws ShibbolethConfigurationException {
87 csid = config.getAttribute("csid");
88 if (csid == null || csid.equals("")) {
89 log.error("(csid) attribute is required for the " + getHandlerName() + "protocol handler.");
90 throw new ShibbolethConfigurationException("Unable to initialize protocol handler.");
93 String portal = config.getAttribute("eAuthPortal");
94 if (portal != null && !portal.equals("")) {
98 String error = config.getAttribute("eAuthError");
99 if (error != null && !error.equals("")) {
105 * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#getHandlerName()
107 public String getHandlerName() {
109 return "E-Authentication SSO";
113 * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
114 * javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
115 * edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport)
117 public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
118 SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
121 if (samlRequest != null) {
122 log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
123 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
126 // If no aaid is specified, redirect to the eAuth portal
127 if (request.getParameter("aaid") == null || request.getParameter("aaid").equals("")) {
128 log.debug("Received an E-Authentication request with no (aaid) parameter. "
129 + "Redirecting to the E-Authentication portal.");
130 response.sendRedirect(eAuthPortal + "?csid=" + csid);
134 // FUTURE at some point this needs to be integrated with SAML2 session reset
135 // If session reset was requested, delete the session and re-direct back
136 // Note, this only works with servler form-auth
137 String reAuth = request.getParameter("sessionreset");
138 if (reAuth != null && reAuth.equals("1")) {
139 log.debug("E-Authebtication session reset requested.");
140 Cookie session = new Cookie("JSESSIONID", null);
141 session.setMaxAge(0);
142 response.addCookie(session);
144 response.sendRedirect(request.getRequestURI()
145 + (request.getQueryString() != null ? "?"
146 + request.getQueryString().replaceAll("(^sessionreset=1&?|&?sessionreset=1)", "") : ""));
151 validateEngineData(request);
152 } catch (InvalidClientDataException e) {
153 throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
156 // Get the authN info
157 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
158 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
159 if ((username == null) || (username.equals(""))) {
160 log.error("Unable to authenticate remote user.");
161 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
163 LocalPrincipal principal = new LocalPrincipal(username);
165 // Select the appropriate Relying Party configuration for the request
166 String remoteProviderId = request.getParameter("aaid");
167 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
168 RelyingParty relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
170 if (relyingParty == null || relyingParty.isLegacyProvider()) {
171 log.error("Unable to identify appropriate relying party configuration.");
172 eAuthError(response, 30, remoteProviderId, csid);
176 // Lookup the provider in the metadata
177 EntityDescriptor entity = support.lookup(relyingParty.getProviderId());
178 if (entity == null) {
179 log.error("No metadata found for EAuth provider.");
180 eAuthError(response, 30, remoteProviderId, csid);
183 SPSSODescriptor role = entity.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
185 log.error("Inappropriate metadata for EAuth provider.");
186 eAuthError(response, 30, remoteProviderId, csid);
190 // The EAuth profile requires metadata, since the assertion consumer is not supplied as a request parameter
191 // Pull the consumer URL from the metadata
192 Iterator endpoints = role.getAssertionConsumerServiceManager().getEndpoints();
193 if (endpoints == null || !endpoints.hasNext()) {
194 log.error("Inappropriate metadata for provider: no roles specified.");
195 eAuthError(response, 30, remoteProviderId, csid);
198 String consumerURL = ((Endpoint) endpoints.next()).getLocation();
199 log.debug("Assertion Consumer URL provider: " + consumerURL);
201 // Create SAML Name Identifier & Subject
202 SAMLNameIdentifier nameId;
204 nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(), principal,
205 relyingParty, relyingParty.getIdentityProvider());
206 if (!nameId.getFormat().equals(SAMLNameIdentifier.FORMAT_X509)) {
207 log.error("SAML Name Identifier format is inappropriate for use with E-Authentication provider. Was ("
208 + nameId.getFormat() + "). Expected (" + SAMLNameIdentifier.FORMAT_X509 + ").");
209 eAuthError(response, 60, remoteProviderId, csid);
212 } catch (NameIdentifierMappingException e) {
213 log.error("Error converting principal to SAML Name Identifier: " + e);
214 eAuthError(response, 60, remoteProviderId, csid);
218 String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
219 SAMLSubject authNSubject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
221 // Determine AuthN method
222 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
223 if (authenticationMethod == null || authenticationMethod.equals("")) {
224 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
225 log.debug("User was authenticated via the default method for this relying party (" + authenticationMethod
228 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
231 String issuer = relyingParty.getIdentityProvider().getProviderId();
233 log.info("Resolving attributes.");
234 List attributes = null;
236 attributes = Arrays.asList(support.getReleaseAttributes(principal, relyingParty, relyingParty
237 .getProviderId(), null));
238 } catch (AAException e1) {
239 log.error("Error resolving attributes: " + e1);
240 eAuthError(response, 90, remoteProviderId, csid);
243 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
245 // Bail if we didn't get any attributes
246 if (attributes == null || attributes.size() < 1) {
247 log.error("Attribute resolver did not return any attributes. "
248 + " The E-Authentication profile's minimum attribute requirements were not met.");
249 eAuthError(response, 60, remoteProviderId, csid);
252 // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
256 attributes = repackageForEauth(attributes);
257 } catch (SAMLException e) {
258 eAuthError(response, 90, remoteProviderId, csid);
262 // Put all attributes into an assertion
264 // TODO provide a way to override authN time
265 SAMLStatement attrStatement = new SAMLAttributeStatement((SAMLSubject) authNSubject.clone(), attributes);
266 SAMLStatement[] statements = {
267 new SAMLAuthenticationStatement(authNSubject, authenticationMethod, new Date(System
268 .currentTimeMillis()), request.getRemoteAddr(), null, null), attrStatement};
269 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
270 System.currentTimeMillis() + 300000), null, null, Arrays.asList(statements));
271 if (log.isDebugEnabled()) {
272 log.debug("Dumping generated SAML Assertion:" + System.getProperty("line.separator")
273 + assertion.toString());
276 // Redirect to agency application
278 respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
281 } catch (SAMLException e) {
282 eAuthError(response, 90, remoteProviderId, csid);
286 } catch (CloneNotSupportedException e) {
287 log.error("An error was encountered while generating assertion: " + e);
288 eAuthError(response, 90, remoteProviderId, csid);
294 private void respondWithArtifact(HttpServletResponse response, IdPProtocolSupport support, String acceptanceURL,
295 Principal principal, SAMLAssertion assertion, SAMLNameIdentifier nameId, SPSSODescriptor descriptor,
296 RelyingParty relyingParty) throws SAMLException, IOException {
298 // Create artifacts for each assertion
299 ArrayList artifacts = new ArrayList();
301 artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
303 String target = relyingParty.getDefaultTarget();
304 if (target == null || target.equals("")) {
305 log.error("No default target found. Relying Party elements corresponding to "
306 + "E-Authentication providers must have a (defaultTarget) attribute specified.");
307 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
310 // Assemble the query string
311 StringBuffer destination = new StringBuffer(acceptanceURL);
312 destination.append("?TARGET=");
313 destination.append(URLEncoder.encode(target, "UTF-8"));
314 Iterator iterator = artifacts.iterator();
315 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
317 // Construct the artifact query parameter
318 while (iterator.hasNext()) {
319 Artifact artifact = (Artifact) iterator.next();
320 artifactBuffer.append("(" + artifact.encode() + ")");
321 destination.append("&SAMLart=");
322 destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
325 log.debug("Redirecting to (" + destination.toString() + ").");
326 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
327 support.getTransactionLog().info(
328 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to E-Authentication provider ("
329 + relyingParty.getProviderId() + ") on behalf of principal (" + principal.getName()
330 + "). Name Identifier: (" + nameId.getName() + "). Name Identifier Format: ("
331 + nameId.getFormat() + ").");
335 private List repackageForEauth(List attributes) throws SAMLException {
337 ArrayList writeable = new ArrayList(attributes);
338 // Bail if we didn't get a commonName, because it is required by the profile
339 SAMLAttribute commonName = getAttribute("commonName", writeable);
340 if (commonName == null) {
341 log.error("The attribute resolver did not return a (commonName) attribute, "
342 + " which is required for the E-Authentication profile.");
343 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
345 // This namespace is required by the eAuth profile
346 commonName.setNamespace("http://eauthentication.gsa.gov/federated/attribute");
347 // TODO Maybe the resolver should set this
349 writeable.add(new SAMLAttribute("csid", "http://eauthentication.gsa.gov/federated/attribute", null, 0, Arrays
350 .asList(new String[]{csid})));
351 // TODO pull from authN system? or make configurable
352 writeable.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
353 0, Arrays.asList(new String[]{"2"})));
357 private SAMLAttribute getAttribute(String name, List attributes) {
359 Iterator iterator = attributes.iterator();
360 while (iterator.hasNext()) {
361 SAMLAttribute attribute = (SAMLAttribute) iterator.next();
362 if (attribute.getName().equals(name)) { return attribute; }
367 private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
369 log.info("Redirecting to E-Authentication error page.");
370 response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);