Fixed typo in a log message.
[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 import sun.misc.BASE64Decoder;
79
80 import edu.internet2.middleware.shibboleth.common.Constants;
81 import edu.internet2.middleware.shibboleth.common.OriginSiteMapperException;
82 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
83 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfileFactory;
84
85 /**
86  *  Implements a SAML POST profile consumer
87  *
88  * @author     Scott Cantor
89  * @created    June 10, 2002
90  */
91
92 public class ShireServlet extends HttpServlet {
93
94         private String shireLocation;
95         private String cookieName;
96         private String cookieDomain;
97         private String sessionDir;
98         private String keyStorePath;
99         private String keyStorePasswd;
100         private String keyStoreAlias;
101         private String registryURI;
102         private boolean sslOnly = true;
103         private boolean checkAddress = true;
104         private boolean verbose = false;
105
106         private XMLOriginSiteMapper mapper = null;
107         private static Logger log = Logger.getLogger(ShireServlet.class.getName());
108
109         /**
110          *  Use the following servlet init parameters:<P>
111          *
112          *
113          *  <DL>
114          *    <DT> shire-location <I>(optional)</I> </DT>
115          *    <DD> The URL of the SHIRE if not derivable from requests</DD>
116          *    <DT> keystore-path <I>(required)</I> </DT>
117          *    <DD> A pathname to the trusted CA roots to accept</DD>
118          *    <DT> keystore-password <I>(required)</I> </DT>
119          *    <DD> The root keystore password</DD>
120          *    <DT> keystore-alias <I>(optional)</I> </DT>
121          *    <DD> An alias in the provided keystore for the cert that can verify
122          *    the origin site registry signature</DD>
123          *    <DT> registry-uri <I>(required)</I> </DT>
124          *    <DD> The origin site registry URI to install</DD>
125          *    <DT> cookie-name <I>(required)</I> </DT>
126          *    <DD> Name of session cookie to set in browser</DD>
127          *    <DT> cookie-domain <I>(optional)</I> </DT>
128          *    <DD> Domain of session cookie to set in browser</DD>
129          *    <DT> ssl-only <I>(defaults to true)</I> </DT>
130          *    <DD> If true, allow only SSL-protected POSTs and issue a secure cookie
131          *    </DD>
132          *    <DT> check-address <I>(defaults to true)</I> </DT>
133          *    <DD> If true, check client's IP address against assertion</DD>
134          *    <DT> session-dir <I>(defaults to /tmp)</I> </DT>
135          *    <DD> Directory in which to place session files</DD>
136          *  </DL>
137          *
138          */
139         public void init() throws ServletException {
140                 super.init();
141                 log.info("Initializing SHIRE.");
142
143                 edu.internet2.middleware.shibboleth.common.Init.init();
144
145                 loadInitParams();
146                 verifyConfig();
147
148                 log.info("Loading keystore.");
149                 try {
150                         Key k = null;
151                         KeyStore ks = KeyStore.getInstance("JKS");
152                         ks.load(getServletContext().getResourceAsStream(keyStorePath), keyStorePasswd.toCharArray());
153
154                         log.debug("Configured to use keystore-alias (" + keyStoreAlias + ") to verify site file.");
155                         if (keyStoreAlias != null) {
156                                 Certificate cert;
157                                 cert = ks.getCertificate(keyStoreAlias);
158
159                                 if (cert == null || (k = cert.getPublicKey()) == null) {
160                                         log.fatal(
161                                                 "Unable to load registry verification certificate ("
162                                                         + keyStoreAlias
163                                                         + ") from keystore");
164                                         throw new UnavailableException(
165                                                 "Unable to load registry verification certificate ("
166                                                         + keyStoreAlias
167                                                         + ") from keystore");
168                                 }
169                         }
170
171                         log.info("Loading shibboleth site information.");
172                         mapper = new XMLOriginSiteMapper(registryURI, k, ks);
173                         log.info("Completed SHIRE initialization");
174
175                 } catch (OriginSiteMapperException e) {
176                         log.fatal("Configuration problem: Unable load shibboleth site information." + e);
177                         throw new UnavailableException(
178                                 "Configuration problem: Unable load shibboleth site information." + e);
179                 } catch (KeyStoreException e) {
180                         log.fatal("Configuration problem: Unable to load supplied keystore." + e);
181                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
182                 } catch (NoSuchAlgorithmException e) {
183                         log.fatal("Configuration problem: Unable to load supplied keystore." + e);
184                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
185                 } catch (CertificateException e) {
186                         log.fatal("Configuration problem: Unable to load supplied keystore." + e);
187                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
188                 } catch (IOException e) {
189                         log.fatal("Configuration problem: Unable to loadsupplied keystore." + e);
190                         throw new UnavailableException("Configuration problem: Unable load supplied keystore." + e);
191                 }
192
193         }
194
195         /**
196          * Ensures that all required initialization attributes have been set.
197          */
198         private void verifyConfig() throws UnavailableException {
199
200                 if (cookieName == null) {
201                         log.fatal("Init parameter (cookie-name) is required in deployment descriptor.");
202                         throw new UnavailableException("Init parameter (cookie-name) is required in deployment descriptor.");
203                 }
204
205                 if (registryURI == null) {
206                         log.fatal("Init parameter (registry-uri) is required in deployment descriptor.");
207                         throw new UnavailableException("Init parameter (registry-uri) is required in deployment descriptor.");
208                 }
209
210                 if (keyStorePath == null) {
211                         log.fatal("Init parameter (keystore-path) is required in deployment descriptor.");
212                         throw new UnavailableException("Init parameter (keystore-path) is required in deployment descriptor.");
213                 }
214
215                 if (keyStorePasswd == null) {
216                         log.fatal("Init parameter (keystore-password) is required in deployment descriptor.");
217                         throw new UnavailableException("Init parameter (keystore-password) is required in deployment descriptor.");
218                 }
219
220         }
221
222         /**
223          * Loads SHIRE configuration parameters.  Sets default values as appropriate.
224          */
225         private void loadInitParams() {
226
227                 log.info("Loading configuration from deployment descriptor (web.xml).");
228
229                 shireLocation = getServletConfig().getInitParameter("shire-location");
230                 cookieDomain = getServletConfig().getInitParameter("cookie-domain");
231                 cookieName = getServletConfig().getInitParameter("cookie-name");
232                 keyStorePath = getServletConfig().getInitParameter("keystore-path");
233                 keyStorePasswd = getServletConfig().getInitParameter("keystore-password");
234                 keyStoreAlias = getServletConfig().getInitParameter("keystore-alias");
235                 registryURI = getServletConfig().getInitParameter("registry-uri");
236
237                 sessionDir = getServletConfig().getInitParameter("session-dir");
238                 if (sessionDir == null) {
239                         sessionDir = "/tmp";
240                         log.warn("No session-dir parameter found... using default location: (" + sessionDir + ").");
241                 }
242
243                 String temp = getServletConfig().getInitParameter("ssl-only");
244                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
245                         sslOnly = false;
246
247                 temp = getServletConfig().getInitParameter("check-address");
248                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
249                         checkAddress = false;
250
251         }
252
253         /**
254          *  Processes a sign-on submission<P>
255          *
256          *
257          *
258          * @param  request               HTTP request context
259          * @param  response              HTTP response context
260          * @exception  IOException       Thrown if an I/O error occurs
261          * @exception  ServletException  Thrown if a servlet engine error occurs
262          */
263         public void doPost(HttpServletRequest request, HttpServletResponse response)
264                 throws IOException, ServletException {
265
266                 try {
267
268                         log.info("Received a handle package.");
269                         log.debug("Target URL from client: " + request.getParameter("TARGET"));
270                         validateRequest(request);
271
272                         SAMLAuthenticationStatement s = processAssertion(request);
273                         shareSession(
274                                 response,
275                                 s.getSubject().getName(),
276                                 s.getSubject().getNameQualifier(),
277                                 System.currentTimeMillis(),
278                                 request.getRemoteAddr(),
279                                 s.getBindings()[0].getBinding(),
280                                 s.getBindings()[0].getLocation());
281
282                         log.info("Redirecting to the requested resource.");
283                         response.sendRedirect(request.getParameter("TARGET"));
284
285                 } catch (ShireException se) {
286                         handleError(se, request, response);
287                 }
288
289         }
290
291         /**
292          * Extracts a SAML Authentication Assertion from a POST request object and performs appropriate validity 
293          * checks on the same. 
294          *
295          * @param  request The <code>HttpServletRequest</code> object for the current request
296          * @exception  ShireException  Thrown if any error is encountered parsing or validating the assertion 
297          * that is retreived from the request object.
298          */
299
300         private SAMLAuthenticationStatement processAssertion(HttpServletRequest request) throws ShireException {
301
302                 log.info("Processing SAML Assertion.");
303                 try {
304                         // Get a profile object using our specifics.
305                         String[] policies = { Constants.POLICY_CLUBSHIB };
306                         ShibPOSTProfile profile =
307                                 ShibPOSTProfileFactory.getInstance(
308                                         policies,
309                                         mapper,
310                                         (shireLocation != null) ? shireLocation : HttpUtils.getRequestURL(request).toString(),
311                                         300);
312
313                         if (log.isDebugEnabled()) {
314                                 try {
315                                         log.debug(
316                                                 "Dumping unparsed SAML Response:"
317                                                         + System.getProperty("line.separator")
318                                                         + new String(
319                                                                 new BASE64Decoder().decodeBuffer(request.getParameter("SAMLResponse")),
320                                                                 "UTF8"));
321                                 } catch (IOException e) {
322                                         log.error("Encountered an error while decoding SAMLReponse for loggin purposes.");
323                                 }
324                         }
325
326                         // Try and accept the response...
327                         SAMLResponse r = profile.accept(request.getParameter("SAMLResponse").getBytes());
328
329                         // We've got a valid signed response we can trust (or the whole response was empty...)
330
331                         ByteArrayOutputStream bytestr = new ByteArrayOutputStream();
332                         try {
333                                 r.toStream(bytestr);
334                         } catch (IOException e) {
335                                 log.error(
336                                         "Very Strange... problem converting SAMLResponse to a Stream for logging purposes.");
337                         }
338
339                         log.debug(
340                                 "Dumping parsed SAML Response:" + System.getProperty("line.separator") + bytestr.toString());
341
342                         // Get the statement we need.
343                         SAMLAuthenticationStatement s = profile.getSSOStatement(r);
344                         if (s == null) {
345                                 throw new ShireException("The assertion of your Shibboleth identity was missing or incompatible with the policies of this site.");
346                         }
347
348                         if (checkAddress) {
349                                 log.debug("Running with client address checking enabled.");
350                                 log.debug("Client Address from request: " + request.getRemoteAddr());
351                                 log.debug("Client Address from assertion: " + s.getSubjectIP());
352                                 if (s.getSubjectIP() == null || !s.getSubjectIP().equals(request.getRemoteAddr())) {
353                                         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.");
354                                 }
355                         } else {
356                                 log.debug("Running with client address checking disabled.");
357                         }
358
359                         // All we really need is here...
360                         log.debug("Shibboleth Origin Site: " + s.getSubject().getNameQualifier());
361                         log.debug("Shibboleth Handle: " + s.getSubject().getName());
362                         log.debug("Shibboleth AA URL: " + s.getBindings()[0].getLocation());
363                         return s;
364
365                 } catch (SAMLException e) {
366                         throw new ShireException("Error processing SAML assertion: " + e);
367                 }
368         }
369
370         /**
371          * Makes user information available to SHAR.
372          * 
373          */
374
375         private void shareSession(
376                 HttpServletResponse response,
377                 String handle,
378                 String domain,
379                 long currentTime,
380                 String clientAddress,
381                 String protocolBinding,
382                 String locationBinding)
383                 throws ShireException {
384
385                 log.info("Generating SHIR/SHAR shared data.");
386                 String filename = UUIDGenerator.getInstance().generateRandomBasedUUID().toString();
387                 log.debug("Created unique session identifier: " + filename);
388
389                 // Write session identifier to a file           
390                 String pathname = null;
391                 if (sessionDir.endsWith(File.separator))
392                         pathname = sessionDir + filename;
393                 else
394                         pathname = sessionDir + File.separatorChar + filename;
395                 PrintWriter fout;
396                 try {
397                         log.debug("Writing session data to file: (" + pathname + ")");
398                         fout = new PrintWriter(new FileWriter(pathname));
399
400                         log.debug("Session Pathname: " + pathname);
401
402                         fout.println("Handle=" + handle);
403                         fout.println("Domain=" + domain);
404                         fout.println("PBinding0=" + protocolBinding);
405                         fout.println("LBinding0=" + locationBinding);
406                         fout.println("Time=" + currentTime / 1000);
407                         fout.println("ClientAddress=" + clientAddress);
408                         fout.println("EOF");
409                         fout.close();
410
411                         Cookie cookie = new Cookie(cookieName, filename);
412                         cookie.setPath("/");
413                         if (cookieDomain != null)
414                                 cookie.setDomain(cookieDomain);
415                         log.debug(
416                                 "Adding session identifier to browser cookie: ("
417                                         + cookie.getDomain()
418                                         + ":"
419                                         + cookie.getName()
420                                         + ")");
421                         response.addCookie(cookie);
422
423                 } catch (IOException e) {
424                         throw new ShireException("Unable to write session to file (" + filename + ") : " + e);
425                 }
426         }
427
428         /**
429          * Ensures that the POST request contains the necessary data elements
430          * 
431          * @param request <code>The HttpServletRequest</code> object for the current request
432          * @exception ShireException thrown if required POST data is missing
433          */
434
435         private void validateRequest(HttpServletRequest request) throws ShireException {
436
437                 log.info("Validating POST request properties.");
438
439                 if (sslOnly && !request.isSecure()) {
440                         throw new ShireException("Access to this site requires the use of SSL.");
441                 }
442
443                 if (request.getParameter("TARGET") == null || request.getParameter("TARGET").length() == 0) {
444                         throw new ShireException("Invalid data from HS: No target URL received.");
445                 }
446
447                 if (request.getParameter("SAMLResponse") == null
448                         || request.getParameter("SAMLResponse").length() == 0) {
449                         throw new ShireException("Invalid data from HS: No SAML Assertion included received.");
450                 }
451
452         }
453
454         /**
455          * Appropriately routes all recoverable errors encountered by the SHIRE
456          */
457
458         private void handleError(ShireException se, HttpServletRequest req, HttpServletResponse res) {
459                 log.error(se);
460                 log.debug("Displaying error page.");
461                 req.setAttribute("errorText", se.toString());
462                 req.setAttribute("requestURL", req.getRequestURI().toString());
463                 RequestDispatcher rd = req.getRequestDispatcher("/shireerror.jsp");
464
465                 try {
466                         rd.forward(req, res);
467                 } catch (IOException ioe) {
468                         log.error("Problem trying to display SHIRE error page: " + ioe.toString());
469                 } catch (ServletException servletE) {
470                         log.error("Problem trying to display SHIRE error page: " + servletE.toString());
471                 }
472         }
473 }