Added eAuth profile error handling.
[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 (request != 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.getAttributeConsumingServices();
195                 if (!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                         nameId = support.getNameMapper().getNameIdentifierName(
207                                         "urn:oasis:names:tc:SAML:1.0:assertion#X509SubjectName", principal, relyingParty,
208                                         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                         repackageForEauth(attributes);
265
266                         // Put all attributes into an assertion
267                         try {
268                                 // TODO provide a way to override authN time
269                                 SAMLStatement attrStatement = new SAMLAttributeStatement((SAMLSubject) authNSubject.clone(), attributes);
270                                 SAMLStatement[] statements = {
271                                                 new SAMLAuthenticationStatement(authNSubject, authenticationMethod, new Date(System
272                                                                 .currentTimeMillis()), request.getRemoteAddr(), null, null), attrStatement};
273                                 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(
274                                                 System.currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
275                                 if (log.isDebugEnabled()) {
276                                         log.debug("Dumping generated SAML Assertion:" + System.getProperty("line.separator")
277                                                         + assertion.toString());
278                                 }
279
280                                 // Redirect to agency application
281                                 try {
282                                         respondWithArtifact(response, support, consumerURL, principal, assertion, nameId, role,
283                                                         relyingParty);
284                                         return null;
285                                 } catch (SAMLException e) {
286                                         eAuthError(response, 90, remoteProviderId, csid);
287                                         return null;
288                                 }
289
290                         } catch (CloneNotSupportedException e) {
291                                 log.error("An error was encountered while generating assertion: " + e);
292                                 eAuthError(response, 90, remoteProviderId, csid);
293                                 return null;
294                         }
295                 }
296         }
297
298         private void respondWithArtifact(HttpServletResponse response, IdPProtocolSupport support, String acceptanceURL,
299                         Principal principal, SAMLAssertion assertion, SAMLNameIdentifier nameId, SPSSODescriptor descriptor,
300                         RelyingParty relyingParty) throws SAMLException, IOException {
301
302                 // Create artifacts for each assertion
303                 ArrayList artifacts = new ArrayList();
304
305                 artifacts.add(support.getArtifactMapper().generateArtifact(assertion, relyingParty));
306
307                 String target = relyingParty.getDefaultTarget();
308                 if (target == null || target.equals("")) {
309                         log.error("No default target found.  Relying Party elements corresponding to "
310                                         + "E-Authentication providers must have a (defaultTarget) attribute specified.");
311                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
312                 }
313
314                 // Assemble the query string
315                 StringBuffer destination = new StringBuffer(acceptanceURL);
316                 destination.append("?TARGET=");
317                 destination.append(URLEncoder.encode(target, "UTF-8"));
318                 Iterator iterator = artifacts.iterator();
319                 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
320
321                 // Construct the artifact query parameter
322                 while (iterator.hasNext()) {
323                         Artifact artifact = (Artifact) iterator.next();
324                         artifactBuffer.append("(" + artifact + ")");
325                         destination.append("&SAMLart=");
326                         destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
327                 }
328
329                 log.debug("Redirecting to (" + destination.toString() + ").");
330                 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
331                 support.getTransactionLog().info(
332                                 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to E-Authentication provider ("
333                                                 + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
334                                                 + principal.getName() + "). Name Identifier: (" + nameId.getName()
335                                                 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
336
337         }
338
339         private void repackageForEauth(List attributes) throws SAMLException {
340
341                 // Bail if we didn't get a commonName, because it is required by the profile
342                 SAMLAttribute commonName = getAttribute("commonName", attributes);
343                 if (commonName == null) {
344                         log.error("The attribute resolver did not return a (commonName) attribute, "
345                                         + " which is required for the E-Authentication profile.");
346                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
347                 } else {
348                         // This namespace is required by the eAuth profile
349                         commonName.setNamespace("http://eauthentication.gsa.gov/federated/attribute");
350                         // TODO Maybe the resolver should set this
351                 }
352                 attributes.add(new SAMLAttribute("csid", "http://eauthentication.gsa.gov/federated/attribute", null, 0, Arrays
353                                 .asList(new String[]{csid})));
354                 // TODO pull from authN system? or make configurable
355                 attributes.add(new SAMLAttribute("assuranceLevel", "http://eauthentication.gsa.gov/federated/attribute", null,
356                                 0, Arrays.asList(new String[]{"2"})));
357         }
358
359         private SAMLAttribute getAttribute(String name, List attributes) {
360
361                 Iterator iterator = attributes.iterator();
362                 while (iterator.hasNext()) {
363                         SAMLAttribute attribute = (SAMLAttribute) iterator.next();
364                         if (attribute.getName().equals(name)) { return attribute; }
365                 }
366                 return null;
367         }
368
369         private void eAuthError(HttpServletResponse response, int code, String aaid, String csid) throws IOException {
370
371                 log.info("Redirecting to E-Authentication error page.");
372                 response.sendRedirect(eAuthError + "?aaid=" + aaid + "&csid=" + csid + "&errcode=" + code);
373         }
374 }