Refactored the error handling in the SHIRE servlet POST processing. Separated presen...
[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
172                 } catch (OriginSiteMapperException e) {
173                         log.fatal("Unable load shibboleth site information." + e.getMessage());
174                         throw new UnavailableException("Unable load shibboleth site information." + e.getMessage());
175                 } catch (KeyStoreException e) {
176                         log.fatal("Unable supplied keystore." + e.getMessage());
177                         throw new UnavailableException("Unable load supplied keystore." + e.getMessage());
178                 } catch (NoSuchAlgorithmException e) {
179                         log.fatal("Unable supplied keystore." + e.getMessage());
180                         throw new UnavailableException("Unable load supplied keystore." + e.getMessage());
181                 } catch (CertificateException e) {
182                         log.fatal("Unable supplied keystore." + e.getMessage());
183                         throw new UnavailableException("Unable load supplied keystore." + e.getMessage());
184                 } catch (IOException e) {
185                         log.fatal("Unable supplied keystore." + e.getMessage());
186                         throw new UnavailableException("Unable load supplied keystore." + e.getMessage());
187                 }
188
189         }
190
191         /**
192          * Ensures that all required initialization attributes have been set.
193          */
194         private void verifyConfig() throws UnavailableException {
195
196                 if (cookieName == null) {
197                         log.fatal("Init parameter (cookie-name) is required in deployment descriptor.");
198                         throw new UnavailableException("Init parameter (cookie-name) is required in deployment descriptor.");
199                 }
200
201                 if (registryURI == null) {
202                         log.fatal("Init parameter (registry-uri) is required in deployment descriptor.");
203                         throw new UnavailableException("Init parameter (registry-uri) is required in deployment descriptor.");
204                 }
205
206                 if (keyStorePath == null) {
207                         log.fatal("Init parameter (keystore-path) is required in deployment descriptor.");
208                         throw new UnavailableException("Init parameter (keystore-path) is required in deployment descriptor.");
209                 }
210
211                 if (keyStorePasswd == null) {
212                         log.fatal("Init parameter (keystore-password) is required in deployment descriptor.");
213                         throw new UnavailableException("Init parameter (keystore-password) is required in deployment descriptor.");
214                 }
215
216         }
217
218         /**
219          * Loads SHIRE configuration parameters.  Sets default values as appropriate.
220          */
221         private void loadInitParams() {
222
223                 log.info("Loading configuration from deployment descriptor (web.xml).");
224
225                 shireLocation = getServletConfig().getInitParameter("shire-location");
226                 cookieDomain = getServletConfig().getInitParameter("cookie-domain");
227                 cookieName = getServletConfig().getInitParameter("cookie-name");
228                 keyStorePath = getServletConfig().getInitParameter("keystore-path");
229                 keyStorePasswd = getServletConfig().getInitParameter("keystore-password");
230                 keyStoreAlias = getServletConfig().getInitParameter("keystore-alias");
231                 registryURI = getServletConfig().getInitParameter("registry-uri");
232
233                 sessionDir = getServletConfig().getInitParameter("session-dir");
234                 if (sessionDir == null) {
235                         sessionDir = "/tmp";
236                         log.warn("No session-dir parameter found... using default location: (" + sessionDir + ").");
237                 }
238
239                 String temp = getServletConfig().getInitParameter("ssl-only");
240                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
241                         sslOnly = false;
242
243                 temp = getServletConfig().getInitParameter("check-address");
244                 if (temp != null && (temp.equalsIgnoreCase("false") || temp.equals("0")))
245                         checkAddress = false;
246
247         }
248
249         /**
250          *  Processes a sign-on submission<P>
251          *
252          *
253          *
254          * @param  request               HTTP request context
255          * @param  response              HTTP response context
256          * @exception  IOException       Thrown if an I/O error occurs
257          * @exception  ServletException  Thrown if a servlet engine error occurs
258          */
259         public void doPost(HttpServletRequest request, HttpServletResponse response)
260                 throws IOException, ServletException {
261
262                 try {
263
264                         log.info("Received a handle package.");
265                         log.debug("Target URL from client: " + request.getParameter("TARGET"));
266                         validateRequest(request);
267
268                         SAMLAuthenticationStatement s = processAssertion(request);
269                         shareSession(
270                                 response,
271                                 s.getSubject().getName(),
272                                 s.getSubject().getNameQualifier(),
273                                 System.currentTimeMillis(),
274                                 request.getRemoteAddr(),
275                                 s.getBindings()[0].getBinding(),
276                                 s.getBindings()[0].getLocation());
277
278                         log.info("Redirecting to the requested resource.");
279                         response.sendRedirect(request.getParameter("TARGET"));
280
281                 } catch (ShireException se) {
282                         handleError(se, request, response);
283                 }
284
285         }
286
287         /**
288          * Extracts a SAML Authentication Assertion from a POST request object and performs appropriate validity 
289          * checks on the same. 
290          *
291          * @param  request The <code>HttpServletRequest</code> object for the current request
292          * @exception  ShireException  Thrown if any error is encountered parsing or validating the assertion 
293          * that is retreived from the request object.
294          */
295
296         private SAMLAuthenticationStatement processAssertion(HttpServletRequest request) throws ShireException {
297
298                 log.info("Processing SAML Assertion.");
299                 try {
300                         // Get a profile object using our specifics.
301                         String[] policies = { Constants.POLICY_CLUBSHIB };
302                         ShibPOSTProfile profile =
303                                 ShibPOSTProfileFactory.getInstance(
304                                         policies,
305                                         mapper,
306                                         (shireLocation != null) ? shireLocation : HttpUtils.getRequestURL(request).toString(),
307                                         300);
308
309                         // Try and accept the response...
310                         SAMLResponse r = profile.accept(request.getParameter("SAMLResponse").getBytes());
311
312                         // We've got a valid signed response we can trust (or the whole response was empty...)
313
314                         ByteArrayOutputStream bytestr = new ByteArrayOutputStream();
315                         try {
316                                 r.toStream(bytestr);
317                         } catch (IOException e) {
318                                 log.error(
319                                         "Very Strange... problem converting SAMLResponse to a Stream for logging purposes.");
320                         }
321
322                         log.debug("Parsed SAML Response: " + bytestr.toString());
323
324                         // Get the statement we need.
325                         SAMLAuthenticationStatement s = profile.getSSOStatement(r);
326                         if (s == null) {
327                                 throw new ShireException("The assertion of your Shibboleth identity was missing or incompatible with the policies of this site.");
328                         }
329
330                         if (checkAddress) {
331                                 log.debug("Running with client address checking enabled.");
332                                 log.debug("Client Address from request: " + request.getRemoteAddr());
333                                 log.debug("Client Address from assertion: " + s.getSubjectIP());
334                                 if (s.getSubjectIP() == null || !s.getSubjectIP().equals(request.getRemoteAddr())) {
335                                         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.");
336                                 }
337                         }
338
339                         // All we really need is here...
340                         log.debug("Shibboleth Origin Site: " + s.getSubject().getNameQualifier());
341                         log.debug("Shibboleth Handle: " + s.getSubject().getName());
342                         log.debug("Shibboleth AA URL:</B>" + s.getBindings()[0].getLocation());
343                         return s;
344
345                 } catch (SAMLException e) {
346                         throw new ShireException("Error processing SAML assertion: " + e.getMessage());
347                 }
348         }
349
350         /**
351          * Makes user information available to SHAR.
352          * 
353          */
354
355         private void shareSession(
356                 HttpServletResponse response,
357                 String handle,
358                 String domain,
359                 long currentTime,
360                 String clientAddress,
361                 String protocolBinding,
362                 String locationBinding)
363                 throws ShireException {
364
365                 log.info("Generating SHIR/SHAR shared data.");
366                 String filename = UUIDGenerator.getInstance().generateRandomBasedUUID().toString();
367                 log.debug("Created unique session identifier: " + filename);
368
369                 // Write session identifier to a file           
370                 String pathname = null;
371                 if (sessionDir.endsWith(File.separator))
372                         pathname = sessionDir + filename;
373                 else
374                         pathname = sessionDir + File.separatorChar + filename;
375                 PrintWriter fout;
376                 try {
377                         log.debug("Writing session data to file: (" + pathname + ")");
378                         fout = new PrintWriter(new FileWriter(pathname));
379
380                         log.debug("Session Pathname: " + pathname);
381
382                         fout.println("Handle=" + handle);
383                         fout.println("Domain=" + domain);
384                         fout.println("PBinding0=" + protocolBinding);
385                         fout.println("LBinding0=" + locationBinding);
386                         fout.println("Time=" + currentTime / 1000);
387                         fout.println("ClientAddress=" + clientAddress);
388                         fout.println("EOF");
389                         fout.close();
390
391                         Cookie cookie = new Cookie(cookieName, filename);
392                         cookie.setPath("/");
393                         if (cookieDomain != null)
394                                 cookie.setDomain(cookieDomain);
395                         log.debug(
396                                 "Adding session identifier to browser cookie: ("
397                                         + cookie.getDomain()
398                                         + ":"
399                                         + cookie.getName()
400                                         + ")");
401                         response.addCookie(cookie);
402
403                 } catch (IOException e) {
404                         throw new ShireException(
405                                 "Unable to write session to file (" + filename + ") : " + e.getMessage());
406                 }
407         }
408
409         /**
410          * Ensures that the POST request contains the necessary data elements
411          * 
412          * @param request <code>The HttpServletRequest</code> object for the current request
413          * @exception ShireException thrown if required POST data is missing
414          */
415
416         private void validateRequest(HttpServletRequest request) throws ShireException {
417
418                 log.info("Validating POST request properties.");
419
420                 if (sslOnly && !request.isSecure()) {
421                         throw new ShireException("Access to this site requires the use of SSL.");
422                 }
423
424                 if (request.getParameter("TARGET") == null || request.getParameter("TARGET").length() == 0) {
425                         throw new ShireException("Invalid data from HS: No target URL received.");
426                 }
427
428                 if (request.getParameter("SAMLResponse") == null
429                         || request.getParameter("SAMLResponse").length() == 0) {
430                         throw new ShireException("Invalid data from HS: No SAML Assertion included received.");
431                 }
432
433         }
434
435         /**
436          * Appropriately routes all recoverable errors encountered by the SHIRE
437          */
438
439         private void handleError(ShireException se, HttpServletRequest req, HttpServletResponse res) {
440                 log.error(se.getMessage());
441                 log.debug("Displaying error page.");
442                 req.setAttribute("errorText", se.getMessage());
443                 req.setAttribute("requestURL", req.getRequestURI().toString());
444                 RequestDispatcher rd = req.getRequestDispatcher("/wayferror.jsp");
445
446                 try {
447                         rd.forward(req, res);
448                 } catch (IOException ioe) {
449                         log.error("Problem trying to display SHIRE error page: " + ioe.toString());
450                 } catch (ServletException servletE) {
451                         log.error("Problem trying to display SHIRE error page: " + servletE.toString());
452                 }
453         }
454 }