Load IdP protocol handlers based on idp.xml configuration.
[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.util.HashMap;
31 import java.util.Random;
32
33 import javax.servlet.RequestDispatcher;
34 import javax.servlet.ServletException;
35 import javax.servlet.UnavailableException;
36 import javax.servlet.http.HttpServlet;
37 import javax.servlet.http.HttpServletRequest;
38 import javax.servlet.http.HttpServletResponse;
39
40 import org.apache.log4j.Logger;
41 import org.apache.log4j.MDC;
42 import org.opensaml.SAMLBinding;
43 import org.opensaml.SAMLBindingFactory;
44 import org.opensaml.SAMLException;
45 import org.opensaml.SAMLRequest;
46 import org.opensaml.SAMLResponse;
47 import org.w3c.dom.Document;
48 import org.w3c.dom.Element;
49 import org.w3c.dom.NodeList;
50
51 import sun.misc.BASE64Decoder;
52 import edu.internet2.middleware.shibboleth.aa.arp.ArpEngine;
53 import edu.internet2.middleware.shibboleth.aa.arp.ArpException;
54 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
55 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolverException;
56 import edu.internet2.middleware.shibboleth.common.Credentials;
57 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
58 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
59 import edu.internet2.middleware.shibboleth.common.NameMapper;
60 import edu.internet2.middleware.shibboleth.common.OriginConfig;
61 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapper;
62 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapperException;
63 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
64 import edu.internet2.middleware.shibboleth.metadata.Metadata;
65 import edu.internet2.middleware.shibboleth.metadata.MetadataException;
66
67 /**
68  * Primary entry point for requests to the SAML IdP. Listens on multiple endpoints, routes requests to the appropriate
69  * IdP processing components, and delivers proper protocol responses.
70  * 
71  * @author Walter Hoehn
72  */
73
74 public class IdPResponder extends HttpServlet {
75
76         private static Logger transactionLog = Logger.getLogger("Shibboleth-TRANSACTION");
77         private static Logger log = Logger.getLogger(IdPResponder.class.getName());
78         private static Random idgen = new Random();
79         private SAMLBinding binding;
80         private Semaphore throttle;
81         private IdPConfig configuration;
82         private HashMap protocolHandlers = new HashMap();
83         private IdPProtocolSupport protocolSupport;
84
85         /*
86          * @see javax.servlet.GenericServlet#init()
87          */
88         public void init() throws ServletException {
89
90                 super.init();
91                 MDC.put("serviceId", "[IdP] Core");
92                 log.info("Initializing Identity Provider.");
93
94                 try {
95                         binding = SAMLBindingFactory.getInstance(SAMLBinding.SOAP);
96
97                         Document originConfig = OriginConfig.getOriginConfig(this.getServletContext());
98
99                         // Load global configuration properties
100                         configuration = new IdPConfig(originConfig.getDocumentElement());
101
102                         // Load a semaphore that throttles how many requests the IdP will handle at once
103                         throttle = new Semaphore(configuration.getMaxThreads());
104
105                         // Load name mappings
106                         NameMapper nameMapper = new NameMapper();
107                         NodeList itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(
108                                         NameIdentifierMapping.mappingNamespace, "NameMapping");
109
110                         for (int i = 0; i < itemElements.getLength(); i++) {
111                                 try {
112                                         nameMapper.addNameMapping((Element) itemElements.item(i));
113                                 } catch (NameIdentifierMappingException e) {
114                                         log.error("Name Identifier mapping could not be loaded: " + e);
115                                 }
116                         }
117
118                         // Load signing credentials
119                         itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(Credentials.credentialsNamespace,
120                                         "Credentials");
121                         if (itemElements.getLength() < 1) {
122                                 log.error("No credentials specified.");
123                         }
124                         if (itemElements.getLength() > 1) {
125                                 log.error("Multiple Credentials specifications found, using first.");
126                         }
127                         Credentials credentials = new Credentials((Element) itemElements.item(0));
128
129                         // Load relying party config
130                         ServiceProviderMapper spMapper;
131                         try {
132                                 spMapper = new ServiceProviderMapper(originConfig.getDocumentElement(), configuration, credentials,
133                                                 nameMapper);
134                         } catch (ServiceProviderMapperException e) {
135                                 log.error("Could not load Identity Provider configuration: " + e);
136                                 throw new ShibbolethConfigurationException("Could not load Identity Provider configuration.");
137                         }
138
139                         // Startup Attribute Resolver & ARP engine
140                         AttributeResolver resolver = null;
141                         ArpEngine arpEngine = null;
142                         try {
143                                 resolver = new AttributeResolver(configuration);
144
145                                 itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(IdPConfig.configNameSpace,
146                                                 "ReleasePolicyEngine");
147
148                                 if (itemElements.getLength() > 1) {
149                                         log.warn("Encountered multiple <ReleasePolicyEngine> configuration elements.  Using first...");
150                                 }
151                                 if (itemElements.getLength() < 1) {
152                                         arpEngine = new ArpEngine();
153                                 } else {
154                                         arpEngine = new ArpEngine((Element) itemElements.item(0));
155                                 }
156
157                         } catch (ArpException ae) {
158                                 log.fatal("The Identity Provider could not be initialized "
159                                                 + "due to a problem with the ARP Engine configuration: " + ae);
160                                 throw new ShibbolethConfigurationException("Could not load ARP Engine.");
161                         } catch (AttributeResolverException ne) {
162                                 log.fatal("The Identity Provider could not be initialized due "
163                                                 + "to a problem with the Attribute Resolver configuration: " + ne);
164                                 throw new ShibbolethConfigurationException("Could not load Attribute Resolver.");
165                         }
166
167                         // Load protocol handlers and support library
168                         protocolSupport = new IdPProtocolSupport(configuration, transactionLog, nameMapper, spMapper, arpEngine,
169                                         resolver);
170                         itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(IdPConfig.configNameSpace,
171                                         "ProtocolHandler");
172
173                         // Default if no handlers are specified
174                         if (itemElements.getLength() < 1) {
175                                 // TODO work out defaulting
176
177                                 // If handlers were specified, load them and register them against their locations
178                         } else {
179                                 EACHHANDLER : for (int i = 0; i < itemElements.getLength(); i++) {
180                                         IdPProtocolHandler handler = ProtocolHandlerFactory.getInstance((Element) itemElements.item(i));
181                                         URI[] locations = handler.getLocations();
182                                         EACHLOCATION : for (int j = 0; j < locations.length; j++) {
183                                                 if (protocolHandlers.containsKey(locations[j].toString())) {
184                                                         log.error("Multiple protocol handlers are registered to listen at ("
185                                                                         + locations[j]
186                                                                         + ").  Ignoring all except ("
187                                                                         + ((IdPProtocolHandler) protocolHandlers.get(locations[j].toString()))
188                                                                                         .getHandlerName() + ").");
189                                                         continue EACHLOCATION;
190                                                 }
191                                                 log.info("Registering handler (" + handler.getHandlerName() + ") to listen at (" + locations[j]
192                                                                 + ").");
193                                                 protocolHandlers.put(locations[j].toString(), handler);
194                                         }
195                                 }
196                         }
197
198                         // Load metadata
199                         itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(IdPConfig.configNameSpace,
200                                         "FederationProvider");
201                         for (int i = 0; i < itemElements.getLength(); i++) {
202                                 protocolSupport.addFederationProvider((Element) itemElements.item(i));
203                         }
204                         if (protocolSupport.providerCount() < 1) {
205                                 log.error("No Federation Provider metadata loaded.");
206                                 throw new ShibbolethConfigurationException("Could not load federation metadata.");
207                         }
208
209                         log.info("Identity Provider initialization complete.");
210
211                 } catch (ShibbolethConfigurationException ae) {
212                         log.fatal("The Identity Provider could not be initialized: " + ae);
213                         throw new UnavailableException("Identity Provider failed to initialize.");
214                 } catch (SAMLException se) {
215                         log.fatal("SAML SOAP binding could not be loaded: " + se);
216                         throw new UnavailableException("Identity Provider failed to initialize.");
217                 }
218         }
219
220         /*
221          * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest,
222          *      javax.servlet.http.HttpServletResponse)
223          */
224         public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
225
226                 MDC.put("serviceId", "[IdP] " + idgen.nextInt());
227                 MDC.put("remoteAddr", request.getRemoteAddr());
228                 log.debug("Recieved a request via GET for location (" + request.getRequestURL() + ").");
229
230                 try {
231                         // TODO this throttle should probably just wrap signing operations...
232                         throttle.enter();
233
234                         // Determine which protocol we are responding to (at this point normally Shibv1 vs. EAuth)
235                         IdPProtocolHandler activeHandler = (IdPProtocolHandler) protocolHandlers.get(request.getRequestURL()
236                                         .toString());
237                         if (activeHandler == null) {
238                                 log.error("No protocol handler registered for location (" + request.getRequestURL() + ").");
239                                 throw new SAMLException("Request submitted to an invalid location.");
240                         }
241
242                         // Pass request to the appropriate handler
243                         log.info("Processing " + activeHandler.getHandlerName() + " request.");
244                         if (activeHandler.processRequest(request, response, null, protocolSupport) != null) {
245                                 // This shouldn't happen unless somebody configures a protocol handler incorrectly
246                                 log.error("Protocol Handler returned a SAML Response, but there is no binding to handle it.");
247                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
248                         }
249
250                 } catch (SAMLException ex) {
251                         log.error(ex);
252                         displayBrowserError(request, response, ex);
253                         return;
254                 } finally {
255                         throttle.exit();
256                 }
257         }
258
259         /*
260          * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest,
261          *      javax.servlet.http.HttpServletResponse)
262          */
263         public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
264
265                 MDC.put("serviceId", "[IdP] " + idgen.nextInt());
266                 MDC.put("remoteAddr", request.getRemoteAddr());
267                 log.debug("Recieved a request via POST for location (" + request.getRequestURL() + ").");
268
269                 // Parse SOAP request and marshall SAML request object
270                 SAMLRequest samlRequest = null;
271                 try {
272                         try {
273                                 samlRequest = binding.receive(request);
274                         } catch (SAMLException e) {
275                                 log.fatal("Unable to parse request: " + e);
276                                 throw new SAMLException("Invalid request data.");
277                         }
278
279                         // If we have DEBUG logging turned on, dump out the request to the log
280                         // This takes some processing, so only do it if we need to
281                         if (log.isDebugEnabled()) {
282                                 try {
283                                         log.debug("Dumping generated SAML Request:"
284                                                         + System.getProperty("line.separator")
285                                                         + new String(new BASE64Decoder().decodeBuffer(new String(samlRequest.toBase64(), "ASCII")),
286                                                                         "UTF8"));
287                                 } catch (SAMLException e) {
288                                         log.error("Encountered an error while decoding SAMLRequest for logging purposes.");
289                                 } catch (IOException e) {
290                                         log.error("Encountered an error while decoding SAMLRequest for logging purposes.");
291                                 }
292                         }
293
294                         // Determine which protocol handler is active for this endpoint
295                         IdPProtocolHandler activeHandler = (IdPProtocolHandler) protocolHandlers.get(request.getRequestURL()
296                                         .toString());
297                         if (activeHandler == null) {
298                                 log.error("No protocol handler registered for location (" + request.getRequestURL() + ").");
299                                 throw new SAMLException("Request submitted to an invalid location.");
300                         }
301
302                         // Pass request to the appropriate handler and respond
303                         log.info("Processing " + activeHandler.getHandlerName() + " request.");
304
305                         SAMLResponse samlResponse = activeHandler.processRequest(request, response, samlRequest, protocolSupport);
306                         binding.respond(response, samlResponse, null);
307
308                 } catch (SAMLException e) {
309                         sendFailureToSAMLBinding(response, samlRequest, e);
310                 }
311         }
312
313         private void sendFailureToSAMLBinding(HttpServletResponse httpResponse, SAMLRequest samlRequest,
314                         SAMLException exception) throws ServletException {
315
316                 log.error("Error while processing request: " + exception);
317                 try {
318                         SAMLResponse samlResponse = new SAMLResponse((samlRequest != null) ? samlRequest.getId() : null, null,
319                                         null, exception);
320                         if (log.isDebugEnabled()) {
321                                 try {
322                                         log.debug("Dumping generated SAML Error Response:"
323                                                         + System.getProperty("line.separator")
324                                                         + new String(
325                                                                         new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
326                                                                         "UTF8"));
327                                 } catch (IOException e) {
328                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
329                                 }
330                         }
331                         binding.respond(httpResponse, samlResponse, null);
332                         log.debug("Returning SAML Error Response.");
333                 } catch (SAMLException se) {
334                         try {
335                                 binding.respond(httpResponse, null, exception);
336                         } catch (SAMLException e) {
337                                 log.error("Caught exception while responding to requester: " + e.getMessage());
338                                 try {
339                                         httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while responding.");
340                                 } catch (IOException ee) {
341                                         log.fatal("Could not construct a SAML error response: " + ee);
342                                         throw new ServletException("Identity Provider response failure.");
343                                 }
344                         }
345                         log.error("Identity Provider failed to make an error message: " + se);
346                 }
347         }
348
349         private static void displayBrowserError(HttpServletRequest req, HttpServletResponse res, Exception e)
350                         throws ServletException, IOException {
351
352                 req.setAttribute("errorText", e.toString());
353                 req.setAttribute("requestURL", req.getRequestURI().toString());
354                 RequestDispatcher rd = req.getRequestDispatcher("/IdPError.jsp");
355                 rd.forward(req, res);
356         }
357
358         private class Semaphore {
359
360                 private int value;
361
362                 public Semaphore(int value) {
363
364                         this.value = value;
365                 }
366
367                 public synchronized void enter() {
368
369                         --value;
370                         if (value < 0) {
371                                 try {
372                                         wait();
373                                 } catch (InterruptedException e) {
374                                         // squelch and continue
375                                 }
376                         }
377                 }
378
379                 public synchronized void exit() {
380
381                         ++value;
382                         notify();
383                 }
384         }
385
386 }
387
388 class FederationProviderFactory {
389
390         private static Logger log = Logger.getLogger(FederationProviderFactory.class.getName());
391
392         public static Metadata loadProvider(Element e) throws MetadataException {
393
394                 String className = e.getAttribute("type");
395                 if (className == null || className.equals("")) {
396                         log.error("Federation Provider requires specification of the attribute \"type\".");
397                         throw new MetadataException("Failed to initialize Federation Provider.");
398                 } else {
399                         try {
400                                 Class[] params = {Class.forName("org.w3c.dom.Element"),};
401                                 return (Metadata) Class.forName(className).getConstructor(params).newInstance(new Object[]{e});
402                         } catch (Exception loaderException) {
403                                 log.error("Failed to load Federation Provider implementation class: " + loaderException);
404                                 Throwable cause = loaderException.getCause();
405                                 while (cause != null) {
406                                         log.error("caused by: " + cause);
407                                         cause = cause.getCause();
408                                 }
409                                 throw new MetadataException("Failed to initialize Federation Provider.");
410                         }
411                 }
412         }
413 }