ac07ff5ac07c0e53f5bfae01ea1e4e1d3ca43dfb
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / ShibbolethV1SSOHandler.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.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.Date;
33 import java.util.Iterator;
34 import java.util.Vector;
35
36 import javax.servlet.RequestDispatcher;
37 import javax.servlet.ServletException;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import javax.xml.namespace.QName;
41
42 import org.apache.log4j.Logger;
43 import org.opensaml.SAMLAssertion;
44 import org.opensaml.SAMLAudienceRestrictionCondition;
45 import org.opensaml.SAMLAuthenticationStatement;
46 import org.opensaml.SAMLAuthorityBinding;
47 import org.opensaml.SAMLBinding;
48 import org.opensaml.SAMLBrowserProfile;
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.Document;
57 import org.w3c.dom.Element;
58
59 import sun.misc.BASE64Decoder;
60
61 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
62 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
63 import edu.internet2.middleware.shibboleth.common.RelyingParty;
64 import edu.internet2.middleware.shibboleth.common.ShibBrowserProfile;
65 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
66 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
67 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
68 import edu.internet2.middleware.shibboleth.idp.InvalidClientDataException;
69
70 import edu.internet2.middleware.shibboleth.metadata.Endpoint;
71 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
72 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
73
74 /**
75  * @author Walter Hoehn
76  */
77 public class ShibbolethV1SSOHandler extends BaseHandler implements IdPProtocolHandler {
78
79         private static Logger log = Logger.getLogger(ShibbolethV1SSOHandler.class.getName());
80
81         /**
82          * Required DOM-based constructor.
83          */
84         public ShibbolethV1SSOHandler(Element config) throws ShibbolethConfigurationException {
85
86                 super(config);
87         }
88
89         /*
90          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
91          *      javax.servlet.http.HttpServletResponse)
92          */
93         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
94                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, ServletException, IOException {
95
96                 // TODO attribute push?
97
98                 if (request == null) {
99                         log.error("Protocol Handler received a SAML Request, but is unable to handle it.");
100                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
101                 }
102
103                 // Set attributes that are needed by the jsp
104                 request.setAttribute("shire", request.getParameter("shire"));
105                 request.setAttribute("target", request.getParameter("target"));
106
107                 try {
108                         // Ensure that we have the required data from the servlet container
109                         IdPProtocolSupport.validateEngineData(request);
110                         validateShibSpecificData(request);
111
112                         // Get the authN info
113                         String username = support.getIdPConfig().getAuthHeaderName().equalsIgnoreCase("REMOTE_USER") ? request
114                                         .getRemoteUser() : request.getHeader(support.getIdPConfig().getAuthHeaderName());
115                         if ((username == null) || (username.equals(""))) { throw new InvalidClientDataException(
116                                         "Unable to authenticate remote user"); }
117
118                         // Select the appropriate Relying Party configuration for the request
119                         RelyingParty relyingParty = null;
120                         String remoteProviderId = request.getParameter("providerId");
121                         // If the target did not send a Provider Id, then assume it is a Shib
122                         // 1.1 or older target
123                         if (remoteProviderId == null) {
124                                 relyingParty = support.getServiceProviderMapper().getLegacyRelyingParty();
125                         } else if (remoteProviderId.equals("")) {
126                                 throw new InvalidClientDataException("Invalid service provider id.");
127                         } else {
128                                 log.debug("Remote provider has identified itself as: (" + remoteProviderId + ").");
129                                 relyingParty = support.getServiceProviderMapper().getRelyingParty(remoteProviderId);
130                         }
131
132                         // Grab the metadata for the provider
133                         EntityDescriptor provider = support.lookup(relyingParty.getProviderId());
134
135                         // Determine the acceptance URL
136                         String acceptanceURL = request.getParameter("shire");
137
138                         // Make sure that the selected relying party configuration is appropriate for this
139                         // acceptance URL
140                         if (!relyingParty.isLegacyProvider()) {
141
142                                 if (provider == null) {
143                                         log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
144                                         relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
145
146                                 } else {
147
148                                         if (isValidAssertionConsumerURL(provider, acceptanceURL)) {
149                                                 log.info("Supplied consumer URL validated for this provider.");
150                                         } else {
151                                                 log.error("Assertion consumer service URL (" + acceptanceURL + ") is NOT valid for provider ("
152                                                                 + relyingParty.getProviderId() + ").");
153                                                 throw new InvalidClientDataException("Invalid assertion consumer service URL.");
154                                         }
155                                 }
156                         }
157
158                         // Create SAML Name Identifier
159                         SAMLNameIdentifier nameId;
160                         try {
161                                 nameId = support.getNameMapper().getNameIdentifierName(relyingParty.getHSNameFormatId(),
162                                                 new AuthNPrincipal(username), relyingParty, relyingParty.getIdentityProvider());
163                         } catch (NameIdentifierMappingException e) {
164                                 log.error("Error converting principal to SAML Name Identifier: " + e);
165                                 throw new SAMLException("Error converting principal to SAML Name Identifier.", e);
166                         }
167
168                         String authenticationMethod = request.getHeader("SAMLAuthenticationMethod");
169                         if (authenticationMethod == null || authenticationMethod.equals("")) {
170                                 authenticationMethod = relyingParty.getDefaultAuthMethod().toString();
171                                 log.debug("User was authenticated via the default method for this relying party ("
172                                                 + authenticationMethod + ").");
173                         } else {
174                                 log.debug("User was authenticated via the method (" + authenticationMethod + ").");
175                         }
176
177                         // TODO Provide a mechanism for the authenticator to specify the auth time
178                         SAMLAssertion[] assertions = generateAssertion(request, relyingParty, provider, nameId,
179                                         authenticationMethod, new Date(System.currentTimeMillis()));
180
181                         // TODO do assertion signing for artifact stuff
182
183                         // SAML Artifact profile
184                         if (useArtifactProfile(provider, acceptanceURL)) {
185                                 log.debug("Responding with Artifact profile.");
186
187                                 // Create artifacts for each assertion
188                                 ArrayList artifacts = new ArrayList();
189                                 for (int i = 0; i < assertions.length; i++) {
190                                         artifacts.add(support.getArtifactMapper().generateArtifact(assertions[i], relyingParty));
191                                 }
192
193                                 // Assemble the query string
194                                 StringBuffer destination = new StringBuffer(acceptanceURL);
195                                 destination.append("?TARGET=");
196
197                                 destination.append(URLEncoder.encode(request.getParameter("target"), "UTF-8"));
198
199                                 Iterator iterator = artifacts.iterator();
200                                 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
201
202                                 // Construct the artifact query parameter
203                                 while (iterator.hasNext()) {
204                                         Artifact artifact = (Artifact) iterator.next();
205                                         artifactBuffer.append("(" + artifact + ")");
206                                         destination.append("&SAMLart=");
207                                         destination.append(artifact.encode());
208                                 }
209
210                                 log.debug("Redirecting to (" + destination.toString() + ").");
211                                 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
212
213                                 support.getTransactionLog().info(
214                                                 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to provider ("
215                                                                 + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
216                                                                 + username + "). Name Identifier: (" + nameId.getName()
217                                                                 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
218
219                                 // SAML POST profile
220                         } else {
221                                 log.debug("Responding with POST profile.");
222                                 request.setAttribute("acceptanceURL", acceptanceURL);
223                                 request.setAttribute("target", request.getParameter("target"));
224
225                                 SAMLResponse samlResponse = new SAMLResponse(null, acceptanceURL, Arrays.asList(assertions), null);
226                                 IdPProtocolSupport.addSignatures(samlResponse, relyingParty, provider, true);
227                                 createPOSTForm(request, response, samlResponse.toBase64());
228
229                                 // Make transaction log entry
230                                 if (relyingParty.isLegacyProvider()) {
231                                         support.getTransactionLog().info(
232                                                         "Authentication assertion issued to legacy provider (SHIRE: "
233                                                                         + request.getParameter("shire") + ") on behalf of principal (" + username
234                                                                         + ") for resource (" + request.getParameter("target") + "). Name Identifier: ("
235                                                                         + nameId.getName() + "). Name Identifier Format: (" + nameId.getFormat() + ").");
236                                 } else {
237                                         support.getTransactionLog().info(
238                                                         "Authentication assertion issued to provider ("
239                                                                         + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
240                                                                         + username + "). Name Identifier: (" + nameId.getName()
241                                                                         + "). Name Identifier Format: (" + nameId.getFormat() + ").");
242                                 }
243                         }
244                 } catch (InvalidClientDataException e) {
245                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
246                 }
247                 return null;
248         }
249
250         private SAMLAssertion[] generateAssertion(HttpServletRequest request, RelyingParty relyingParty,
251                         EntityDescriptor provider, SAMLNameIdentifier nameId, String authenticationMethod, Date authTime)
252                         throws SAMLException, IOException {
253
254                 Document doc = org.opensaml.XML.parserPool.newDocument();
255
256                 // Determine the correct audiences
257                 ArrayList audiences = new ArrayList();
258                 if (relyingParty.getProviderId() != null) {
259                         audiences.add(relyingParty.getProviderId());
260                 }
261                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
262                         audiences.add(relyingParty.getName());
263                 }
264
265                 // Determine the correct issuer
266                 String issuer = null;
267                 if (relyingParty.isLegacyProvider()) {
268
269                         log.debug("Service Provider is running Shibboleth <= 1.1. Using old style issuer.");
270                         if (relyingParty.getIdentityProvider().getSigningCredential() == null
271                                         || relyingParty.getIdentityProvider().getSigningCredential().getX509Certificate() == null) { throw new SAMLException(
272                                         "Cannot serve legacy style assertions without an X509 certificate"); }
273                         issuer = ShibBrowserProfile.getHostNameFromDN(relyingParty.getIdentityProvider().getSigningCredential()
274                                         .getX509Certificate().getSubjectX500Principal());
275                         if (issuer == null || issuer.equals("")) { throw new SAMLException(
276                                         "Error parsing certificate DN while determining legacy issuer name."); }
277
278                 } else {
279                         issuer = relyingParty.getIdentityProvider().getProviderId();
280                 }
281
282                 // For compatibility with pre-1.2 shibboleth targets, include a pointer to the AA
283                 ArrayList bindings = new ArrayList();
284                 if (relyingParty.isLegacyProvider()) {
285
286                         SAMLAuthorityBinding binding = new SAMLAuthorityBinding(SAMLBinding.SOAP, relyingParty.getAAUrl()
287                                         .toString(), new QName(org.opensaml.XML.SAMLP_NS, "AttributeQuery"));
288                         bindings.add(binding);
289                 }
290
291                 // Create the authN assertion
292                 Vector conditions = new Vector(1);
293                 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
294
295                 String[] confirmationMethods = {SAMLSubject.CONF_BEARER};
296                 SAMLSubject subject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
297
298                 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request
299                                 .getRemoteAddr(), null, bindings)};
300
301                 SAMLAssertion[] assertions = {new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
302                                 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements))};
303
304                 if (log.isDebugEnabled()) {
305                         log.debug("Dumping generated SAML Assertions:"
306                                         + System.getProperty("line.separator")
307                                         + new String(new BASE64Decoder().decodeBuffer(new String(assertions[0].toBase64(), "ASCII")),
308                                                         "UTF8"));
309                 }
310
311                 return assertions;
312         }
313
314         /*
315          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
316          */
317         public String getHandlerName() {
318
319                 return "Shibboleth v1.x SSO";
320         }
321
322         private void validateShibSpecificData(HttpServletRequest request) throws InvalidClientDataException {
323
324                 if (request.getParameter("target") == null || request.getParameter("target").equals("")) { throw new InvalidClientDataException(
325                                 "Invalid data from Service Provider: no target URL received."); }
326                 if ((request.getParameter("shire") == null) || (request.getParameter("shire").equals(""))) { throw new InvalidClientDataException(
327                                 "Invalid data from Service Provider: No acceptance URL received."); }
328         }
329
330         private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res, byte[] buf) throws IOException,
331                         ServletException {
332
333                 // Hardcoded to ASCII to ensure Base64 encoding compatibility
334                 req.setAttribute("assertion", new String(buf, "ASCII"));
335
336                 if (log.isDebugEnabled()) {
337                         try {
338                                 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
339                                                 + new String(new BASE64Decoder().decodeBuffer(new String(buf, "ASCII")), "UTF8"));
340                         } catch (IOException e) {
341                                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
342                         }
343                 }
344
345                 RequestDispatcher rd = req.getRequestDispatcher("/IdP.jsp");
346                 rd.forward(req, res);
347         }
348
349         private static boolean useArtifactProfile(EntityDescriptor provider, String acceptanceURL) {
350
351                 // Default to POST if we have no metadata
352                 if (provider == null) { return false; }
353
354                 // Default to POST if we have incomplete metadata
355                 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
356                 if (sp == null) { return false; }
357
358                 // Look at the bindings.. prefer POST if we have multiple
359                 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
360                 while (endpoints.hasNext()) {
361                         Endpoint ep = (Endpoint) endpoints.next();
362                         if (acceptanceURL.equals(ep.getLocation()) && SAMLBrowserProfile.PROFILE_POST_URI.equals(ep.getBinding())) { return false; }
363                         if (acceptanceURL.equals(ep.getLocation())
364                                         && SAMLBrowserProfile.PROFILE_ARTIFACT_URI.equals(ep.getBinding())) { return true; }
365                 }
366
367                 // Default to POST if we have incomplete metadata
368                 return false;
369         }
370
371         private static boolean isValidAssertionConsumerURL(EntityDescriptor provider, String shireURL)
372                         throws InvalidClientDataException {
373
374                 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
375                 if (sp == null) {
376                         log.info("Inappropriate metadata for provider.");
377                         return false;
378                 }
379
380                 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
381                 while (endpoints.hasNext()) {
382                         if (shireURL.equals(((Endpoint) endpoints.next()).getLocation())) { return true; }
383                 }
384                 log.info("Supplied consumer URL not found in metadata.");
385                 return false;
386         }
387 }