1b0f0c3a79a7a0622db4fda60be7d775270dee65
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / shire / ShireServlet.java
1 /* 
2  * The Shibboleth License, Version 1. 
3  * Copyright (c) 2002 
4  * University Corporation for Advanced Internet Development, Inc. 
5  * All rights reserved
6  * 
7  * 
8  * Redistribution and use in source and binary forms, with or without 
9  * modification, are permitted provided that the following conditions are met:
10  * 
11  * Redistributions of source code must retain the above copyright notice, this 
12  * list of conditions and the following disclaimer.
13  * 
14  * Redistributions in binary form must reproduce the above copyright notice, 
15  * this list of conditions and the following disclaimer in the documentation 
16  * and/or other materials provided with the distribution, if any, must include 
17  * the following acknowledgment: "This product includes software developed by 
18  * the University Corporation for Advanced Internet Development 
19  * <http://www.ucaid.edu>Internet2 Project. Alternately, this acknowledegement 
20  * may appear in the software itself, if and wherever such third-party 
21  * acknowledgments normally appear.
22  * 
23  * Neither the name of Shibboleth nor the names of its contributors, nor 
24  * Internet2, nor the University Corporation for Advanced Internet Development, 
25  * Inc., nor UCAID may be used to endorse or promote products derived from this 
26  * software without specific prior written permission. For written permission, 
27  * please contact shibboleth@shibboleth.org
28  * 
29  * Products derived from this software may not be called Shibboleth, Internet2, 
30  * UCAID, or the University Corporation for Advanced Internet Development, nor 
31  * may Shibboleth appear in their name, without prior written permission of the 
32  * University Corporation for Advanced Internet Development.
33  * 
34  * 
35  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
36  * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
37  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
38  * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK 
39  * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. 
40  * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY 
41  * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, 
42  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
43  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
44  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
46  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
47  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48  */
49
50 package edu.internet2.middleware.shibboleth.shire;
51
52 import java.io.ByteArrayOutputStream;
53 import java.io.File;
54 import java.io.FileWriter;
55 import java.io.IOException;
56 import java.io.PrintWriter;
57 import java.security.Key;
58 import java.security.KeyStore;
59 import java.security.KeyStoreException;
60 import java.security.NoSuchAlgorithmException;
61 import java.security.cert.Certificate;
62 import java.security.cert.CertificateException;
63
64 import javax.servlet.RequestDispatcher;
65 import javax.servlet.ServletException;
66 import javax.servlet.UnavailableException;
67 import javax.servlet.http.Cookie;
68 import javax.servlet.http.HttpServlet;
69 import javax.servlet.http.HttpServletRequest;
70 import javax.servlet.http.HttpServletResponse;
71 import javax.servlet.http.HttpUtils;
72
73 import org.apache.log4j.Logger;
74 import org.doomdark.uuid.UUIDGenerator;
75 import org.opensaml.SAMLAuthenticationStatement;
76 import org.opensaml.SAMLException;
77 import org.opensaml.SAMLResponse;
78
79 import edu.internet2.middleware.shibboleth.common.Constants;
80 import edu.internet2.middleware.shibboleth.common.OriginSiteMapperException;
81 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
82 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfileFactory;
83
84 /**
85  *  Implements a SAML POST profile consumer
86  *
87  * @author     Scott Cantor
88  * @created    June 10, 2002
89  */
90
91 public class ShireServlet extends HttpServlet {
92
93         private String shireLocation;
94         private String cookieName;
95         private String cookieDomain;
96         private String sessionDir;
97         private String keyStorePath;
98         private String keyStorePasswd;
99         private String keyStoreAlias;
100         private String registryURI;
101         private boolean sslOnly = true;
102         private boolean checkAddress = true;
103         private boolean verbose = false;
104
105         private XMLOriginSiteMapper mapper = null;
106         private static Logger log = Logger.getLogger(ShireServlet.class.getName());
107
108         /**
109          *  Use the following servlet init parameters:<P>
110          *
111          *
112          *  <DL>
113          *    <DT> shire-location <I>(optional)</I> </DT>
114          *    <DD> The URL of the SHIRE if not derivable from requests</DD>
115          *    <DT> keystore-path <I>(required)</I> </DT>
116          *    <DD> A pathname to the trusted CA roots to accept</DD>
117          *    <DT> keystore-password <I>(required)</I> </DT>
118          *    <DD> The root keystore password</DD>
119          *    <DT> registry-alias <I>(optional)</I> </DT>
120          *    <DD> An alias in the provided keystore for the cert that can verify
121          *    the origin site registry signature</DD>
122          *    <DT> registry-uri <I>(required)</I> </DT>
123          *    <DD> The origin site registry URI to install</DD>
124          *    <DT> cookie-name <I>(required)</I> </DT>
125          *    <DD> Name of session cookie to set in browser</DD>
126          *    <DT> cookie-domain <I>(optional)</I> </DT>
127          *    <DD> Domain of session cookie to set in browser</DD>
128          *    <DT> ssl-only <I>(defaults to true)</I> </DT>
129          *    <DD> If true, allow only SSL-protected POSTs and issue a secure cookie
130          *    </DD>
131          *    <DT> check-address <I>(defaults to true)</I> </DT>
132          *    <DD> If true, check client's IP address against assertion</DD>
133          *    <DT> session-dir <I>(defaults to /tmp)</I> </DT>
134          *    <DD> Directory in which to place session files</DD>
135          *  </DL>
136          *
137          */
138         public void init() throws ServletException {
139                 super.init();
140                 log.info("Initializing SHIRE.");
141
142                 edu.internet2.middleware.shibboleth.common.Init.init();
143
144                 loadInitParams();
145                 verifyConfig();
146
147                 log.info("Loading keystore.");
148                 try {
149                         Key k = null;
150                         KeyStore ks = KeyStore.getInstance("JKS");
151                         ks.load(getServletContext().getResourceAsStream(keyStorePath), keyStorePasswd.toCharArray());
152
153                         if (keyStoreAlias != null) {
154                                 Certificate cert;
155                                 cert = ks.getCertificate(keyStoreAlias);
156
157                                 if (cert == null || (k = cert.getPublicKey()) == null) {
158                                         log.fatal(
159                                                 "Unable to load registry verification certificate ("
160                                                         + keyStoreAlias
161                                                         + ") from keystore");
162                                         throw new UnavailableException(
163                                                 "Unable to load registry verification certificate ("
164                                                         + keyStoreAlias
165                                                         + ") from keystore");
166                                 }
167                         }
168
169                         log.info("Loading shibboleth site information.");
170                         mapper = new XMLOriginSiteMapper(registryURI, k, ks);
171                         log.info("Completed SHIRE initialization");
172
173                 } catch (OriginSiteMapperException e) {
174                         log.fatal("Unable load shibboleth site information." + e);
175                         throw new UnavailableException("Unable load shibboleth site information." + e);
176                 } catch (KeyStoreException e) {
177                         log.fatal("Unable supplied keystore." + e);
178                         throw new UnavailableException("Unable load supplied keystore." + e);
179                 } catch (NoSuchAlgorithmException e) {
180                         log.fatal("Unable supplied keystore." + e);
181                         throw new UnavailableException("Unable load supplied keystore." + e);
182                 } catch (CertificateException e) {
183                         log.fatal("Unable supplied keystore." + e);
184                         throw new UnavailableException("Unable load supplied keystore." + e);
185                 } catch (IOException e) {
186                         log.fatal("Unable supplied keystore." + e);
187                         throw new UnavailableException("Unable load supplied keystore." + e);
188                 }
189
190         }
191
192         /**
193          * Ensures that all required initialization attributes have been set.
194          */
195         private void verifyConfig() throws UnavailableException {
196
197                 if (cookieName == null) {
198                         log.fatal("Init parameter (cookie-name) is required in deployment descriptor.");
199                         throw new UnavailableException("Init parameter (cookie-name) is required in deployment descriptor.");
200                 }
201
202                 if (registryURI == null) {
203                         log.fatal("Init parameter (registry-uri) is required in deployment descriptor.");
204                         throw new UnavailableException("Init parameter (registry-uri) is required in deployment descriptor.");
205                 }
206
207                 if (keyStorePath == null) {
208                         log.fatal("Init parameter (keystore-path) is required in deployment descriptor.");
209                         throw new UnavailableException("Init parameter (keystore-path) is required in deployment descriptor.");
210                 }
211
212                 if (keyStorePasswd == null) {
213                         log.fatal("Init parameter (keystore-password) is required in deployment descriptor.");
214                         throw new UnavailableException("Init parameter (keystore-password) is required in deployment descriptor.");
215                 }
216
217         }
218
219         /**
220          * Loads SHIRE configuration parameters.  Sets default values as appropriate.
221          */
222         private void loadInitParams() {
223
224                 log.info("Loading configuration from deployment descriptor (web.xml).");
225
226                 shireLocation = getServletConfig().getInitParameter("shire-location");
227                 cookieDomain = getServletConfig().getInitParameter("cookie-domain");
228                 cookieName = getServletConfig().getInitParameter("cookie-name");
229                 keyStorePath = getServletConfig().getInitParameter("keystore-path");
230                 keyStorePasswd = getServletConfig().getInitParameter("keystore-password");
231                 keyStoreAlias = getServletConfig().getInitParameter("keystore-alias");
232                 registryURI = getServletConfig().getInitParameter("registry-uri");
233
234                 sessionDir = getServletConfig().getInitParameter("session-dir");
235                 if (sessionDir == null) {
236                         sessionDir = "/tmp";
237                         log.warn("No session-dir parameter found... using default location: (" + sessionDir + ").");
238                 }
239
240                 String temp = getServletConfig().getInitParameter("ssl-only");
241                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
242                         sslOnly = false;
243
244                 temp = getServletConfig().getInitParameter("check-address");
245                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
246                         checkAddress = false;
247
248         }
249
250         /**
251          *  Processes a sign-on submission<P>
252          *
253          *
254          *
255          * @param  request               HTTP request context
256          * @param  response              HTTP response context
257          * @exception  IOException       Thrown if an I/O error occurs
258          * @exception  ServletException  Thrown if a servlet engine error occurs
259          */
260         public void doPost(HttpServletRequest request, HttpServletResponse response)
261                 throws IOException, ServletException {
262
263                 try {
264
265                         log.info("Received a handle package.");
266                         log.debug("Target URL from client: " + request.getParameter("TARGET"));
267                         validateRequest(request);
268
269                         SAMLAuthenticationStatement s = processAssertion(request);
270                         shareSession(
271                                 response,
272                                 s.getSubject().getName(),
273                                 s.getSubject().getNameQualifier(),
274                                 System.currentTimeMillis(),
275                                 request.getRemoteAddr(),
276                                 s.getBindings()[0].getBinding(),
277                                 s.getBindings()[0].getLocation());
278
279                         log.info("Redirecting to the requested resource.");
280                         response.sendRedirect(request.getParameter("TARGET"));
281
282                 } catch (ShireException se) {
283                         handleError(se, request, response);
284                 }
285
286         }
287
288         /**
289          * Extracts a SAML Authentication Assertion from a POST request object and performs appropriate validity 
290          * checks on the same. 
291          *
292          * @param  request The <code>HttpServletRequest</code> object for the current request
293          * @exception  ShireException  Thrown if any error is encountered parsing or validating the assertion 
294          * that is retreived from the request object.
295          */
296
297         private SAMLAuthenticationStatement processAssertion(HttpServletRequest request) throws ShireException {
298
299                 log.info("Processing SAML Assertion.");
300                 try {
301                         // Get a profile object using our specifics.
302                         String[] policies = { Constants.POLICY_CLUBSHIB };
303                         ShibPOSTProfile profile =
304                                 ShibPOSTProfileFactory.getInstance(
305                                         policies,
306                                         mapper,
307                                         (shireLocation != null) ? shireLocation : HttpUtils.getRequestURL(request).toString(),
308                                         300);
309
310                         // Try and accept the response...
311                         SAMLResponse r = profile.accept(request.getParameter("SAMLResponse").getBytes());
312
313                         // We've got a valid signed response we can trust (or the whole response was empty...)
314
315                         ByteArrayOutputStream bytestr = new ByteArrayOutputStream();
316                         try {
317                                 r.toStream(bytestr);
318                         } catch (IOException e) {
319                                 log.error(
320                                         "Very Strange... problem converting SAMLResponse to a Stream for logging purposes.");
321                         }
322
323                         log.debug("Parsed SAML Response: " + bytestr.toString());
324
325                         // Get the statement we need.
326                         SAMLAuthenticationStatement s = profile.getSSOStatement(r);
327                         if (s == null) {
328                                 throw new ShireException("The assertion of your Shibboleth identity was missing or incompatible with the policies of this site.");
329                         }
330
331                         if (checkAddress) {
332                                 log.debug("Running with client address checking enabled.");
333                                 log.debug("Client Address from request: " + request.getRemoteAddr());
334                                 log.debug("Client Address from assertion: " + s.getSubjectIP());
335                                 if (s.getSubjectIP() == null || !s.getSubjectIP().equals(request.getRemoteAddr())) {
336                                         throw new ShireException("The IP address provided by your origin site was either missing or did not match your current address.  To correct this problem, you may need to bypass a local proxy server.");
337                                 }
338                         }
339
340                         // All we really need is here...
341                         log.debug("Shibboleth Origin Site: " + s.getSubject().getNameQualifier());
342                         log.debug("Shibboleth Handle: " + s.getSubject().getName());
343                         log.debug("Shibboleth AA URL:</B>" + s.getBindings()[0].getLocation());
344                         return s;
345
346                 } catch (SAMLException e) {
347                         throw new ShireException("Error processing SAML assertion: " + e);
348                 }
349         }
350
351         /**
352          * Makes user information available to SHAR.
353          * 
354          */
355
356         private void shareSession(
357                 HttpServletResponse response,
358                 String handle,
359                 String domain,
360                 long currentTime,
361                 String clientAddress,
362                 String protocolBinding,
363                 String locationBinding)
364                 throws ShireException {
365
366                 log.info("Generating SHIR/SHAR shared data.");
367                 String filename = UUIDGenerator.getInstance().generateRandomBasedUUID().toString();
368                 log.debug("Created unique session identifier: " + filename);
369
370                 // Write session identifier to a file           
371                 String pathname = null;
372                 if (sessionDir.endsWith(File.separator))
373                         pathname = sessionDir + filename;
374                 else
375                         pathname = sessionDir + File.separatorChar + filename;
376                 PrintWriter fout;
377                 try {
378                         log.debug("Writing session data to file: (" + pathname + ")");
379                         fout = new PrintWriter(new FileWriter(pathname));
380
381                         log.debug("Session Pathname: " + pathname);
382
383                         fout.println("Handle=" + handle);
384                         fout.println("Domain=" + domain);
385                         fout.println("PBinding0=" + protocolBinding);
386                         fout.println("LBinding0=" + locationBinding);
387                         fout.println("Time=" + currentTime / 1000);
388                         fout.println("ClientAddress=" + clientAddress);
389                         fout.println("EOF");
390                         fout.close();
391
392                         Cookie cookie = new Cookie(cookieName, filename);
393                         cookie.setPath("/");
394                         if (cookieDomain != null)
395                                 cookie.setDomain(cookieDomain);
396                         log.debug(
397                                 "Adding session identifier to browser cookie: ("
398                                         + cookie.getDomain()
399                                         + ":"
400                                         + cookie.getName()
401                                         + ")");
402                         response.addCookie(cookie);
403
404                 } catch (IOException e) {
405                         throw new ShireException(
406                                 "Unable to write session to file (" + filename + ") : " + e);
407                 }
408         }
409
410         /**
411          * Ensures that the POST request contains the necessary data elements
412          * 
413          * @param request <code>The HttpServletRequest</code> object for the current request
414          * @exception ShireException thrown if required POST data is missing
415          */
416
417         private void validateRequest(HttpServletRequest request) throws ShireException {
418
419                 log.info("Validating POST request properties.");
420
421                 if (sslOnly && !request.isSecure()) {
422                         throw new ShireException("Access to this site requires the use of SSL.");
423                 }
424
425                 if (request.getParameter("TARGET") == null || request.getParameter("TARGET").length() == 0) {
426                         throw new ShireException("Invalid data from HS: No target URL received.");
427                 }
428
429                 if (request.getParameter("SAMLResponse") == null
430                         || request.getParameter("SAMLResponse").length() == 0) {
431                         throw new ShireException("Invalid data from HS: No SAML Assertion included received.");
432                 }
433
434         }
435
436         /**
437          * Appropriately routes all recoverable errors encountered by the SHIRE
438          */
439
440         private void handleError(ShireException se, HttpServletRequest req, HttpServletResponse res) {
441                 log.error(se);
442                 log.debug("Displaying error page.");
443                 req.setAttribute("errorText", se.toString());
444                 req.setAttribute("requestURL", req.getRequestURI().toString());
445                 RequestDispatcher rd = req.getRequestDispatcher("/wayferror.jsp");
446
447                 try {
448                         rd.forward(req, res);
449                 } catch (IOException ioe) {
450                         log.error("Problem trying to display SHIRE error page: " + ioe.toString());
451                 } catch (ServletException servletE) {
452                         log.error("Problem trying to display SHIRE error page: " + servletE.toString());
453                 }
454         }
455 }