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