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