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