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 // TODO verify that the nameId is the right format here and error if not
205 nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(), principal,
206 relyingParty, relyingParty.getIdentityProvider());
207 } catch (NameIdentifierMappingException e) {
208 log.error("Error converting principal to SAML Name Identifier: " + e);
209 eAuthError(response, 60, remoteProviderId, csid);
213 String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
214 SAMLSubject authNSubject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
216 // Determine AuthN method
217 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
218 if (authenticationMethod == null || authenticationMethod.equals("")) {
219 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
220 log.debug("User was authenticated via the default method for this relying party (" + authenticationMethod
223 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
226 String issuer = relyingParty.getIdentityProvider().getProviderId();
228 log.info("Resolving attributes.");
229 List attributes = null;
231 attributes = Arrays.asList(support.getReleaseAttributes(principal, relyingParty, relyingParty.getProviderId(), null));
232 } catch (AAException e1) {
233 log.error("Error resolving attributes: " + e1);
234 eAuthError(response, 90, remoteProviderId, csid);
237 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
239 // Bail if we didn't get any attributes
240 if (attributes == null || attributes.size() < 1) {
241 log.error("Attribute resolver did not return any attributes. "
242 + " The E-Authentication profile's minimum attribute requirements were not met.");
243 eAuthError(response, 60, remoteProviderId, csid);
246 // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
250 attributes = repackageForEauth(attributes);
251 } catch (SAMLException e) {
252 eAuthError(response, 90, remoteProviderId, csid);
256 // Put all attributes into an assertion
258 // TODO provide a way to override authN time
259 SAMLStatement attrStatement = new SAMLAttributeStatement((SAMLSubject) authNSubject.clone(), attributes);
260 SAMLStatement[] statements = {
261 new SAMLAuthenticationStatement(authNSubject, authenticationMethod, new Date(System
262 .currentTimeMillis()), request.getRemoteAddr(), null, null), attrStatement};
263 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
264 System.currentTimeMillis() + 300000), null, null, Arrays.asList(statements));
265 if (log.isDebugEnabled()) {
266 log.debug("Dumping generated SAML Assertion:" + System.getProperty("line.separator")
267 + assertion.toString());
270 // Redirect to agency application
272 respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
275 } catch (SAMLException e) {
276 eAuthError(response, 90, remoteProviderId, csid);
280 } catch (CloneNotSupportedException e) {
281 log.error("An error was encountered while generating assertion: " + e);
282 eAuthError(response, 90, remoteProviderId, csid);
288 private void respondWithArtifact(HttpServletResponse response, IdPProtocolSupport support, String acceptanceURL,
289 Principal principal, SAMLAssertion assertion, SAMLNameIdentifier nameId, SPSSODescriptor descriptor,
290 RelyingParty relyingParty) throws SAMLException, IOException {
292 // Create artifacts for each assertion
293 ArrayList artifacts = new ArrayList();
295 artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
297 String target = relyingParty.getDefaultTarget();
298 if (target == null || target.equals("")) {
299 log.error("No default target found. Relying Party elements corresponding to "
300 + "E-Authentication providers must have a (defaultTarget) attribute specified.");
301 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
304 // Assemble the query string
305 StringBuffer destination = new StringBuffer(acceptanceURL);
306 destination.append("?TARGET=");
307 destination.append(URLEncoder.encode(target, "UTF-8"));
308 Iterator iterator = artifacts.iterator();
309 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
311 // Construct the artifact query parameter
312 while (iterator.hasNext()) {
313 Artifact artifact = (Artifact) iterator.next();
314 artifactBuffer.append("(" + artifact.encode() + ")");
315 destination.append("&SAMLart=");
316 destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
319 log.debug("Redirecting to (" + destination.toString() + ").");
320 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
321 support.getTransactionLog().info(
322 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to E-Authentication provider ("
323 + relyingParty.getProviderId() + ") on behalf of principal ("
324 + principal.getName() + "). Name Identifier: (" + nameId.getName()
325 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
329 private List repackageForEauth(List attributes) throws SAMLException {
331 ArrayList writeable = new ArrayList(attributes);
332 // Bail if we didn't get a commonName, because it is required by the profile
333 SAMLAttribute commonName = getAttribute("commonName", writeable);
334 if (commonName == null) {
335 log.error("The attribute resolver did not return a (commonName) attribute, "
336 + " which is required for the E-Authentication profile.");
337 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
339 // This namespace is required by the eAuth profile
340 commonName.setNamespace("http://eauthentication.gsa.gov/federated/attribute");
341 // TODO Maybe the resolver should set this
343 writeable.add(new SAMLAttribute("csid", "http://eauthentication.gsa.gov/federated/attribute", null, 0, Arrays
344 .asList(new String[]{csid})));
345 // TODO pull from authN system? or make configurable
346 writeable.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
347 0, Arrays.asList(new String[]{"2"})));
351 private SAMLAttribute getAttribute(String name, List attributes) {
353 Iterator iterator = attributes.iterator();
354 while (iterator.hasNext()) {
355 SAMLAttribute attribute = (SAMLAttribute) iterator.next();
356 if (attribute.getName().equals(name)) { return attribute; }
361 private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
363 log.info("Redirecting to E-Authentication error page.");
364 response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);