Integrated more of the AA functionality into the unified IdP servlet.
[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
105  * endpoints, routes requests to the appropriate IdP processing components, and
106  * delivers proper protocol responses.
107  * 
108  * @author Walter Hoehn
109  */
110
111 public class IdPResponder extends TargetFederationComponent {
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                 log.info("Recieved a request to dereference an assertion artifact.");
269                 processArtifactDereference(samlRequest, request, response);
270                 return;
271             }
272
273             if (samlRequest.getQuery() != null && (samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
274                 log.info("Recieved an attribute query.");
275                 processAttributeQuery(samlRequest, request, response);
276                 return;
277             }
278
279             throw new SAMLException(SAMLException.REQUESTER,
280                     "Identity Provider unable to respond to this SAML Request type.");
281
282         } catch (Exception e) {
283             log.error("Error while processing request: " + e);
284             try {
285                 //TODO figure out how to implement the passThru error handling
286                 // below
287                 //if (relyingParty != null && relyingParty.passThruErrors()) {
288                 if (false) {
289                     sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
290                             "General error processing request.", e));
291                 } else if (configuration.passThruErrors()) {
292                     sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
293                             "General error processing request.", e));
294                 } else {
295                     sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
296                             "General error processing request."));
297                 }
298                 return;
299             } catch (Exception ee) {
300                 log.fatal("Could not construct a SAML error response: " + ee);
301                 throw new ServletException("Identity Provider response failure.");
302             }
303         }
304     }
305
306     //TODO get rid of this AAException thing
307     private void processAttributeQuery(SAMLRequest samlRequest, HttpServletRequest request, HttpServletResponse response)
308             throws SAMLException, IOException, ServletException, AAException, NameIdentifierMappingException {
309         //TODO validate that the endpoint is valid for the request type
310
311         AARelyingParty relyingParty = null;
312
313         SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
314
315         if (!fromLegacyProvider(request)) {
316             log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
317         }
318
319         //This is the requester name that will be passed to subsystems
320         String effectiveName = null;
321
322         X509Certificate credential = getCredentialFromProvider(request);
323         if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
324             log.info("Request is from an unauthenticated service provider.");
325         } else {
326
327             //Identify a Relying Party
328             relyingParty = targetMapper.getRelyingParty(attributeQuery.getResource());
329
330             try {
331                 effectiveName = getEffectiveName(request, relyingParty);
332             } catch (InvalidProviderCredentialException ipc) {
333                 sendSAMLFailureResponse(response, samlRequest, new SAMLException(SAMLException.RESPONDER,
334                         "Invalid credentials for request."));
335                 return;
336             }
337         }
338
339         if (effectiveName == null) {
340             log.debug("Using default Relying Party for unauthenticated provider.");
341             relyingParty = targetMapper.getRelyingParty(null);
342         }
343
344         //Fail if we can't honor SAML Subject Confirmation
345         if (!fromLegacyProvider(request)) {
346             Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
347             boolean hasConfirmationMethod = false;
348             while (iterator.hasNext()) {
349                 log.info("Request contains SAML Subject Confirmation method: (" + (String) iterator.next() + ").");
350             }
351             if (hasConfirmationMethod) {
352                 throw new SAMLException(SAMLException.REQUESTER,
353                         "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method.");
354             }
355         }
356
357         //Map Subject to local principal
358         Principal principal = null;
359         try {
360             principal = nameMapper.getPrincipal(attributeQuery.getSubject().getName(), relyingParty, relyingParty
361                     .getIdentityProvider());
362             log.info("Request is for principal (" + principal.getName() + ").");
363
364         } catch (InvalidNameIdentifierException invalidNameE) {
365             log.info("Could not associate the request subject with a principal: " + invalidNameE);
366             try {
367                 if (relyingParty.passThruErrors()) {
368                     sendSAMLFailureResponse(response, samlRequest, new SAMLException(Arrays.asList(invalidNameE
369                             .getSAMLErrorCodes()), "The supplied Subject was unrecognized.", invalidNameE));
370
371                 } else {
372                     sendSAMLFailureResponse(response, samlRequest, new SAMLException(Arrays.asList(invalidNameE
373                             .getSAMLErrorCodes()), "The supplied Subject was unrecognized."));
374                 }
375                 return;
376             } catch (Exception ee) {
377                 log.fatal("Could not construct a SAML error response: " + ee);
378                 throw new ServletException("Identity Provider response failure.");
379             }
380         }
381
382         SAMLAttribute[] attrs;
383         Iterator requestedAttrsIterator = attributeQuery.getDesignators();
384         if (requestedAttrsIterator.hasNext()) {
385             log.info("Request designates specific attributes, resolving this set.");
386             ArrayList requestedAttrs = new ArrayList();
387             while (requestedAttrsIterator.hasNext()) {
388                 SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
389                 try {
390                     log.debug("Designated attribute: (" + attribute.getName() + ")");
391                     requestedAttrs.add(new URI(attribute.getName()));
392                 } catch (URISyntaxException use) {
393                     log.error("Request designated an attribute name that does not conform to the required URI syntax ("
394                             + attribute.getName() + ").  Ignoring this attribute");
395                 }
396             }
397
398             attrs = responder.getReleaseAttributes(principal, effectiveName, null, (URI[]) requestedAttrs
399                     .toArray(new URI[0]));
400         } else {
401             log.info("Request does not designate specific attributes, resolving all available.");
402             attrs = responder.getReleaseAttributes(principal, effectiveName, null);
403         }
404
405         log.info("Found " + attrs.length + " attribute(s) for " + principal.getName());
406         sendSAMLResponse(response, attrs, samlRequest, relyingParty, null);
407         log.info("Successfully responded about " + principal.getName());
408
409         if (effectiveName == null) {
410             if (fromLegacyProvider(request)) {
411                 transactionLog.info("Attribute assertion issued to anonymous legacy provider at ("
412                         + request.getRemoteAddr() + ") on behalf of principal (" + principal.getName() + ").");
413             } else {
414                 transactionLog.info("Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
415                         + ") on behalf of principal (" + principal.getName() + ").");
416             }
417         } else {
418             if (fromLegacyProvider(request)) {
419                 transactionLog.info("Attribute assertion issued to legacy provider (" + effectiveName
420                         + ") on behalf of principal (" + principal.getName() + ").");
421             } else {
422                 transactionLog.info("Attribute assertion issued to provider (" + effectiveName
423                         + ") on behalf of principal (" + principal.getName() + ").");
424             }
425         }
426
427     }
428
429     private void processArtifactDereference(SAMLRequest samlRequest, HttpServletRequest request,
430             HttpServletResponse response) throws SAMLException, IOException {
431         //TODO validate that the endpoint is valid for the request type
432         //TODO how about signatures on artifact dereferencing
433
434         // Pull credential from request
435         X509Certificate credential = getCredentialFromProvider(request);
436         if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
437             log.info("Request is from an unauthenticated service provider.");
438             throw new SAMLException(SAMLException.REQUESTER,
439                     "SAML Artifacts cannot be dereferenced for unauthenticated requesters.");
440         }
441
442         log.info("Request contains credential: (" + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
443                 + ").");
444
445         ArrayList assertions = new ArrayList();
446         Iterator artifacts = samlRequest.getArtifacts();
447
448         int queriedArtifacts = 0;
449         StringBuffer dereferencedArtifacts = new StringBuffer(); //for
450         // transaction
451         // log
452         while (artifacts.hasNext()) {
453             queriedArtifacts++;
454             String artifact = (String) artifacts.next();
455             log.debug("Attempting to dereference artifact: (" + artifact + ").");
456             ArtifactMapping mapping = artifactRepository.recoverAssertion(artifact);
457             if (mapping != null) {
458                 SAMLAssertion assertion = mapping.getAssertion();
459
460                 //See if we have metadata for this provider
461                 Provider provider = lookup(mapping.getServiceProviderId());
462                 if (provider == null) {
463                     log.info("No metadata found for provider: (" + mapping.getServiceProviderId() + ").");
464                     throw new SAMLException(SAMLException.REQUESTER, "Invalid service provider.");
465                 }
466
467                 //Make sure that the suppplied credential is valid for the
468                 // provider to which the artifact was issued
469                 if (!isValidCredential(provider, credential)) {
470                     log.error("Supplied credential ("
471                             + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
472                             + ") is NOT valid for provider (" + mapping.getServiceProviderId()
473                             + "), to whom this artifact was issued.");
474                     throw new SAMLException(SAMLException.REQUESTER, "Invalid credential.");
475                 }
476
477                 log.debug("Supplied credential validated for the provider to which this artifact was issued.");
478
479                 assertions.add(assertion);
480                 dereferencedArtifacts.append("(" + artifact + ")");
481             }
482         }
483
484         //The spec requires that if any artifacts are dereferenced, they must
485         // all be dereferenced
486         if (assertions.size() > 0 & assertions.size() != queriedArtifacts) {
487             throw new SAMLException(SAMLException.REQUESTER, "Unable to successfully dereference all artifacts.");
488         }
489
490         //Create and send response
491         SAMLResponse samlResponse = new SAMLResponse(samlRequest.getId(), null, assertions, null);
492
493         if (log.isDebugEnabled()) {
494             try {
495                 log.debug("Dumping generated SAML Response:"
496                         + System.getProperty("line.separator")
497                         + new String(new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
498                                 "UTF8"));
499             } catch (SAMLException e) {
500                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
501             } catch (IOException e) {
502                 log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
503             }
504         }
505
506         binding.respond(response, samlResponse, null);
507
508         transactionLog.info("Succesfully dereferenced the following artifacts: " + dereferencedArtifacts.toString());
509         //TODO make sure we can delete this junk below
510         /*
511          * } catch (Exception e) { log.error("Error while processing request: " +
512          * e); try { sendFailure(res, samlRequest, new
513          * SAMLException(SAMLException.RESPONDER, "General error processing
514          * request.")); return; } catch (Exception ee) { log.fatal("Could not
515          * construct a SAML error response: " + ee); throw new
516          * ServletException("Handle Service response failure."); } }
517          */
518     }
519
520     public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
521
522         MDC.put("serviceId", "[IdP] " + new SAMLIdentifier().toString());
523         MDC.put("remoteAddr", request.getRemoteAddr());
524         log.debug("Recieved a request via GET.");
525     }
526
527     private static X509Certificate getCredentialFromProvider(HttpServletRequest req) {
528         X509Certificate[] certArray = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
529         if (certArray != null && certArray.length > 0) {
530             return certArray[0];
531         }
532         return null;
533     }
534
535     private static boolean isValidCredential(Provider provider, X509Certificate certificate) {
536
537         ProviderRole[] roles = provider.getRoles();
538         if (roles.length == 0) {
539             log.info("Inappropriate metadata for provider.");
540             return false;
541         }
542         //TODO figure out what to do about this role business here
543         for (int i = 0; roles.length > i; i++) {
544             if (roles[i] instanceof AttributeConsumerRole) {
545                 KeyDescriptor[] descriptors = roles[i].getKeyDescriptors();
546                 for (int j = 0; descriptors.length > j; j++) {
547                     KeyInfo[] keyInfo = descriptors[j].getKeyInfo();
548                     for (int k = 0; keyInfo.length > k; k++) {
549                         for (int l = 0; keyInfo[k].lengthKeyName() > l; l++) {
550                             try {
551
552                                 //First, try to match DN against metadata
553                                 try {
554                                     if (certificate.getSubjectX500Principal().getName(X500Principal.RFC2253).equals(
555                                             new X500Principal(keyInfo[k].itemKeyName(l).getKeyName())
556                                                     .getName(X500Principal.RFC2253))) {
557                                         log.debug("Matched against DN.");
558                                         return true;
559                                     }
560                                 } catch (IllegalArgumentException iae) {
561                                     //squelch this runtime exception, since
562                                     // this might be a valid case
563                                 }
564
565                                 //If that doesn't work, we try matching against
566                                 // some Subject Alt Names
567                                 try {
568                                     Collection altNames = certificate.getSubjectAlternativeNames();
569                                     if (altNames != null) {
570                                         for (Iterator nameIterator = altNames.iterator(); nameIterator.hasNext();) {
571                                             List altName = (List) nameIterator.next();
572                                             if (altName.get(0).equals(new Integer(2))
573                                                     || altName.get(0).equals(new Integer(6))) { //2 is
574                                                 // DNS,
575                                                 // 6 is
576                                                 // URI
577                                                 if (altName.get(1).equals(keyInfo[k].itemKeyName(l).getKeyName())) {
578                                                     log.debug("Matched against SubjectAltName.");
579                                                     return true;
580                                                 }
581                                             }
582                                         }
583                                     }
584                                 } catch (CertificateParsingException e1) {
585                                     log
586                                             .error("Encountered an problem trying to extract Subject Alternate Name from supplied certificate: "
587                                                     + e1);
588                                 }
589
590                                 //If that doesn't work, try to match using
591                                 // SSL-style hostname matching
592                                 if (ShibPOSTProfile.getHostNameFromDN(certificate.getSubjectX500Principal()).equals(
593                                         keyInfo[k].itemKeyName(l).getKeyName())) {
594                                     log.debug("Matched against hostname.");
595                                     return true;
596                                 }
597
598                             } catch (XMLSecurityException e) {
599                                 log.error("Encountered an error reading federation metadata: " + e);
600                             }
601                         }
602                     }
603                 }
604             }
605         }
606         log.info("Supplied credential not found in metadata.");
607         return false;
608     }
609
610     private void sendSAMLFailureResponse(HttpServletResponse httpResponse, SAMLRequest samlRequest,
611             SAMLException exception) throws IOException {
612         try {
613             SAMLResponse samlResponse = new SAMLResponse((samlRequest != null) ? samlRequest.getId() : null, null,
614                     null, exception);
615             if (log.isDebugEnabled()) {
616                 try {
617                     log.debug("Dumping generated SAML Error Response:"
618                             + System.getProperty("line.separator")
619                             + new String(
620                                     new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
621                                     "UTF8"));
622                 } catch (IOException e) {
623                     log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
624                 }
625             }
626             binding.respond(httpResponse, samlResponse, null);
627             log.debug("Returning SAML Error Response.");
628         } catch (SAMLException se) {
629             binding.respond(httpResponse, null, exception);
630             log.error("Identity Provider failed to make an error message: " + se);
631         }
632     }
633
634     private static boolean fromLegacyProvider(HttpServletRequest request) {
635         String version = request.getHeader("Shibboleth");
636         if (version != null) {
637             log.debug("Request from Shibboleth version: " + version);
638             return false;
639         }
640         log.debug("No version header found.");
641         return true;
642     }
643
644     private String getEffectiveName(HttpServletRequest req, AARelyingParty relyingParty)
645             throws InvalidProviderCredentialException {
646
647         //X500Principal credentialName = getCredentialName(req);
648         X509Certificate credential = getCredentialFromProvider(req);
649
650         if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
651             log.info("Request is from an unauthenticated service provider.");
652             return null;
653
654         } else {
655             log.info("Request contains credential: ("
656                     + credential.getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
657             //Mockup old requester name for requests from < 1.2 targets
658             if (fromLegacyProvider(req)) {
659                 String legacyName = ShibPOSTProfile.getHostNameFromDN(credential.getSubjectX500Principal());
660                 if (legacyName == null) {
661                     log.error("Unable to extract legacy requester name from certificate subject.");
662                 }
663
664                 log.info("Request from legacy service provider: (" + legacyName + ").");
665                 return legacyName;
666
667             } else {
668
669                 //See if we have metadata for this provider
670                 Provider provider = lookup(relyingParty.getProviderId());
671                 if (provider == null) {
672                     log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
673                     log.info("Treating remote provider as unauthenticated.");
674                     return null;
675                 }
676
677                 //Make sure that the suppplied credential is valid for the
678                 // selected relying party
679                 if (isValidCredential(provider, credential)) {
680                     log.info("Supplied credential validated for this provider.");
681                     log.info("Request from service provider: (" + relyingParty.getProviderId() + ").");
682                     return relyingParty.getProviderId();
683                 } else {
684                     log.error("Supplied credential ("
685                             + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
686                             + ") is NOT valid for provider (" + relyingParty.getProviderId() + ").");
687                     throw new InvalidProviderCredentialException("Invalid credential.");
688                 }
689             }
690         }
691     }
692
693     //TODO this should be renamed, since it is now only one type of response
694     // that we can send
695     public void sendSAMLResponse(HttpServletResponse resp, SAMLAttribute[] attrs, SAMLRequest samlRequest,
696             RelyingParty relyingParty, SAMLException exception) throws IOException {
697
698         SAMLException ourSE = null;
699         SAMLResponse samlResponse = null;
700
701         try {
702             if (attrs == null || attrs.length == 0) {
703                 //No attribute found
704                 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, exception);
705             } else {
706
707                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
708
709                 //Reference requested subject
710                 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
711
712                 //Set appropriate audience
713                 ArrayList audiences = new ArrayList();
714                 if (relyingParty.getProviderId() != null) {
715                     audiences.add(relyingParty.getProviderId());
716                 }
717                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
718                     audiences.add(relyingParty.getName());
719                 }
720                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
721
722                 //Put all attributes into an assertion
723                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, Arrays.asList(attrs));
724
725                 //Set assertion expiration to longest attribute expiration
726                 long max = 0;
727                 for (int i = 0; i < attrs.length; i++) {
728                     if (max < attrs[i].getLifetime()) {
729                         max = attrs[i].getLifetime();
730                     }
731                 }
732                 Date now = new Date();
733                 Date then = new Date(now.getTime() + (max * 1000)); //max is in
734                 // seconds
735
736                 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
737                         then, Collections.singleton(condition), null, Collections.singleton(statement));
738
739                 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), exception);
740                 addSignatures(samlResponse, relyingParty);
741             }
742         } catch (SAMLException se) {
743             ourSE = se;
744         } catch (CloneNotSupportedException ex) {
745             ourSE = new SAMLException(SAMLException.RESPONDER, ex);
746
747         } finally {
748
749             if (log.isDebugEnabled()) {
750                 try {
751                     log.debug("Dumping generated SAML Response:"
752                             + System.getProperty("line.separator")
753                             + new String(
754                                     new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
755                                     "UTF8"));
756                 } catch (SAMLException e) {
757                     log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
758                 } catch (IOException e) {
759                     log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
760                 }
761             }
762
763             binding.respond(resp, samlResponse, ourSE);
764         }
765     }
766
767     private static void addSignatures(SAMLResponse reponse, RelyingParty relyingParty) throws SAMLException {
768
769         //Sign the assertions, if appropriate
770         if (relyingParty.getIdentityProvider().getAssertionSigningCredential() != null
771                 && relyingParty.getIdentityProvider().getAssertionSigningCredential().getPrivateKey() != null) {
772
773             String assertionAlgorithm;
774             if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.RSA) {
775                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
776             } else if (relyingParty.getIdentityProvider().getAssertionSigningCredential().getCredentialType() == Credential.DSA) {
777                 assertionAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
778             } else {
779                 throw new InvalidCryptoException(SAMLException.RESPONDER,
780                         "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
781             }
782
783             ((SAMLAssertion) reponse.getAssertions().next()).sign(assertionAlgorithm, relyingParty
784                     .getIdentityProvider().getAssertionSigningCredential().getPrivateKey(), Arrays.asList(relyingParty
785                     .getIdentityProvider().getAssertionSigningCredential().getX509CertificateChain()));
786         }
787
788         //Sign the response, if appropriate
789         if (relyingParty.getIdentityProvider().getResponseSigningCredential() != null
790                 && relyingParty.getIdentityProvider().getResponseSigningCredential().getPrivateKey() != null) {
791
792             String responseAlgorithm;
793             if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.RSA) {
794                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA1;
795             } else if (relyingParty.getIdentityProvider().getResponseSigningCredential().getCredentialType() == Credential.DSA) {
796                 responseAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA;
797             } else {
798                 throw new InvalidCryptoException(SAMLException.RESPONDER,
799                         "ShibPOSTProfile.prepare() currently only supports signing with RSA and DSA keys.");
800             }
801
802             reponse.sign(responseAlgorithm, relyingParty.getIdentityProvider().getResponseSigningCredential()
803                     .getPrivateKey(), Arrays.asList(relyingParty.getIdentityProvider().getResponseSigningCredential()
804                     .getX509CertificateChain()));
805         }
806     }
807
808     class InvalidProviderCredentialException extends Exception {
809
810         public InvalidProviderCredentialException(String message) {
811             super(message);
812         }
813     }
814
815     abstract class ArtifactRepository {
816
817         // TODO figure out what to do about this interface long term
818         abstract String addAssertion(SAMLAssertion assertion, HSRelyingParty relyingParty);
819
820         abstract ArtifactMapping recoverAssertion(String artifact);
821     }
822
823     class ArtifactMapping {
824
825         //TODO figure out what to do about this interface long term
826         private String assertionHandle;
827
828         private long expirationTime;
829
830         private SAMLAssertion assertion;
831
832         private String serviceProviderId;
833
834         ArtifactMapping(String assertionHandle, SAMLAssertion assertion, ServiceProvider sp) {
835             this.assertionHandle = assertionHandle;
836             this.assertion = assertion;
837             expirationTime = System.currentTimeMillis() + (1000 * 60 * 5); //in 5
838             // minutes
839             serviceProviderId = sp.getProviderId();
840         }
841
842         boolean isExpired() {
843             if (System.currentTimeMillis() > expirationTime) {
844                 return true;
845             }
846             return false;
847         }
848
849         boolean isCorrectProvider(ServiceProvider sp) {
850             if (sp.getProviderId().equals(serviceProviderId)) {
851                 return true;
852             }
853             return false;
854         }
855
856         SAMLAssertion getAssertion() {
857             return assertion;
858         }
859
860         String getServiceProviderId() {
861             return serviceProviderId;
862         }
863     }
864 }