012b0d186dc9764589d1423e00645b09c21fa82c
[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 import java.util.Vector;
37
38 import javax.servlet.ServletException;
39 import javax.servlet.http.Cookie;
40 import javax.servlet.http.HttpServletRequest;
41 import javax.servlet.http.HttpServletResponse;
42
43 import org.apache.log4j.Logger;
44 import org.opensaml.SAMLAssertion;
45 import org.opensaml.SAMLAttribute;
46 import org.opensaml.SAMLAttributeStatement;
47 import org.opensaml.SAMLAudienceRestrictionCondition;
48 import org.opensaml.SAMLAuthenticationStatement;
49 import org.opensaml.SAMLException;
50 import org.opensaml.SAMLNameIdentifier;
51 import org.opensaml.SAMLRequest;
52 import org.opensaml.SAMLResponse;
53 import org.opensaml.SAMLStatement;
54 import org.opensaml.SAMLSubject;
55 import org.opensaml.artifact.Artifact;
56 import org.w3c.dom.Element;
57
58 import edu.internet2.middleware.shibboleth.aa.AAException;
59 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
60 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
61 import edu.internet2.middleware.shibboleth.common.RelyingParty;
62 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
63 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
64 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
65 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
66 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
67 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
68 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
69
70 /**
71  * <code>ProtocolHandler</code> implementation that responds to SSO flows as specified in "E-Authentication Interface
72  * Specifications for the SAML Artifact Profile ".
73  * 
74  * @author Walter Hoehn
75  */
76 public class E_AuthSSOHandler extends SSOHandler implements IdPProtocolHandler {
77
78         private static Logger log = Logger.getLogger(E_AuthSSOHandler.class.getName());
79         private String eAuthPortal = "http://eauth.firstgov.gov/service/select";
80         private String eAuthError = "http://eauth.firstgov.gov/service/error";
81         private String csid;
82
83         /**
84          * Required DOM-based constructor.
85          */
86         public E_AuthSSOHandler(Element config) throws ShibbolethConfigurationException {
87
88                 super(config);
89                 csid = config.getAttribute("csid");
90                 if (csid == null || csid.equals("")) {
91                         log.error("(csid) attribute is required for the " + getHandlerName() + "protocol handler.");
92                         throw new ShibbolethConfigurationException("Unable to initialize protocol handler.");
93                 }
94
95                 String portal = config.getAttribute("eAuthPortal");
96                 if (portal != null && !portal.equals("")) {
97                         eAuthPortal = portal;
98                 }
99
100                 String error = config.getAttribute("eAuthError");
101                 if (error != null && !error.equals("")) {
102                         eAuthError = portal;
103                 }
104         }
105
106         /*
107          * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#getHandlerName()
108          */
109         public String getHandlerName() {
110
111                 return "E-Authentication SSO";
112         }
113
114         /*
115          * @see edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
116          *      javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
117          *      edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport)
118          */
119         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
120                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
121
122                 // Sanity check
123                 if (samlRequest != null) {
124                         log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
125                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
126                 }
127
128                 // If no aaid is specified, redirect to the eAuth portal
129                 if (request.getParameter("aaid") == null || request.getParameter("aaid").equals("")) {
130                         log.debug("Received an E-Authentication request with no (aaid) parameter.  "
131                                         + "Redirecting to the E-Authentication portal.");
132                         response.sendRedirect(eAuthPortal + "?csid=" + csid);
133                         return null;
134                 }
135
136                 // FUTURE at some point this needs to be integrated with SAML2 session reset
137                 // If session reset was requested, delete the session and re-direct back
138                 // Note, this only works with servler form-auth
139                 String reAuth = request.getParameter("sessionreset");
140                 if (reAuth != null && reAuth.equals("1")) {
141                         log.debug("E-Authebtication session reset requested.");
142                         Cookie session = new Cookie("JSESSIONID", null);
143                         session.setMaxAge(0);
144                         response.addCookie(session);
145
146                         response.sendRedirect(request.getRequestURI()
147                                         + (request.getQueryString() != null ? "?"
148                                                         + request.getQueryString().replaceAll("(^sessionreset=1&?|&?sessionreset=1)", "") : ""));
149                         return null;
150                 }
151                 // Sanity check
152                 try {
153                         validateEngineData(request);
154                 } catch (InvalidClientDataException e) {
155                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
156                 }
157
158                 // Get the authN info
159                 String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
160                                 .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
161                 if ((username == null) || (username.equals(""))) {
162                         log.error("Unable to authenticate remote user.");
163                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
164                 }
165                 AuthNPrincipal principal = new AuthNPrincipal(username);
166
167                 // Select the appropriate Relying Party configuration for the request
168                 String remoteProviderId = request.getParameter("aaid");
169                 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
170                 RelyingParty relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
171
172                 if (relyingParty == null || relyingParty.isLegacyProvider()) {
173                         log.error("Unable to identify appropriate relying party configuration.");
174                         eAuthError(response, 30, remoteProviderId, csid);
175                         return null;
176                 }
177
178                 // Lookup the provider in the metadata
179                 EntityDescriptor entity = support.lookup(relyingParty.getProviderId());
180                 if (entity == null) {
181                         log.error("No metadata found for EAuth provider.");
182                         eAuthError(response, 30, remoteProviderId, csid);
183                         return null;
184                 }
185                 SPSSODescriptor role = entity.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
186                 if (role == null) {
187                         log.error("Inappropriate metadata for EAuth provider.");
188                         eAuthError(response, 30, remoteProviderId, csid);
189                         return null;
190                 }
191
192                 // The EAuth profile requires metadata, since the assertion consumer is not supplied as a request parameter
193                 // Pull the consumer URL from the metadata
194                 Iterator endpoints = role.getAssertionConsumerServiceManager().getEndpoints();
195                 if (endpoints == null || !endpoints.hasNext()) {
196                         log.error("Inappropriate metadata for provider: no roles specified.");
197                         eAuthError(response, 30, remoteProviderId, csid);
198                         return null;
199                 }
200                 String consumerURL = ((Endpoint) endpoints.next()).getLocation();
201                 log.debug("Assertion Consumer URL provider: " + consumerURL);
202
203                 // Create SAML Name Identifier & Subject
204                 SAMLNameIdentifier nameId;
205                 try {
206                         // TODO verify that the nameId is the right format here and error if not
207                         nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(), principal,
208                                         relyingParty, relyingParty.getIdentityProvider());
209                 } catch (NameIdentifierMappingException e) {
210                         log.error("Error converting principal to SAML Name Identifier: " + e);
211                         eAuthError(response, 60, remoteProviderId, csid);
212                         return null;
213                 }
214
215                 String[] confirmationMethods = {SAMLSubject.CONF_ARTIFACT};
216                 SAMLSubject authNSubject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
217
218                 // Determine AuthN method
219                 String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
220                 if (authenticationMethod == null || authenticationMethod.equals("")) {
221                         authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
222                         log.debug("User was authenticated via the default method for this relying party (" + authenticationMethod
223                                         + ").");
224                 } else {
225                         log.debug("User was authenticated via the method (" + authenticationMethod + ").");
226                 }
227
228                 // Generate SAML audiences
229                 ArrayList audiences = new ArrayList();
230                 if (relyingParty.getProviderId() != null) {
231                         audiences.add(relyingParty.getProviderId());
232                 }
233                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
234                         audiences.add(relyingParty.getName());
235                 }
236                 Vector conditions = new Vector(1);
237                 if (audiences != null && audiences.size() > 0) {
238                         conditions.add(new SAMLAudienceRestrictionCondition(audiences));
239                 }
240
241                 String issuer = relyingParty.getIdentityProvider().getProviderId();
242
243                 log.info("Resolving attributes.");
244                 List attributes = null;
245                 try {
246                         attributes = Arrays.asList(support.getReleaseAttributes(principal, relyingParty.getProviderId(), null));
247                 } catch (AAException e1) {
248                         log.error("Error resolving attributes: " + e1);
249                         eAuthError(response, 90, remoteProviderId, csid);
250                         return null;
251                 }
252                 log.info("Found " + attributes.size() + " attribute(s) for " + principal.getName());
253
254                 // Bail if we didn't get any attributes
255                 if (attributes == null || attributes.size() < 1) {
256                         log.error("Attribute resolver did not return any attributes. "
257                                         + " The E-Authentication profile's minimum attribute requirements were not met.");
258                         eAuthError(response, 60, remoteProviderId, csid);
259                         return null;
260
261                         // OK, we got attributes back, package them as required for eAuth and combine them with the authN data in an
262                         // assertion
263                 } else {
264                         try {
265                                 attributes = repackageForEauth(attributes);
266                         } catch (SAMLException e) {
267                                 eAuthError(response, 90, remoteProviderId, csid);
268                                 return null;
269                         }
270
271                         // Put all attributes into an assertion
272                         try {
273                                 // TODO provide a way to override authN time
274                                 SAMLStatement attrStatement = new SAMLAttributeStatement((SAMLSubject) authNSubject.clone(), attributes);
275                                 SAMLStatement[] statements = {
276                                                 new SAMLAuthenticationStatement(authNSubject, authenticationMethod, new Date(System
277                                                                 .currentTimeMillis()), request.getRemoteAddr(), null, null), attrStatement};
278                                 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
279                                                 System.currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
280                                 if (log.isDebugEnabled()) {
281                                         log.debug("Dumping generated SAML Assertion:" + System.getProperty("line.separator")
282                                                         + assertion.toString());
283                                 }
284
285                                 // Redirect to agency application
286                                 try {
287                                         respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
288                                                         relyingParty);
289                                         return null;
290                                 } catch (SAMLException e) {
291                                         eAuthError(response, 90, remoteProviderId, csid);
292                                         return null;
293                                 }
294
295                         } catch (CloneNotSupportedException e) {
296                                 log.error("An error was encountered while generating assertion: " + e);
297                                 eAuthError(response, 90, remoteProviderId, csid);
298                                 return null;
299                         }
300                 }
301         }
302
303         private void respondWithArtifact(HttpServletResponse response, IdPProtocolSupport support, String acceptanceURL,
304                         Principal principal, SAMLAssertion assertion, SAMLNameIdentifier nameId, SPSSODescriptor descriptor,
305                         RelyingParty relyingParty) throws SAMLException, IOException {
306
307                 // Create artifacts for each assertion
308                 ArrayList artifacts = new ArrayList();
309
310                 artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
311
312                 String target = relyingParty.getDefaultTarget();
313                 if (target == null || target.equals("")) {
314                         log.error("No default target found.  Relying Party elements corresponding to "
315                                         + "E-Authentication providers must have a (defaultTarget) attribute specified.");
316                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
317                 }
318
319                 // Assemble the query string
320                 StringBuffer destination = new StringBuffer(acceptanceURL);
321                 destination.append("?TARGET=");
322                 destination.append(URLEncoder.encode(target, "UTF-8"));
323                 Iterator iterator = artifacts.iterator();
324                 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
325
326                 // Construct the artifact query parameter
327                 while (iterator.hasNext()) {
328                         Artifact artifact = (Artifact) iterator.next();
329                         artifactBuffer.append("(" + artifact.encode() + ")");
330                         System.err.println(artifact.encode());
331                         destination.append("&SAMLart=");
332                         destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
333                 }
334
335                 log.debug("Redirecting to (" + destination.toString() + ").");
336                 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
337                 support.getTransactionLog().info(
338                                 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to E-Authentication provider ("
339                                                 + relyingParty.getProviderId() + ") on behalf of principal ("
340                                                 + principal.getName() + "). Name Identifier: (" + nameId.getName()
341                                                 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
342
343         }
344
345         private List repackageForEauth(List attributes) throws SAMLException {
346
347                 ArrayList  writeable = new ArrayList(attributes); 
348                 // Bail if we didn't get a commonName, because it is required by the profile
349                 SAMLAttribute commonName = getAttribute("commonName", writeable);
350                 if (commonName == null) {
351                         log.error("The attribute resolver did not return a (commonName) attribute, "
352                                         + " which is required for the E-Authentication profile.");
353                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
354                 } else {
355                         // This namespace is required by the eAuth profile
356                         commonName.setNamespace("http://eauthentication.gsa.gov/federated/attribute");
357                         // TODO Maybe the resolver should set this
358                 }
359                 writeable.add(new SAMLAttribute("csid", "http://eauthentication.gsa.gov/federated/attribute", null, 0, Arrays
360                                 .asList(new String[]{csid})));
361                 // TODO pull from authN system? or make configurable
362                 writeable.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
363                                 0, Arrays.asList(new String[]{"2"})));
364                 return writeable;
365         }
366
367         private SAMLAttribute getAttribute(String name, List attributes) {
368
369                 Iterator iterator = attributes.iterator();
370                 while (iterator.hasNext()) {
371                         SAMLAttribute attribute = (SAMLAttribute) iterator.next();
372                         if (attribute.getName().equals(name)) { return attribute; }
373                 }
374                 return null;
375         }
376
377         private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
378
379                 log.info("Redirecting to E-Authentication error page.");
380                 response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);
381         }
382 }