A little more work on the attribute query portion of the unified IdP responder. ...
[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.security.Principal;
32 import java.security.cert.CertificateParsingException;
33 import java.security.cert.X509Certificate;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.Date;
39 import java.util.Iterator;
40 import java.util.List;
41
42 import javax.security.auth.x500.X500Principal;
43 import javax.servlet.ServletException;
44 import javax.servlet.UnavailableException;
45 import javax.servlet.http.HttpServletRequest;
46 import javax.servlet.http.HttpServletResponse;
47
48 import org.apache.log4j.Logger;
49 import org.apache.log4j.MDC;
50 import org.apache.xml.security.exceptions.XMLSecurityException;
51 import org.apache.xml.security.keys.KeyInfo;
52 import org.apache.xml.security.signature.XMLSignature;
53 import org.opensaml.InvalidCryptoException;
54 import org.opensaml.SAMLAssertion;
55 import org.opensaml.SAMLAttribute;
56 import org.opensaml.SAMLAttributeDesignator;
57 import org.opensaml.SAMLAttributeQuery;
58 import org.opensaml.SAMLAttributeStatement;
59 import org.opensaml.SAMLAudienceRestrictionCondition;
60 import org.opensaml.SAMLBinding;
61 import org.opensaml.SAMLCondition;
62 import org.opensaml.SAMLException;
63 import org.opensaml.SAMLIdentifier;
64 import org.opensaml.SAMLRequest;
65 import org.opensaml.SAMLResponse;
66 import org.opensaml.SAMLStatement;
67 import org.opensaml.SAMLSubject;
68 import org.w3c.dom.Document;
69 import org.w3c.dom.Element;
70 import org.w3c.dom.NodeList;
71
72 import sun.misc.BASE64Decoder;
73 import edu.internet2.middleware.shibboleth.aa.AAConfig;
74 import edu.internet2.middleware.shibboleth.aa.AAException;
75 import edu.internet2.middleware.shibboleth.aa.AARelyingParty;
76 import edu.internet2.middleware.shibboleth.aa.AAResponder;
77 import edu.internet2.middleware.shibboleth.aa.AAServiceProviderMapper;
78 import edu.internet2.middleware.shibboleth.aa.arp.ArpEngine;
79 import edu.internet2.middleware.shibboleth.aa.arp.ArpException;
80 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolver;
81 import edu.internet2.middleware.shibboleth.aa.attrresolv.AttributeResolverException;
82 import edu.internet2.middleware.shibboleth.common.Credential;
83 import edu.internet2.middleware.shibboleth.common.Credentials;
84 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
85 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
86 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
87 import edu.internet2.middleware.shibboleth.common.NameMapper;
88 import edu.internet2.middleware.shibboleth.common.OriginConfig;
89 import edu.internet2.middleware.shibboleth.common.RelyingParty;
90 import edu.internet2.middleware.shibboleth.common.SAMLBindingFactory;
91 import edu.internet2.middleware.shibboleth.common.ServiceProvider;
92 import edu.internet2.middleware.shibboleth.common.ServiceProviderMapperException;
93 import edu.internet2.middleware.shibboleth.common.ShibPOSTProfile;
94 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
95 import edu.internet2.middleware.shibboleth.common.ShibbolethOriginConfig;
96 import edu.internet2.middleware.shibboleth.common.TargetFederationComponent;
97 import edu.internet2.middleware.shibboleth.hs.HSRelyingParty;
98 import edu.internet2.middleware.shibboleth.metadata.AttributeConsumerRole;
99 import edu.internet2.middleware.shibboleth.metadata.KeyDescriptor;
100 import edu.internet2.middleware.shibboleth.metadata.Provider;
101 import edu.internet2.middleware.shibboleth.metadata.ProviderRole;
102
103 /**
104  * Primary entry point for requests to the SAML IdP. Listens on multiple endpoints, routes requests to the appropriate
105  * IdP processing components, and delivers proper protocol responses.
106  * 
107  * @author Walter Hoehn
108  */
109
110 public class IdPResponder extends TargetFederationComponent {
111
112         //TODO Maybe should rethink the inheritance here, since there is only one
113         // servlet
114
115         private static Logger                   transactionLog  = Logger.getLogger("Shibboleth-TRANSACTION");
116
117         private static Logger                   log                             = Logger.getLogger(IdPResponder.class.getName());
118
119         private SAMLBinding                             binding;
120
121         //TODO Need to init
122         private ArtifactRepository              artifactRepository;
123
124         //TODO Obviously this has got to be unified
125         private AAConfig                                configuration;
126
127         //TODO Need to init
128         private NameMapper                              nameMapper;
129
130         //TODO Need to init
131         private AAServiceProviderMapper targetMapper;
132
133         //TODO Need to rename, rework, and init
134         private AAResponder                             responder;
135
136         public void init() throws ServletException {
137
138                 super.init();
139                 MDC.put("serviceId", "[IdP] Core");
140                 log.info("Initializing Identity Provider.");
141
142                 try {
143                         binding = SAMLBindingFactory.getInstance(SAMLBinding.SAML_SOAP_HTTPS);
144                         nameMapper = new NameMapper();
145                         loadConfiguration();
146                         log.info("Identity Provider initialization complete.");
147
148                 } catch (ShibbolethConfigurationException ae) {
149                         log.fatal("The Identity Provider could not be initialized: " + ae);
150                         throw new UnavailableException("Identity Provider failed to initialize.");
151                 } catch (SAMLException se) {
152                         log.fatal("SAML SOAP binding could not be loaded: " + se);
153                         throw new UnavailableException("Identity Provider failed to initialize.");
154                 }
155         }
156
157         private void loadConfiguration() throws ShibbolethConfigurationException {
158                 Document originConfig = OriginConfig.getOriginConfig(this.getServletContext());
159
160                 //TODO I think some of the failure cases here are different than in the
161                 // HS, so when the loadConfiguration() is unified, that must be taken
162                 // into account
163
164                 //TODO do we need to check active endpoints to determine which
165                 // components to load, for instance artifact repository, arp engine,
166                 // attribute resolver
167
168                 //Load global configuration properties
169                 //TODO make AA and HS config unified
170                 configuration = new AAConfig(originConfig.getDocumentElement());
171
172                 //Load name mappings
173                 NodeList itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(
174                                 NameIdentifierMapping.mappingNamespace, "NameMapping");
175
176                 for (int i = 0; i < itemElements.getLength(); i++) {
177                         try {
178                                 nameMapper.addNameMapping((Element) itemElements.item(i));
179                         } catch (NameIdentifierMappingException e) {
180                                 log.error("Name Identifier mapping could not be loaded: " + e);
181                         }
182                 }
183
184                 //Load signing credentials
185                 itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(Credentials.credentialsNamespace,
186                                 "Credentials");
187                 if (itemElements.getLength() < 1) {
188                         log.error("No credentials specified.");
189                 }
190                 if (itemElements.getLength() > 1) {
191                         log.error("Multiple Credentials specifications found, using first.");
192                 }
193                 Credentials credentials = new Credentials((Element) itemElements.item(0));
194
195                 //Load metadata
196                 itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(
197                                 ShibbolethOriginConfig.originConfigNamespace, "FederationProvider");
198                 for (int i = 0; i < itemElements.getLength(); i++) {
199                         addFederationProvider((Element) itemElements.item(i));
200                 }
201                 if (providerCount() < 1) {
202                         log.error("No Federation Provider metadata loaded.");
203                         throw new ShibbolethConfigurationException("Could not load federation metadata.");
204                 }
205
206                 //Load relying party config
207                 try {
208                         //TODO unify the service provider mapper
209                         targetMapper = new AAServiceProviderMapper(originConfig.getDocumentElement(), configuration, credentials,
210                                         this);
211                 } catch (ServiceProviderMapperException e) {
212                         log.error("Could not load Identity Provider configuration: " + e);
213                         throw new ShibbolethConfigurationException("Could not load Identity Provider configuration.");
214                 }
215
216                 try {
217                         //Startup Attribute Resolver
218                         AttributeResolver resolver = new AttributeResolver(configuration);
219
220                         //Startup ARP Engine
221                         ArpEngine arpEngine = null;
222                         itemElements = originConfig.getDocumentElement().getElementsByTagNameNS(
223                                         ShibbolethOriginConfig.originConfigNamespace, "ReleasePolicyEngine");
224
225                         if (itemElements.getLength() > 1) {
226                                 log.warn("Encountered multiple <ReleasePolicyEngine> configuration elements.  Using first...");
227                         }
228                         if (itemElements.getLength() < 1) {
229                                 arpEngine = new ArpEngine();
230                         } else {
231                                 arpEngine = new ArpEngine((Element) itemElements.item(0));
232                         }
233
234                         //Startup responder
235                         responder = new AAResponder(arpEngine, resolver);
236
237                 } catch (ArpException ae) {
238                         log.fatal("The Identity Provider could not be initialized "
239                                         + "due to a problem with the ARP Engine configuration: " + ae);
240                         throw new ShibbolethConfigurationException("Could not load ARP Engine.");
241                 } catch (AttributeResolverException ne) {
242                         log.fatal("The Identity Provider could not be initialized due "
243                                         + "to a problem with the Attribute Resolver configuration: " + ne);
244                         throw new ShibbolethConfigurationException("Could not load Attribute Resolver.");
245                 }
246
247         }
248
249         public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
250
251                 MDC.put("serviceId", "[IdP] " + new SAMLIdentifier().toString());
252                 MDC.put("remoteAddr", request.getRemoteAddr());
253                 log.debug("Recieved a request via POST.");
254
255                 // Parse SOAP request and marshall SAML request object
256                 SAMLRequest samlRequest = null;
257                 try {
258                         try {
259                                 samlRequest = binding.receive(request);
260                         } catch (SAMLException e) {
261                                 log.fatal("Unable to parse request: " + e);
262                                 throw new SAMLException("Invalid request data.");
263                         }
264
265                         // Determine the request type
266                         Iterator artifacts = samlRequest.getArtifacts();
267                         if (artifacts.hasNext()) {
268                                 artifacts = null; // get rid of the iterator
269                                 log.info("Recieved a request to dereference an assertion artifact.");
270                                 processArtifactDereference(samlRequest, request, response);
271                                 return;
272                         }
273
274                         if (samlRequest.getQuery() != null && (samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
275                                 log.info("Recieved an attribute query.");
276                                 processAttributeQuery(samlRequest, request, response);
277                                 return;
278                         }
279
280                         throw new SAMLException(SAMLException.REQUESTER,
281                                         "Identity Provider unable to respond to this SAML Request type.");
282
283                 } catch (InvalidNameIdentifierException invalidNameE) {
284                         log.info("Could not associate the request subject with a principal: " + invalidNameE);
285                         try {
286                                 //TODO once again, ifgure out passThruErrors
287                                 if (false) {
288                                         //if (relyingParty.passThruErrors()) {
289                                         sendSAMLFailureResponse(response, samlRequest, new SAMLException(Arrays.asList(invalidNameE
290                                                         .getSAMLErrorCodes()), "The supplied Subject was unrecognized.", invalidNameE));
291
292                                 } else {
293                                         sendSAMLFailureResponse(response, samlRequest, new SAMLException(Arrays.asList(invalidNameE
294                                                         .getSAMLErrorCodes()), "The supplied Subject was unrecognized."));
295                                 }
296                                 return;
297                         } catch (Exception ee) {
298                                 log.fatal("Could not construct a SAML error response: " + ee);
299                                 throw new ServletException("Identity Provider response failure.");
300                         }
301                 } catch (Exception e) {
302                         log.error("Error while processing request: " + e);
303                         try {
304                                 //TODO figure out how to implement the passThru error handling
305                                 // below
306                                 //if (relyingParty != null && relyingParty.passThruErrors()) {
307                                 if (false) {
308                                         sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
309                                                         "General error processing request.", e));
310                                 } else if (configuration.passThruErrors()) {
311                                         sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
312                                                         "General error processing request.", e));
313                                 } else {
314                                         sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
315                                                         "General error processing request."));
316                                 }
317                                 return;
318                         } catch (Exception ee) {
319                                 log.fatal("Could not construct a SAML error response: " + ee);
320                                 throw new ServletException("Identity Provider response failure.");
321                         }
322                 }
323         }
324
325         //TODO get rid of this AAException thing
326         private void processAttributeQuery(SAMLRequest samlRequest, HttpServletRequest request, HttpServletResponse response)
327                         throws SAMLException, IOException, ServletException, AAException, InvalidNameIdentifierException,
328                         NameIdentifierMappingException {
329                 //TODO validate that the endpoint is valid for the request type
330
331                 AARelyingParty relyingParty = null;
332
333                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
334
335                 if (!fromLegacyProvider(request)) {
336                         log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
337                 }
338
339                 //This is the requester name that will be passed to subsystems
340                 String effectiveName = null;
341
342                 X509Certificate credential = getCredentialFromProvider(request);
343                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
344                         log.info("Request is from an unauthenticated service provider.");
345                 } else {
346
347                         //Identify a Relying Party
348                         relyingParty = targetMapper.getRelyingParty(attributeQuery.getResource());
349
350                         try {
351                                 effectiveName = getEffectiveName(request, relyingParty);
352                         } catch (InvalidProviderCredentialException ipc) {
353                                 sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
354                                                 "Invalid credentials for request."));
355                                 return;
356                         }
357                 }
358
359                 if (effectiveName == null) {
360                         log.debug("Using default Relying Party for unauthenticated provider.");
361                         relyingParty = targetMapper.getRelyingParty(null);
362                 }
363
364                 //Fail if we can't honor SAML Subject Confirmation
365                 if (!fromLegacyProvider(request)) {
366                         Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
367                         boolean hasConfirmationMethod = false;
368                         while (iterator.hasNext()) {
369                                 log.info("Request contains SAML Subject Confirmation method: (" + (String) iterator.next() + ").");
370                         }
371                         if (hasConfirmationMethod) { throw new SAMLException(SAMLException.REQUESTER,
372                                         "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method."); }
373                 }
374
375                 //Map Subject to local principal
376                 Principal principal = nameMapper.getPrincipal(attributeQuery.getSubject().getName(), relyingParty, relyingParty
377                                 .getIdentityProvider());
378                 log.info("Request is for principal (" + principal.getName() + ").");
379                 //TODO probably need to move this error handing out
380
381                 SAMLAttribute[] attrs;
382                 Iterator requestedAttrsIterator = attributeQuery.getDesignators();
383                 if (requestedAttrsIterator.hasNext()) {
384                         log.info("Request designates specific attributes, resolving this set.");
385                         ArrayList requestedAttrs = new ArrayList();
386                         while (requestedAttrsIterator.hasNext()) {
387                                 SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
388                                 try {
389                                         log.debug("Designated attribute: (" + attribute.getName() + ")");
390                                         requestedAttrs.add(new URI(attribute.getName()));
391                                 } catch (URISyntaxException use) {
392                                         log.error("Request designated an attribute name that does not conform to the required URI syntax ("
393                                                         + attribute.getName() + ").  Ignoring this attribute");
394                                 }
395                         }
396
397                         attrs = responder.getReleaseAttributes(principal, effectiveName, null, (URI[]) requestedAttrs
398                                         .toArray(new URI[0]));
399                 } else {
400                         log.info("Request does not designate specific attributes, resolving all available.");
401                         attrs = responder.getReleaseAttributes(principal, effectiveName, null);
402                 }
403
404                 log.info("Found " + attrs.length + " attribute(s) for " + principal.getName());
405                 sendSAMLResponse(response, attrs, samlRequest, relyingParty, null);
406                 log.info("Successfully responded about " + principal.getName());
407
408                 if (effectiveName == null) {
409                         if (fromLegacyProvider(request)) {
410                                 transactionLog.info("Attribute assertion issued to anonymous legacy provider at ("
411                                                 + request.getRemoteAddr() + ") on behalf of principal (" + principal.getName() + ").");
412                         } else {
413                                 transactionLog.info("Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
414                                                 + ") on behalf of principal (" + principal.getName() + ").");
415                         }
416                 } else {
417                         if (fromLegacyProvider(request)) {
418                                 transactionLog.info("Attribute assertion issued to legacy provider (" + effectiveName
419                                                 + ") on behalf of principal (" + principal.getName() + ").");
420                         } else {
421                                 transactionLog.info("Attribute assertion issued to provider (" + effectiveName
422                                                 + ") on behalf of principal (" + principal.getName() + ").");
423                         }
424                 }
425
426         }
427
428         private void processArtifactDereference(SAMLRequest samlRequest, HttpServletRequest request,
429                         HttpServletResponse response) throws SAMLException, IOException {
430                 //TODO validate that the endpoint is valid for the request type
431                 //TODO how about signatures on artifact dereferencing
432
433                 // Pull credential from request
434                 X509Certificate credential = getCredentialFromProvider(request);
435                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
436                         log.info("Request is from an unauthenticated service provider.");
437                         throw new SAMLException(SAMLException.REQUESTER,
438                                         "SAML Artifacts cannot be dereferenced for unauthenticated requesters.");
439                 }
440
441                 log.info("Request contains credential: (" + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
442                                 + ").");
443
444                 ArrayList assertions = new ArrayList();
445                 Iterator artifacts = samlRequest.getArtifacts();
446
447                 int queriedArtifacts = 0;
448                 StringBuffer dereferencedArtifacts = new StringBuffer(); //for
449                 // transaction
450                 // log
451                 while (artifacts.hasNext()) {
452                         queriedArtifacts++;
453                         String artifact = (String) artifacts.next();
454                         log.debug("Attempting to dereference artifact: (" + artifact + ").");
455                         ArtifactMapping mapping = artifactRepository.recoverAssertion(artifact);
456                         if (mapping != null) {
457                                 SAMLAssertion assertion = mapping.getAssertion();
458
459                                 //See if we have metadata for this provider
460                                 Provider provider = lookup(mapping.getServiceProviderId());
461                                 if (provider == null) {
462                                         log.info("No metadata found for provider: (" + mapping.getServiceProviderId() + ").");
463                                         throw new SAMLException(SAMLException.REQUESTER, "Invalid service provider.");
464                                 }
465
466                                 //Make sure that the suppplied credential is valid for the
467                                 // provider to which the artifact was issued
468                                 if (!isValidCredential(provider, credential)) {
469                                         log.error("Supplied credential ("
470                                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
471                                                         + ") is NOT valid for provider (" + mapping.getServiceProviderId()
472                                                         + "), to whom this artifact was issued.");
473                                         throw new SAMLException(SAMLException.REQUESTER, "Invalid credential.");
474                                 }
475
476                                 log.debug("Supplied credential validated for the provider to which this artifact was issued.");
477
478                                 assertions.add(assertion);
479                                 dereferencedArtifacts.append("(" + artifact + ")");
480                         }
481                 }
482
483                 //The spec requires that if any artifacts are dereferenced, they must
484                 // all be dereferenced
485                 if (assertions.size() > 0 & assertions.size() != queriedArtifacts) { throw new SAMLException(
486                                 SAMLException.REQUESTER, "Unable to successfully dereference all artifacts."); }
487
488                 //Create and send response
489                 SAMLResponse samlResponse = new SAMLResponse(samlRequest.getId(), null, assertions, null);
490
491                 if (log.isDebugEnabled()) {
492                         try {
493                                 log.debug("Dumping generated SAML Response:"
494                                                 + System.getProperty("line.separator")
495                                                 + new String(new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
496                                                                 "UTF8"));
497                         } catch (SAMLException e) {
498                                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
499                         } catch (IOException e) {
500                                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
501                         }
502                 }
503
504                 binding.respond(response, samlResponse, null);
505
506                 transactionLog.info("Succesfully dereferenced the following artifacts: " + dereferencedArtifacts.toString());
507                 //TODO make sure we can delete this junk below
508                 /*
509                  * } catch (Exception e) { log.error("Error while processing request: " + e); try { sendFailure(res,
510                  * samlRequest, new SAMLException(SAMLException.RESPONDER, "General error processing request.")); return; }
511                  * catch (Exception ee) { log.fatal("Could not construct a SAML error response: " + ee); throw new
512                  * ServletException("Handle Service response failure."); } }
513                  */
514         }
515
516         public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
517
518                 MDC.put("serviceId", "[IdP] " + new SAMLIdentifier().toString());
519                 MDC.put("remoteAddr", request.getRemoteAddr());
520                 log.debug("Recieved a request via GET.");
521         }
522
523         private static X509Certificate getCredentialFromProvider(HttpServletRequest req) {
524                 X509Certificate[] certArray = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
525                 if (certArray != null && certArray.length > 0) { return certArray[0]; }
526                 return null;
527         }
528
529         private static boolean isValidCredential(Provider provider, X509Certificate certificate) {
530
531                 ProviderRole[] roles = provider.getRoles();
532                 if (roles.length == 0) {
533                         log.info("Inappropriate metadata for provider.");
534                         return false;
535                 }
536                 //TODO figure out what to do about this role business here
537                 for (int i = 0; roles.length > i; i++) {
538                         if (roles[i] instanceof AttributeConsumerRole) {
539                                 KeyDescriptor[] descriptors = roles[i].getKeyDescriptors();
540                                 for (int j = 0; descriptors.length > j; j++) {
541                                         KeyInfo[] keyInfo = descriptors[j].getKeyInfo();
542                                         for (int k = 0; keyInfo.length > k; k++) {
543                                                 for (int l = 0; keyInfo[k].lengthKeyName() > l; l++) {
544                                                         try {
545
546                                                                 //First, try to match DN against metadata
547                                                                 try {
548                                                                         if (certificate.getSubjectX500Principal().getName(X500Principal.RFC2253).equals(
549                                                                                         new X500Principal(keyInfo[k].itemKeyName(l).getKeyName())
550                                                                                                         .getName(X500Principal.RFC2253))) {
551                                                                                 log.debug("Matched against DN.");
552                                                                                 return true;
553                                                                         }
554                                                                 } catch (IllegalArgumentException iae) {
555                                                                         //squelch this runtime exception, since
556                                                                         // this might be a valid case
557                                                                 }
558
559                                                                 //If that doesn't work, we try matching against
560                                                                 // some Subject Alt Names
561                                                                 try {
562                                                                         Collection altNames = certificate.getSubjectAlternativeNames();
563                                                                         if (altNames != null) {
564                                                                                 for (Iterator nameIterator = altNames.iterator(); nameIterator.hasNext();) {
565                                                                                         List altName = (List) nameIterator.next();
566                                                                                         if (altName.get(0).equals(new Integer(2))
567                                                                                                         || altName.get(0).equals(new Integer(6))) { //2 is
568                                                                                                 // DNS,
569                                                                                                 // 6 is
570                                                                                                 // URI
571                                                                                                 if (altName.get(1).equals(keyInfo[k].itemKeyName(l).getKeyName())) {
572                                                                                                         log.debug("Matched against SubjectAltName.");
573                                                                                                         return true;
574                                                                                                 }
575                                                                                         }
576                                                                                 }
577                                                                         }
578                                                                 } catch (CertificateParsingException e1) {
579                                                                         log
580                                                                                         .error("Encountered an problem trying to extract Subject Alternate Name from supplied certificate: "
581                                                                                                         + e1);
582                                                                 }
583
584                                                                 //If that doesn't work, try to match using
585                                                                 // SSL-style hostname matching
586                                                                 if (ShibPOSTProfile.getHostNameFromDN(certificate.getSubjectX500Principal()).equals(
587                                                                                 keyInfo[k].itemKeyName(l).getKeyName())) {
588                                                                         log.debug("Matched against hostname.");
589                                                                         return true;
590                                                                 }
591
592                                                         } catch (XMLSecurityException e) {
593                                                                 log.error("Encountered an error reading federation metadata: " + e);
594                                                         }
595                                                 }
596                                         }
597                                 }
598                         }
599                 }
600                 log.info("Supplied credential not found in metadata.");
601                 return false;
602         }
603
604         private void sendSAMLFailureResponse(HttpServletResponse httpResponse, SAMLRequest samlRequest,
605                         SAMLException exception) throws IOException {
606                 try {
607                         SAMLResponse samlResponse = new SAMLResponse((samlRequest != null) ? samlRequest.getId() : null, null,
608                                         null, exception);
609                         if (log.isDebugEnabled()) {
610                                 try {
611                                         log.debug("Dumping generated SAML Error Response:"
612                                                         + System.getProperty("line.separator")
613                                                         + new String(
614                                                                         new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
615                                                                         "UTF8"));
616                                 } catch (IOException e) {
617                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
618                                 }
619                         }
620                         binding.respond(httpResponse, samlResponse, null);
621                         log.debug("Returning SAML Error Response.");
622                 } catch (SAMLException se) {
623                         binding.respond(httpResponse, null, exception);
624                         log.error("Identity Provider failed to make an error message: " + se);
625                 }
626         }
627
628         private static boolean fromLegacyProvider(HttpServletRequest request) {
629                 String version = request.getHeader("Shibboleth");
630                 if (version != null) {
631                         log.debug("Request from Shibboleth version: " + version);
632                         return false;
633                 }
634                 log.debug("No version header found.");
635                 return true;
636         }
637
638         private String getEffectiveName(HttpServletRequest req, AARelyingParty relyingParty)
639                         throws InvalidProviderCredentialException {
640
641                 //X500Principal credentialName = getCredentialName(req);
642                 X509Certificate credential = getCredentialFromProvider(req);
643
644                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
645                         log.info("Request is from an unauthenticated service provider.");
646                         return null;
647
648                 } else {
649                         log.info("Request contains credential: ("
650                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
651                         //Mockup old requester name for requests from < 1.2 targets
652                         if (fromLegacyProvider(req)) {
653                                 String legacyName = ShibPOSTProfile.getHostNameFromDN(credential.getSubjectX500Principal());
654                                 if (legacyName == null) {
655                                         log.error("Unable to extract legacy requester name from certificate subject.");
656                                 }
657
658                                 log.info("Request from legacy service provider: (" + legacyName + ").");
659                                 return legacyName;
660
661                         } else {
662
663                                 //See if we have metadata for this provider
664                                 Provider provider = lookup(relyingParty.getProviderId());
665                                 if (provider == null) {
666                                         log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
667                                         log.info("Treating remote provider as unauthenticated.");
668                                         return null;
669                                 }
670
671                                 //Make sure that the suppplied credential is valid for the
672                                 // selected relying party
673                                 if (isValidCredential(provider, credential)) {
674                                         log.info("Supplied credential validated for this provider.");
675                                         log.info("Request from service provider: (" + relyingParty.getProviderId() + ").");
676                                         return relyingParty.getProviderId();
677                                 } else {
678                                         log.error("Supplied credential ("
679                                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
680                                                         + ") is NOT valid for provider (" + relyingParty.getProviderId() + ").");
681                                         throw new InvalidProviderCredentialException("Invalid credential.");
682                                 }
683                         }
684                 }
685         }
686
687         //TODO this should be renamed, since it is now only one type of response
688         // that we can send
689         public void sendSAMLResponse(HttpServletResponse resp, SAMLAttribute[] attrs, SAMLRequest samlRequest,
690                         RelyingParty relyingParty, SAMLException exception) throws IOException {
691
692                 SAMLException ourSE = null;
693                 SAMLResponse samlResponse = null;
694
695                 try {
696                         if (attrs == null || attrs.length == 0) {
697                                 //No attribute found
698                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, exception);
699                         } else {
700
701                                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
702
703                                 //Reference requested subject
704                                 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
705
706                                 //Set appropriate audience
707                                 ArrayList audiences = new ArrayList();
708                                 if (relyingParty.getProviderId() != null) {
709                                         audiences.add(relyingParty.getProviderId());
710                                 }
711                                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
712                                         audiences.add(relyingParty.getName());
713                                 }
714                                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
715
716                                 //Put all attributes into an assertion
717                                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, Arrays.asList(attrs));
718
719                                 //Set assertion expiration to longest attribute expiration
720                                 long max = 0;
721                                 for (int i = 0; i < attrs.length; i++) {
722                                         if (max < attrs[i].getLifetime()) {
723                                                 max = attrs[i].getLifetime();
724                                         }
725                                 }
726                                 Date now = new Date();
727                                 Date then = new Date(now.getTime() + (max * 1000)); //max is in
728                                 // seconds
729
730                                 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
731                                                 then, Collections.singleton(condition), null, Collections.singleton(statement));
732
733                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), exception);
734                                 addSignatures(samlResponse, relyingParty);
735                         }
736                 } catch (SAMLException se) {
737                         ourSE = se;
738                 } catch (CloneNotSupportedException ex) {
739                         ourSE = new SAMLException(SAMLException.RESPONDER, ex);
740
741                 } finally {
742
743                         if (log.isDebugEnabled()) {
744                                 try {
745                                         log.debug("Dumping generated SAML Response:"
746                                                         + System.getProperty("line.separator")
747                                                         + new String(
748                                                                         new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
749                                                                         "UTF8"));
750                                 } catch (SAMLException e) {
751                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
752                                 } catch (IOException e) {
753                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
754                                 }
755                         }
756
757                         binding.respond(resp, samlResponse, ourSE);
758                 }
759         }
760
761         private static void addSignatures(SAMLResponse reponse, RelyingParty relyingParty) throws SAMLException {
762
763                 //Sign the assertions, if appropriate
764                 if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
765                                 && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
766
767                         String assertionAlgorithm;
768                         if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.RSA) {
769                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
770                         } else if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.DSA) {
771                                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
772                         } else {
773                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
774                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
775                         }
776
777                         ((SAMLAssertion) reponse.getAssertions().next()).sign(assertionAlgorithm, relyingParty
778                                         .getIdentityProvider().getAssertionSigningCredential().getPrivateKey(), Arrays.asList(relyingParty
779                                         .getIdentityProvider().getAssertionSigningCredential().getX509CertificateChain()));
780                 }
781
782                 //Sign the response, if appropriate
783                 if (relyingParty.getIdentityProvider().getResponseSigningCredential() != null
784                                 && relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() != null) {
785
786                         String responseAlgorithm;
787                         if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
788                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
789                         } else if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
790                                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
791                         } else {
792                                 throw new InvalidCryptoException(SAMLException.RESPONDER,
793                                                 "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
794                         }
795
796                         reponse.sign(responseAlgorithm, relyingParty.getIdentityProvider().getResponseSigningCredential()
797                                         .getPrivateKey(), Arrays.asList(relyingParty.getIdentityProvider().getResponseSigningCredential()
798                                         .getX509CertificateChain()));
799                 }
800         }
801
802         class InvalidProviderCredentialException extends Exception {
803
804                 public InvalidProviderCredentialException(String message) {
805                         super(message);
806                 }
807         }
808
809         abstract class ArtifactRepository {
810
811                 // TODO figure out what to do about this interface long term
812                 abstract String addAssertion(SAMLAssertion assertion, HSRelyingParty relyingParty);
813
814                 abstract ArtifactMapping recoverAssertion(String artifact);
815         }
816
817         class ArtifactMapping {
818
819                 //TODO figure out what to do about this interface long term
820                 private String                  assertionHandle;
821
822                 private long                    expirationTime;
823
824                 private SAMLAssertion   assertion;
825
826                 private String                  serviceProviderId;
827
828                 ArtifactMapping(String assertionHandle, SAMLAssertion assertion, ServiceProvider sp) {
829                         this.assertionHandle = assertionHandle;
830                         this.assertion = assertion;
831                         expirationTime = System.currentTimeMillis() + (1000 * 60 * 5); //in 5
832                         // minutes
833                         serviceProviderId = sp.getProviderId();
834                 }
835
836                 boolean isExpired() {
837                         if (System.currentTimeMillis() > expirationTime) { return true; }
838                         return false;
839                 }
840
841                 boolean isCorrectProvider(ServiceProvider sp) {
842                         if (sp.getProviderId().equals(serviceProviderId)) { return true; }
843                         return false;
844                 }
845
846                 SAMLAssertion getAssertion() {
847                         return assertion;
848                 }
849
850                 String getServiceProviderId() {
851                         return serviceProviderId;
852                 }
853         }
854 }