5951965e4aa11a1fa819be3237e88ada3dffada6
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / IdPResponder.java
1 /*
2  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
3  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
4  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
5  * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
6  * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
7  * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
8  * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
9  * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
10  * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
11  * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
12  * products derived from this software without specific prior written permission. For written permission, please contact
13  * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
14  * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
15  * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
16  * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
18  * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
19  * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
20  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 package edu.internet2.middleware.shibboleth.idp;
27
28 import java.io.IOException;
29 import java.net.URI;
30 import java.net.URISyntaxException;
31 import java.util.HashMap;
32 import java.util.Random;
33
34 import javax.servlet.RequestDispatcher;
35 import javax.servlet.ServletException;
36 import javax.servlet.UnavailableException;
37 import javax.servlet.http.HttpServlet;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40
41 import org.apache.log4j.Logger;
42 import org.apache.log4j.MDC;
43 import org.opensaml.SAMLBinding;
44 import org.opensaml.SAMLBindingFactory;
45 import org.opensaml.SAMLException;
46 import org.opensaml.SAMLRequest;
47 import org.opensaml.SAMLResponse;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.Element;
50 import org.w3c.dom.NodeList;
51
52 import sun.misc.BASE64Decoder;
53 import edu.internet2.middleware.shibboleth.aa.arp.ArpEngine;
54 import edu.internet2.middleware.shibboleth.aa.arp.ArpException;
55 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
56 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolverException;
57 import edu.internet2.middleware.shibboleth.common.Credentials;
58 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
59 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
60 import edu.internet2.middleware.shibboleth.common.NameMapper;
61 import edu.internet2.middleware.shibboleth.common.OriginConfig;
62 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapper;
63 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapperException;
64 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
65 import edu.internet2.middleware.shibboleth.idp.provider.ShibbolethV1SSOHandler;
66 import edu.internet2.middleware.shibboleth.metadata.Metadata;
67 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
68
69 /**
70  * Primary entry point for requests to the SAML IdP. Listens on multiple endpoints, routes requests to the appropriate
71  * IdP processing components, and delivers proper protocol responses.
72  * 
73  * @author Walter Hoehn
74  */
75
76 public class IdPResponder extends HttpServlet {
77
78         private static Logger transactionLog = Logger.getLogger("Shibboleth-TRANSACTION");
79         private static Logger log = Logger.getLogger(IdPResponder.class.getName());
80         private static Random idgen = new Random();
81         private SAMLBinding binding;
82         private Semaphore throttle;
83         private IdPConfig configuration;
84         private HashMap protocolHandlers = new HashMap();
85         private IdPProtocolSupport protocolSupport;
86
87         /*
88          * @see javax.servlet.GenericServlet#init()
89          */
90         public void init() throws ServletException {
91
92                 super.init();
93                 MDC.put("serviceId", "[IdP] Core");
94                 log.info("Initializing Identity Provider.");
95
96                 try {
97                         binding = SAMLBindingFactory.getInstance(SAMLBinding.SOAP);
98
99                         Document originConfig = OriginConfig.getOriginConfig(this.getServletContext());
100
101                         // Load global configuration properties
102                         configuration = new IdPConfig(originConfig.getDocumentElement());
103
104                         // Load a semaphore that throttles how many requests the IdP will handle at once
105                         throttle = new Semaphore(configuration.getMaxThreads());
106
107                         // Load name mappings
108                         NameMapper nameMapper = new NameMapper();
109                         NodeList itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(
110                                         NameIdentifierMapping.mappingNamespace, "NameMapping");
111
112                         for (int i = 0; i < itemElements.getLength(); i++) {
113                                 try {
114                                         nameMapper.addNameMapping((Element) itemElements.item(i));
115                                 } catch (NameIdentifierMappingException e) {
116                                         log.error("Name Identifier mapping could not be loaded: " + e);
117                                 }
118                         }
119
120                         // Load signing credentials
121                         itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(Credentials.credentialsNamespace,
122                                         "Credentials");
123                         if (itemElements.getLength() < 1) {
124                                 log.error("No credentials specified.");
125                         }
126                         if (itemElements.getLength() > 1) {
127                                 log.error("Multiple Credentials specifications found, using first.");
128                         }
129                         Credentials credentials = new Credentials((Element) itemElements.item(0));
130
131                         // Load relying party config
132                         ServiceProviderMapper spMapper;
133                         try {
134                                 spMapper = new ServiceProviderMapper(originConfig.getDocumentElement(), configuration, credentials,
135                                                 nameMapper);
136                         } catch (ServiceProviderMapperException e) {
137                                 log.error("Could not load Identity Provider configuration: " + e);
138                                 throw new ShibbolethConfigurationException("Could not load Identity Provider configuration.");
139                         }
140
141                         // Startup Attribute Resolver & ARP engine
142                         AttributeResolver resolver = null;
143                         ArpEngine arpEngine = null;
144                         try {
145                                 resolver = new AttributeResolver(configuration);
146
147                                 itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(
148                                                 IdPConfig.originConfigNamespace, "ReleasePolicyEngine");
149
150                                 if (itemElements.getLength() > 1) {
151                                         log.warn("Encountered multiple <ReleasePolicyEngine> configuration elements.  Using first...");
152                                 }
153                                 if (itemElements.getLength() < 1) {
154                                         arpEngine = new ArpEngine();
155                                 } else {
156                                         arpEngine = new ArpEngine((Element) itemElements.item(0));
157                                 }
158
159                         } catch (ArpException ae) {
160                                 log.fatal("The Identity Provider could not be initialized "
161                                                 + "due to a problem with the ARP Engine configuration: " + ae);
162                                 throw new ShibbolethConfigurationException("Could not load ARP Engine.");
163                         } catch (AttributeResolverException ne) {
164                                 log.fatal("The Identity Provider could not be initialized due "
165                                                 + "to a problem with the Attribute Resolver configuration: " + ne);
166                                 throw new ShibbolethConfigurationException("Could not load Attribute Resolver.");
167                         }
168
169                         // Load protocol handlers and support library
170                         protocolSupport = new IdPProtocolSupport(configuration, transactionLog, nameMapper, spMapper, arpEngine,
171                                         resolver);
172                         log.debug("Starting with Shibboleth v1 protocol handling enabled.");
173                         try {
174                                 protocolHandlers.put(new URI("https://wraith.memphis.edu/shibboleth/HS"), new ShibbolethV1SSOHandler());
175                         } catch (URISyntaxException e1) {
176                                 // TODO get rid of this
177                         }
178
179                         // Load metadata
180                         itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(IdPConfig.originConfigNamespace,
181                                         "FederationProvider");
182                         for (int i = 0; i < itemElements.getLength(); i++) {
183                                 protocolSupport.addFederationProvider((Element) itemElements.item(i));
184                         }
185                         if (protocolSupport.providerCount() < 1) {
186                                 log.error("No Federation Provider metadata loaded.");
187                                 throw new ShibbolethConfigurationException("Could not load federation metadata.");
188                         }
189
190                         log.info("Identity Provider initialization complete.");
191
192                 } catch (ShibbolethConfigurationException ae) {
193                         log.fatal("The Identity Provider could not be initialized: " + ae);
194                         throw new UnavailableException("Identity Provider failed to initialize.");
195                 } catch (SAMLException se) {
196                         log.fatal("SAML SOAP binding could not be loaded: " + se);
197                         throw new UnavailableException("Identity Provider failed to initialize.");
198                 }
199         }
200
201         /*
202          * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
203          *      javax.servlet.http.HttpServletResponse)
204          */
205         public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
206
207                 MDC.put("serviceId", "[IdP] " + idgen.nextInt());
208                 MDC.put("remoteAddr", request.getRemoteAddr());
209                 log.debug("Recieved a request via GET.");
210
211                 try {
212                         // TODO this throttle should probably just wrap signing operations...
213                         throttle.enter();
214
215                         // Determine which protocol we are responding to (at this point, Shibv1 vs. EAuth)
216                         IdPProtocolHandler activeHandler = null;
217                         activeHandler = (IdPProtocolHandler) protocolHandlers.get(request.getRequestURI());
218
219                         if (activeHandler == null) { throw new InvalidClientDataException(
220                                         "The request did not contain sufficient parameter data to determine the protocol."); }
221
222                         // Pass request to the appropriate handler
223                         log.info("Processing " + activeHandler.getHandlerName() + " request.");
224                         activeHandler.processRequest(request, response, null, protocolSupport);
225
226                         // TODO hmmm... there is kind of an assupmtion here that we don't get a response... how to handle?
227                 } catch (InvalidClientDataException ex) {
228                         log.error(ex);
229                         displayBrowserError(request, response, ex);
230                         return;
231                 } catch (SAMLException ex) {
232                         log.error(ex);
233                         displayBrowserError(request, response, ex);
234                         return;
235                 } finally {
236                         throttle.exit();
237                 }
238         }
239
240         /*
241          * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
242          *      javax.servlet.http.HttpServletResponse)
243          */
244         public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
245
246                 MDC.put("serviceId", "[IdP] " + idgen.nextInt());
247                 MDC.put("remoteAddr", request.getRemoteAddr());
248                 log.debug("Recieved a request via POST.");
249
250                 // Parse SOAP request and marshall SAML request object
251                 SAMLRequest samlRequest = null;
252                 try {
253                         try {
254                                 samlRequest = binding.receive(request);
255                         } catch (SAMLException e) {
256                                 log.fatal("Unable to parse request: " + e);
257                                 throw new SAMLException("Invalid request data.");
258                         }
259
260                         // If we have DEBUG logging turned on, dump out the request to the log
261                         // This takes some processing, so only do it if we need to
262                         if (log.isDebugEnabled()) {
263                                 try {
264                                         log.debug("Dumping generated SAML Request:"
265                                                         + System.getProperty("line.separator")
266                                                         + new String(new BASE64Decoder().decodeBuffer(new String(samlRequest.toBase64(), "ASCII")),
267                                                                         "UTF8"));
268                                 } catch (SAMLException e) {
269                                         log.error("Encountered an error while decoding SAMLRequest for logging purposes.");
270                                 } catch (IOException e) {
271                                         log.error("Encountered an error while decoding SAMLRequest for logging purposes.");
272                                 }
273                         }
274
275                         // Determine which protocol handler is active for this endpoint
276                         IdPProtocolHandler activeHandler = null;
277                         activeHandler = (IdPProtocolHandler) protocolHandlers.get(request.getRequestURI());
278                         if (activeHandler == null) {
279                                 log.fatal("No protocol handler registered for endpoint (" + request.getRequestURI() + ").");
280                                 throw new SAMLException("Invalid request endpoint.");
281                         }
282
283                         // Pass request to the appropriate handler and respond
284                         log.info("Processing " + activeHandler.getHandlerName() + " request.");
285                         try {
286                                 SAMLResponse samlResponse = activeHandler.processRequest(request, response, samlRequest,
287                                                 protocolSupport);
288                                 binding.respond(response, samlResponse, null);
289
290                         } catch (InvalidClientDataException e1) {
291                                 // TODO throw SAML Exception here
292                                 e1.printStackTrace();
293                         }
294
295                 } catch (SAMLException e) {
296                         log.error("Error while processing request: " + e);
297                         try {
298                                 // TODO pass thru SAML Exception
299                                 sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
300                                                 "General error processing request."));
301                                 return;
302                         } catch (Exception ee) {
303                                 log.fatal("Could not construct a SAML error response: " + ee);
304                                 throw new ServletException("Identity Provider response failure.");
305                         }
306                 }
307
308         }
309
310         private void sendSAMLFailureResponse(HttpServletResponse httpResponse, SAMLRequest samlRequest,
311                         SAMLException exception) throws IOException {
312
313                 try {
314                         SAMLResponse samlResponse = new SAMLResponse((samlRequest != null) ? samlRequest.getId() : null, null,
315                                         null, exception);
316                         if (log.isDebugEnabled()) {
317                                 try {
318                                         log.debug("Dumping generated SAML Error Response:"
319                                                         + System.getProperty("line.separator")
320                                                         + new String(
321                                                                         new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
322                                                                         "UTF8"));
323                                 } catch (IOException e) {
324                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
325                                 }
326                         }
327                         binding.respond(httpResponse, samlResponse, null);
328                         log.debug("Returning SAML Error Response.");
329                 } catch (SAMLException se) {
330                         try {
331                                 binding.respond(httpResponse, null, exception);
332                         } catch (SAMLException e) {
333                                 log.error("Caught exception while responding to requester: " + e.getMessage());
334                                 httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while responding.");
335                         }
336                         log.error("Identity Provider failed to make an error message: " + se);
337                 }
338         }
339
340         private static void displayBrowserError(HttpServletRequest req, HttpServletResponse res, Exception e)
341                         throws ServletException, IOException {
342
343                 req.setAttribute("errorText", e.toString());
344                 req.setAttribute("requestURL", req.getRequestURI().toString());
345                 RequestDispatcher rd = req.getRequestDispatcher("/IdPError.jsp");
346                 rd.forward(req, res);
347         }
348
349         private class Semaphore {
350
351                 private int value;
352
353                 public Semaphore(int value) {
354
355                         this.value = value;
356                 }
357
358                 public synchronized void enter() {
359
360                         --value;
361                         if (value < 0) {
362                                 try {
363                                         wait();
364                                 } catch (InterruptedException e) {
365                                         // squelch and continue
366                                 }
367                         }
368                 }
369
370                 public synchronized void exit() {
371
372                         ++value;
373                         notify();
374                 }
375         }
376
377 }
378
379 class FederationProviderFactory {
380
381         private static Logger log = Logger.getLogger(FederationProviderFactory.class.getName());
382
383         public static Metadata loadProvider(Element e) throws MetadataException {
384
385                 String className = e.getAttribute("type");
386                 if (className == null || className.equals("")) {
387                         log.error("Federation Provider requires specification of the attribute \"type\".");
388                         throw new MetadataException("Failed to initialize Federation Provider.");
389                 } else {
390                         try {
391                                 Class[] params = {Class.forName("org.w3c.dom.Element"),};
392                                 return (Metadata) Class.forName(className).getConstructor(params).newInstance(new Object[]{e});
393                         } catch (Exception loaderException) {
394                                 log.error("Failed to load Federation Provider implementation class: " + loaderException);
395                                 Throwable cause = loaderException.getCause();
396                                 while (cause != null) {
397                                         log.error("caused by: " + cause);
398                                         cause = cause.getCause();
399                                 }
400                                 throw new MetadataException("Failed to initialize Federation Provider.");
401                         }
402                 }
403         }
404 }