Proper URL encoding of saml artifacts.
[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
179                         ArrayList assertions = new ArrayList();
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                                 assertions.add(generateAuthNAssertion(request, relyingParty, provider, nameId, authenticationMethod,
188                                                 new Date(System.currentTimeMillis()), false));
189
190                                 // Create artifacts for each assertion
191                                 ArrayList artifacts = new ArrayList();
192                                 for (int i = 0; i < assertions.size(); i++) {
193                                         artifacts.add(support.getArtifactMapper().generateArtifact((SAMLAssertion) assertions.get(i),
194                                                         relyingParty));
195                                 }
196
197                                 // Assemble the query string
198                                 StringBuffer destination = new StringBuffer(acceptanceURL);
199                                 destination.append("?TARGET=");
200
201                                 destination.append(URLEncoder.encode(request.getParameter("target"), "UTF-8"));
202
203                                 Iterator iterator = artifacts.iterator();
204                                 StringBuffer artifactBuffer = new StringBuffer(); // Buffer for the transaction log
205
206                                 // Construct the artifact query parameter
207                                 while (iterator.hasNext()) {
208                                         Artifact artifact = (Artifact) iterator.next();
209                                         artifactBuffer.append("(" + artifact + ")");
210                                         destination.append("&SAMLart=");
211                                         destination.append(URLEncoder.encode(artifact.encode(), "UTF-8"));
212                                 }
213
214                                 log.debug("Redirecting to (" + destination.toString() + ").");
215                                 response.sendRedirect(destination.toString()); // Redirect to the artifact receiver
216
217                                 support.getTransactionLog().info(
218                                                 "Assertion artifact(s) (" + artifactBuffer.toString() + ") issued to provider ("
219                                                                 + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
220                                                                 + username + "). Name Identifier: (" + nameId.getName()
221                                                                 + "). Name Identifier Format: (" + nameId.getFormat() + ").");
222
223                                 // SAML POST profile
224                         } else {
225                                 log.debug("Responding with POST profile.");
226
227                                 assertions.add(generateAuthNAssertion(request, relyingParty, provider, nameId, authenticationMethod,
228                                                 new Date(System.currentTimeMillis()), true));
229
230                                 request.setAttribute("acceptanceURL", acceptanceURL);
231                                 request.setAttribute("target", request.getParameter("target"));
232
233                                 SAMLResponse samlResponse = new SAMLResponse(null, acceptanceURL, assertions, null);
234                                 IdPProtocolSupport.addSignatures(samlResponse, relyingParty, provider, true);
235                                 createPOSTForm(request, response, samlResponse.toBase64());
236
237                                 // Make transaction log entry
238                                 if (relyingParty.isLegacyProvider()) {
239                                         support.getTransactionLog().info(
240                                                         "Authentication assertion issued to legacy provider (SHIRE: "
241                                                                         + request.getParameter("shire") + ") on behalf of principal (" + username
242                                                                         + ") for resource (" + request.getParameter("target") + "). Name Identifier: ("
243                                                                         + nameId.getName() + "). Name Identifier Format: (" + nameId.getFormat() + ").");
244                                 } else {
245                                         support.getTransactionLog().info(
246                                                         "Authentication assertion issued to provider ("
247                                                                         + relyingParty.getIdentityProvider().getProviderId() + ") on behalf of principal ("
248                                                                         + username + "). Name Identifier: (" + nameId.getName()
249                                                                         + "). Name Identifier Format: (" + nameId.getFormat() + ").");
250                                 }
251                         }
252                 } catch (InvalidClientDataException e) {
253                         throw new SAMLException(SAMLException.RESPONDER, e.getMessage());
254                 }
255                 return null;
256         }
257
258         private SAMLAssertion generateAuthNAssertion(HttpServletRequest request, RelyingParty relyingParty,
259                         EntityDescriptor provider, SAMLNameIdentifier nameId, String authenticationMethod, Date authTime,
260                         boolean bearerConfirmation) throws SAMLException, IOException {
261
262                 Document doc = org.opensaml.XML.parserPool.newDocument();
263
264                 // Determine the correct audiences
265                 ArrayList audiences = new ArrayList();
266                 if (relyingParty.getProviderId() != null) {
267                         audiences.add(relyingParty.getProviderId());
268                 }
269                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
270                         audiences.add(relyingParty.getName());
271                 }
272
273                 // Determine the correct issuer
274                 String issuer = null;
275                 if (relyingParty.isLegacyProvider()) {
276
277                         log.debug("Service Provider is running Shibboleth <= 1.1. Using old style issuer.");
278                         if (relyingParty.getIdentityProvider().getSigningCredential() == null
279                                         || relyingParty.getIdentityProvider().getSigningCredential().getX509Certificate() == null) { throw new SAMLException(
280                                         "Cannot serve legacy style assertions without an X509 certificate"); }
281                         issuer = ShibBrowserProfile.getHostNameFromDN(relyingParty.getIdentityProvider().getSigningCredential()
282                                         .getX509Certificate().getSubjectX500Principal());
283                         if (issuer == null || issuer.equals("")) { throw new SAMLException(
284                                         "Error parsing certificate DN while determining legacy issuer name."); }
285
286                 } else {
287                         issuer = relyingParty.getIdentityProvider().getProviderId();
288                 }
289
290                 // For compatibility with pre-1.2 shibboleth targets, include a pointer to the AA
291                 ArrayList bindings = new ArrayList();
292                 if (relyingParty.isLegacyProvider()) {
293
294                         SAMLAuthorityBinding binding = new SAMLAuthorityBinding(SAMLBinding.SOAP, relyingParty.getAAUrl()
295                                         .toString(), new QName(org.opensaml.XML.SAMLP_NS, "AttributeQuery"));
296                         bindings.add(binding);
297                 }
298
299                 // Create the authN assertion
300                 Vector conditions = new Vector(1);
301                 if (audiences != null && audiences.size() > 0) conditions.add(new SAMLAudienceRestrictionCondition(audiences));
302
303                 String[] confirmationMethods = null;
304                 if (bearerConfirmation) {
305                         confirmationMethods = new String[]{SAMLSubject.CONF_BEARER};
306                 }
307
308                 SAMLSubject subject = new SAMLSubject(nameId, Arrays.asList(confirmationMethods), null, null);
309
310                 SAMLStatement[] statements = {new SAMLAuthenticationStatement(subject, authenticationMethod, authTime, request
311                                 .getRemoteAddr(), null, bindings)};
312
313                 SAMLAssertion assertion = new SAMLAssertion(issuer, new Date(System.currentTimeMillis()), new Date(System
314                                 .currentTimeMillis() + 300000), conditions, null, Arrays.asList(statements));
315
316                 if (log.isDebugEnabled()) {
317                         log.debug("Dumping generated SAML Assertions:" + System.getProperty("line.separator")
318                                         + new String(new BASE64Decoder().decodeBuffer(new String(assertion.toBase64(), "ASCII")), "UTF8"));
319                 }
320
321                 return assertion;
322         }
323
324         /*
325          * @see edu.internet2.middleware.shibboleth.idp.IdPResponder.ProtocolHandler#getHandlerName()
326          */
327         public String getHandlerName() {
328
329                 return "Shibboleth v1.x SSO";
330         }
331
332         private void validateShibSpecificData(HttpServletRequest request) throws InvalidClientDataException {
333
334                 if (request.getParameter("target") == null || request.getParameter("target").equals("")) { throw new InvalidClientDataException(
335                                 "Invalid data from Service Provider: no target URL received."); }
336                 if ((request.getParameter("shire") == null) || (request.getParameter("shire").equals(""))) { throw new InvalidClientDataException(
337                                 "Invalid data from Service Provider: No acceptance URL received."); }
338         }
339
340         private static void createPOSTForm(HttpServletRequest req, HttpServletResponse res, byte[] buf) throws IOException,
341                         ServletException {
342
343                 // Hardcoded to ASCII to ensure Base64 encoding compatibility
344                 req.setAttribute("assertion", new String(buf, "ASCII"));
345
346                 if (log.isDebugEnabled()) {
347                         try {
348                                 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
349                                                 + new String(new BASE64Decoder().decodeBuffer(new String(buf, "ASCII")), "UTF8"));
350                         } catch (IOException e) {
351                                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
352                         }
353                 }
354
355                 RequestDispatcher rd = req.getRequestDispatcher("/IdP.jsp");
356                 rd.forward(req, res);
357         }
358
359         private static boolean useArtifactProfile(EntityDescriptor provider, String acceptanceURL) {
360
361                 // Default to POST if we have no metadata
362                 if (provider == null) { return false; }
363
364                 // Default to POST if we have incomplete metadata
365                 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
366                 if (sp == null) { return false; }
367
368                 // Look at the bindings.. prefer POST if we have multiple
369                 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
370                 while (endpoints.hasNext()) {
371                         Endpoint ep = (Endpoint) endpoints.next();
372                         if (acceptanceURL.equals(ep.getLocation()) && SAMLBrowserProfile.PROFILE_POST_URI.equals(ep.getBinding())) { return false; }
373                         if (acceptanceURL.equals(ep.getLocation())
374                                         && SAMLBrowserProfile.PROFILE_ARTIFACT_URI.equals(ep.getBinding())) { return true; }
375                 }
376
377                 // Default to POST if we have incomplete metadata
378                 return false;
379         }
380
381         private static boolean isValidAssertionConsumerURL(EntityDescriptor provider, String shireURL)
382                         throws InvalidClientDataException {
383
384                 SPSSODescriptor sp = provider.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
385                 if (sp == null) {
386                         log.info("Inappropriate metadata for provider.");
387                         return false;
388                 }
389
390                 Iterator endpoints = sp.getAssertionConsumerServiceManager().getEndpoints();
391                 while (endpoints.hasNext()) {
392                         if (shireURL.equals(((Endpoint) endpoints.next()).getLocation())) { return true; }
393                 }
394                 log.info("Supplied consumer URL not found in metadata.");
395                 return false;
396         }
397 }