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