Added Barbara's name to the javadoc on the HandleService, since she wrote the origina...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / hs / HandleService.java
1 package edu.internet2.middleware.shibboleth.hs;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.security.Security;
6 import java.util.Date;
7
8 import javax.crypto.SecretKey;
9 import javax.crypto.SecretKeyFactory;
10 import javax.crypto.spec.DESedeKeySpec;
11 import javax.servlet.RequestDispatcher;
12 import javax.servlet.ServletException;
13 import javax.servlet.http.HttpServlet;
14 import javax.servlet.http.HttpServletRequest;
15 import javax.servlet.http.HttpServletResponse;
16 import org.apache.log4j.Logger;
17 import org.apache.log4j.PropertyConfigurator;
18 import org.bouncycastle.jce.provider.BouncyCastleProvider;
19 import org.xml.sax.SAXException;
20
21 import edu.internet2.middleware.shibboleth.AABindingInfo;
22 import edu.internet2.middleware.shibboleth.Policies;
23 import edu.internet2.middleware.shibboleth.SAMLAuthenticationAssertionFactory;
24 import edu.internet2.middleware.shibboleth.SAMLException;
25 import edu.internet2.middleware.shibboleth.common.AttributeQueryHandle;
26 import edu.internet2.middleware.shibboleth.common.Base64;
27 import edu.internet2.middleware.shibboleth.common.HandleException;
28
29 /**
30  * 
31  * A servlet implementation of the Shibboleth Handle Service.  Accepts 
32  * Shibboleth Attribute Query Handle Requests via HTTP GET and generates 
33  * SAML authN assertions containing an opaque user handle.  These assertions are 
34  * embedded in an HTML that auto-POSTs to the referring SHIRE.
35  * 
36  * @author Walter Hoehn wassa@columbia.edu
37  * @author Barbara Jenson blk@cmu.edu
38  *
39  */
40
41 public class HandleService extends HttpServlet {
42
43         private static Logger log = Logger.getLogger(HandleService.class.getName());
44         private SAMLAuthenticationAssertionFactory factory;
45         private String hsConfigFileLocation;
46         private String log4jConfigFileLocation;
47         private SecretKey key;
48
49         /**
50          * @see GenericServlet#init()
51          */
52
53         public void init() throws ServletException {
54
55                 super.init();
56                 loadInitParams();
57                 initLogger();
58                 initConfig();
59                 initViewConfig();
60                 initSecretKey();
61                 initAuthNFactory();
62         }
63
64         /**
65          * Initializes symmetric key for use in AQH creation
66          */
67
68         private void initSecretKey() throws ServletException {
69
70                 try {
71
72                         //Currently hardcoded to use Bouncy Castle
73                         //Decided to change this or not based on overall shibboleth policy
74                         Security.addProvider(new BouncyCastleProvider());
75                         SecretKeyFactory keyFactory =
76                                 SecretKeyFactory.getInstance("DESede");
77                         DESedeKeySpec keySpec =
78                                 new DESedeKeySpec(
79                                         Base64.decode(HandleServiceConfig.getSecretKey()));
80                         key = keyFactory.generateSecret(keySpec);
81                 } catch (Exception t) {
82                         log.fatal("Error reading Secret Key from configuration.", t);
83                         throw new ServletException("Error reading Key from configuration.");
84                 }
85
86         }
87
88         /**
89          * Retrieves location of HS configuration files from the servlet configuration.
90          */
91
92         private void loadInitParams() {
93
94                 hsConfigFileLocation =
95                         getServletConfig().getInitParameter("HSConfigFileLocation");
96                 if (hsConfigFileLocation == null) {
97                         hsConfigFileLocation = "/WEB-INF/conf/shibboleth.xml";
98                 }
99                 log4jConfigFileLocation =
100                         getServletConfig().getInitParameter("log4jConfigFileLocation");
101                 if (log4jConfigFileLocation == null) {
102                         log4jConfigFileLocation = "/WEB-INF/conf/log4j.properties";
103                 }
104
105         }
106
107         /**
108          * Loads HS configuration.
109          */
110
111         private void initConfig() throws ServletException {
112
113                 InputStream is =
114                         getServletContext().getResourceAsStream(hsConfigFileLocation);
115                 HsConfigDigester digester = new HsConfigDigester();
116                 try {
117                         digester.parse(is);
118                 } catch (SAXException se) {
119                         log.fatal("Error parsing HS configuration file.", se);
120                         throw new ServletException(
121                                 "Error parsing HS configuration file.",
122                                 se);
123                 } catch (IOException ioe) {
124                         log.fatal("Error reading HS configuration file.", ioe);
125                         throw new ServletException(
126                                 "Error reading HS configuration file.",
127                                 ioe);
128                 }
129
130         }
131
132         /**
133          * Starts up Log4J.
134          */
135
136         private void initLogger() {
137
138                 PropertyConfigurator.configure(
139                         getServletContext().getRealPath("/") + log4jConfigFileLocation);
140
141         }
142
143         /**
144          * Places configuration parameters in the <code>ServletContext</code> so that they may 
145          * be retreived by view components.
146          */
147
148         private void initViewConfig() {
149                 getServletContext().setAttribute(
150                         "hs_supportContact",
151                         HandleServiceConfig.getSupportContact());
152                 getServletContext().setAttribute(
153                         "hs_logoLocation",
154                         HandleServiceConfig.getLogoLocation());
155                 getServletContext().setAttribute(
156                         "hs_helpText",
157                         HandleServiceConfig.getHelpText());
158                 getServletContext().setAttribute(
159                         "hs_detailedHelpURL",
160                         HandleServiceConfig.getDetailedHelpURL());
161                 String hsLocation = HandleServiceConfig.getLocation();
162                 if (hsLocation == null) {
163                         hsLocation = "HS";
164                 }
165                 getServletContext().setAttribute("hs_location", hsLocation);
166         }
167
168         /**
169          * Initializes SAML AuthN Factory
170          */
171
172         private void initAuthNFactory() throws ServletException {
173                 try {
174                         AABindingInfo[] binfo = new AABindingInfo[1];
175                         binfo[0] =
176                                 new AABindingInfo(
177                                         AABindingInfo.SAML_SOAP_HTTPS,
178                                         HandleServiceConfig.getAaURL());
179                         String[] policies = { Policies.POLICY_URI_CLUBSHIB };
180                         factory =
181                                 SAMLAuthenticationAssertionFactory.getInstance(
182                                         policies,
183                                         HandleServiceConfig.getIssuer(),
184                                         HandleServiceConfig.getDomain(),
185                                         binfo,
186                                         null,
187                                         null);
188
189                 } catch (SAMLException se) {
190                         log.fatal("Error initializing SAML library: ", se);
191                         throw new ServletException("Error initializing SAML library: ", se);
192                 }
193         }
194
195         /**
196          * @see HttpServlet#doGet(HttpServletRequest, HttpServletResponse)
197          */
198
199         public void doGet(HttpServletRequest req, HttpServletResponse resp)
200                 throws ServletException, IOException {
201
202                 try {
203                         validateRequestParameters(req);
204                         req.setAttribute("shire", req.getParameter("shire"));
205                         req.setAttribute("target", req.getParameter("target"));
206                         log.info("Generating assertion...");
207                         long startTime = System.currentTimeMillis();
208                         byte[] assertion =
209                                 generateAssertion(
210                                         req.getParameter("shire"),
211                                         req.getRemoteAddr(),
212                                         req.getRemoteUser(),
213                                         req.getAuthType());
214                         log.info("Assertion Generated: " + "elapsed time " + (System.currentTimeMillis() - startTime) + " milliseconds.");
215                         log.debug("Assertion: " + new String(Base64.decode(assertion)));
216                         handleForm(req, resp, assertion);
217                 } catch (HandleServiceException e) {
218                         handleError(req, resp, e);
219                 }
220
221         }
222
223         /**
224          * Deals with HS runtime exceptions.  Logs errors locally and then 
225          * formats them for output to user.
226          * 
227          * @param e The Exception to be handled
228          */
229
230         private void handleError(
231                 HttpServletRequest req,
232                 HttpServletResponse res,
233                 Exception e) {
234
235                 log.info("Handle Service Failure: " + e);
236
237                 req.setAttribute("errorText", e.toString());
238                 RequestDispatcher rd = req.getRequestDispatcher("/hserror.jsp");
239
240                 try {
241                         rd.forward(req, res);
242                 } catch (IOException ioe) {
243                         log.info(
244                                 "IO operation interrupted when displaying Handle Service error page: " + ioe);
245                 } catch (ServletException se) {
246                         log.error(
247                                 "Problem trying to display Handle Service error page: " + se);
248                 }
249         }
250
251         /**
252          * Method for auto-POSTing a Base64 encoded SAML assertion.
253          * 
254          * @param assertion Base64 encoded SAML authN assertion
255          */
256
257         private void handleForm(
258                 HttpServletRequest req,
259                 HttpServletResponse res,
260                 byte[] assertion)
261                 throws HandleServiceException {
262
263                 try {
264                         //Hardcoded to ASCII to ensure Base64 encoding compatibility
265                         req.setAttribute("assertion", new String(assertion, "ASCII"));
266                         RequestDispatcher rd = req.getRequestDispatcher("/hs.jsp");
267                         log.info("POSTing assertion to SHIRE.");
268                         rd.forward(req, res);
269                 } catch (IOException ioe) {
270                         throw new HandleServiceException(
271                                 "IO interruption while displaying Handle Service UI." + ioe);
272                 } catch (ServletException se) {
273                         throw new HandleServiceException(
274                                 "Problem displaying Handle Service UI." + se);
275                 }
276         }
277
278         /**
279          * Generates a new <code>AttributeQueryHandle</code> and includes it in a 
280          * <code>SAMLAuthenticationAssertion</code>.
281          */
282
283         private byte[] generateAssertion(
284                 String shireURL,
285                 String clientAddress,
286                 String remoteUser,
287                 String authType)
288                 throws HandleServiceException {
289                 try {
290
291                         AttributeQueryHandle aqh =
292                                 new AttributeQueryHandle(
293                                         remoteUser,
294                                         key,
295                                         Long.parseLong(HandleServiceConfig.getValidityPeriod()),
296                                         HandleServiceConfig.getLocation());
297
298                         log.info("Acquired Handle: " + aqh.getHandleID());
299
300                         return factory
301                                 .getAssertion(
302                                         aqh.serialize(),
303                                         shireURL,
304                                         clientAddress,
305                                         authType,
306                                         new Date(),
307                                         null)
308                                 .toBase64();
309
310                 } catch (SAMLException se) {
311                         throw new HandleServiceException(
312                                 "Error creating SAML assertion: " + se);
313                 } catch (IOException ioe) {
314                         throw new HandleServiceException(
315                                 "Error creating SAML assertion: " + ioe);
316                 } catch (HandleException he) {
317                         throw new HandleServiceException(
318                                 "Error creating User Handle: " + he);
319                 }
320         }
321
322         /**
323          * Ensures that <code>HttpServletRequest</code> contains all of the parameters necessary
324          * for generation of an <code>AttributeQueryHandle</code>.
325          */
326
327         private void validateRequestParameters(HttpServletRequest req)
328                 throws HandleServiceException {
329
330                 if ((req.getParameter("shire") == null)
331                         || (req.getParameter("shire").equals(""))) {
332                         throw new HandleServiceException("Invalid data from SHIRE: No acceptance URL received.");
333                 }
334                 if ((req.getParameter("target") == null)
335                         || (req.getParameter("target").equals(""))) {
336                         throw new HandleServiceException("Invalid data from SHIRE: No target URL received.");
337                 }
338                 if ((req.getRemoteUser() == null)
339                         || (req.getRemoteUser().equals(""))) {
340                         throw new HandleServiceException("No authentication received from webserver.");
341                 }
342                 if ((req.getAuthType() == null) || (req.getAuthType().equals(""))) {
343                         throw new HandleServiceException("Unable to ascertain authentication type.");
344                 }
345                 if ((req.getRemoteAddr() == null)
346                         || (req.getRemoteAddr().equals(""))) {
347                         throw new HandleServiceException("Unable to ascertain client address.");
348                 }
349         }
350
351 }