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