e332109caeedcc6d2a5da9b5950c0b781cb4565d
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / E_AuthSSOHandler.java
1 /*
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.
24  */
25
26 package edu.internet2.middleware.shibboleth.idp.provider;
27
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;
36
37 import javax.servlet.ServletException;
38 import javax.servlet.http.Cookie;
39 import javax.servlet.http.HttpServletRequest;
40 import javax.servlet.http.HttpServletResponse;
41
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;
55
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;
67
68 /**
69  * <code>ProtocolHandler</code> implementation that responds to SSO flows as specified in "E-Authentication Interface
70  * Specifications for the SAML Artifact Profile ".
71  * 
72  * @author Walter Hoehn
73  */
74 public class E_AuthSSOHandler extends SSOHandler implements IdPProtocolHandler {
75
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";
79         private String csid;
80
81         /**
82          * Required DOM-based constructor.
83          */
84         public E_AuthSSOHandler(Element config) throws ShibbolethConfigurationException {
85
86                 super(config);
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.");
91                 }
92
93                 String portal = config.getAttribute("eAuthPortal");
94                 if (portal != null && !portal.equals("")) {
95                         eAuthPortal = portal;
96                 }
97
98                 String error = config.getAttribute("eAuthError");
99                 if (error != null && !error.equals("")) {
100                         eAuthError = portal;
101                 }
102         }
103
104         /*
105          * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#getHandlerName()
106          */
107         public String getHandlerName() {
108
109                 return "E-Authentication SSO";
110         }
111
112         /*
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)
116          */
117         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
118                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
119
120                 // Sanity check
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.");
124                 }
125
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);
131                         return null;
132                 }
133
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);
143
144                         response.sendRedirect(request.getRequestURI()
145                                         + (request.getQueryString() != null ? "?"
146                                                         + request.getQueryString().replaceAll("(^sessionreset=1&?|&?sessionreset=1)", "") : ""));
147                         return null;
148                 }
149                 // Sanity check
150                 try {
151                         validateEngineData(request);
152                 } catch (InvalidClientDataException e) {
153                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
154                 }
155
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.");
162                 }
163                 LocalPrincipal principal = new LocalPrincipal(username);
164
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);
169
170                 if (relyingParty == null || relyingParty.isLegacyProvider()) {
171                         log.error("Unable to identify appropriate relying party configuration.");
172                         eAuthError(response, 30, remoteProviderId, csid);
173                         return null;
174                 }
175
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);
181                         return null;
182                 }
183                 SPSSODescriptor role = entity.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
184                 if (role == null) {
185                         log.error("Inappropriate metadata for EAuth provider.");
186                         eAuthError(response, 30, remoteProviderId, csid);
187                         return null;
188                 }
189
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);
196                         return null;
197                 }
198                 String consumerURL = ((Endpoint) endpoints.next()).getLocation();
199                 log.debug("Assertion Consumer URL provider: " + consumerURL);
200
201                 // Create SAML Name Identifier & Subject
202                 SAMLNameIdentifier nameId;
203                 try {
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);
210                         return null;
211                 }
212
213                 String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
214                 SAMLSubject authNSubject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
215
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
221                                         + ").");
222                 } else {
223                         log.debug("User was authenticated via the method (" + authenticationMethod + ").");
224                 }
225
226                 String issuer = relyingParty.getIdentityProvider().getProviderId();
227
228                 log.info("Resolving attributes.");
229                 List attributes = null;
230                 try {
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);
235                         return null;
236                 }
237                 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
238
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);
244                         return null;
245
246                         // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
247                         // assertion
248                 } else {
249                         try {
250                                 attributes = repackageForEauth(attributes);
251                         } catch (SAMLException e) {
252                                 eAuthError(response, 90, remoteProviderId, csid);
253                                 return null;
254                         }
255
256                         // Put all attributes into an assertion
257                         try {
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());
268                                 }
269
270                                 // Redirect to agency application
271                                 try {
272                                         respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
273                                                         relyingParty);
274                                         return null;
275                                 } catch (SAMLException e) {
276                                         eAuthError(response, 90, remoteProviderId, csid);
277                                         return null;
278                                 }
279
280                         } catch (CloneNotSupportedException e) {
281                                 log.error("An error was encountered while generating assertion: " + e);
282                                 eAuthError(response, 90, remoteProviderId, csid);
283                                 return null;
284                         }
285                 }
286         }
287
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 {
291
292                 // Create artifacts for each assertion
293                 ArrayList artifacts = new ArrayList();
294
295                 artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
296
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.");
302                 }
303
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
310
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"));
317                 }
318
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() + ").");
326
327         }
328
329         private List repackageForEauth(List attributes) throws SAMLException {
330
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.");
338                 } else {
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
342                 }
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"})));
348                 return writeable;
349         }
350
351         private SAMLAttribute getAttribute(String name, List attributes) {
352
353                 Iterator iterator = attributes.iterator();
354                 while (iterator.hasNext()) {
355                         SAMLAttribute attribute = (SAMLAttribute) iterator.next();
356                         if (attribute.getName().equals(name)) { return attribute; }
357                 }
358                 return null;
359         }
360
361         private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
362
363                 log.info("Redirecting to E-Authentication error page.");
364                 response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);
365         }
366 }