Plug new Trust logic into IdP processing.
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / provider / SAMLv1_AttributeQueryHandler.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.provider;
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.X509Certificate;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collections;
36 import java.util.Date;
37 import java.util.Iterator;
38
39 import javax.security.auth.x500.X500Principal;
40 import javax.servlet.ServletException;
41 import javax.servlet.http.HttpServletRequest;
42 import javax.servlet.http.HttpServletResponse;
43
44 import org.apache.log4j.Logger;
45 import org.opensaml.SAMLAssertion;
46 import org.opensaml.SAMLAttribute;
47 import org.opensaml.SAMLAttributeDesignator;
48 import org.opensaml.SAMLAttributeQuery;
49 import org.opensaml.SAMLAttributeStatement;
50 import org.opensaml.SAMLAudienceRestrictionCondition;
51 import org.opensaml.SAMLCondition;
52 import org.opensaml.SAMLException;
53 import org.opensaml.SAMLRequest;
54 import org.opensaml.SAMLResponse;
55 import org.opensaml.SAMLStatement;
56 import org.opensaml.SAMLSubject;
57 import org.w3c.dom.Element;
58
59 import sun.misc.BASE64Decoder;
60 import edu.internet2.middleware.shibboleth.aa.AAException;
61 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
62 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
63 import edu.internet2.middleware.shibboleth.common.RelyingParty;
64 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
65 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
66 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
67 import edu.internet2.middleware.shibboleth.metadata.EntityDescriptor;
68 import edu.internet2.middleware.shibboleth.metadata.KeyDescriptor;
69 import edu.internet2.middleware.shibboleth.metadata.RoleDescriptor;
70 import edu.internet2.middleware.shibboleth.metadata.SPSSODescriptor;
71
72 /**
73  * @author Walter Hoehn
74  */
75 public class SAMLv1_AttributeQueryHandler extends BaseServiceHandler implements IdPProtocolHandler {
76
77         private static Logger log = Logger.getLogger(SAMLv1_AttributeQueryHandler.class.getName());
78
79         /**
80          * Required DOM-based constructor.
81          */
82         public SAMLv1_AttributeQueryHandler(Element config) throws ShibbolethConfigurationException {
83
84                 super(config);
85         }
86
87         /*
88          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#getHandlerName()
89          */
90         public String getHandlerName() {
91
92                 return "SAML v1.1 Attribute Query";
93         }
94
95         private String getEffectiveName(HttpServletRequest req, RelyingParty relyingParty, IdPProtocolSupport support)
96                         throws InvalidProviderCredentialException {
97
98                 X509Certificate credential = getCredentialFromProvider(req);
99
100                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
101                         log.info("Request is from an unauthenticated service provider.");
102                         return null;
103
104                 } else {
105                         log.info("Request contains credential: ("
106                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
107                         // Mockup old requester name for requests from < 1.2 targets
108                         if (fromLegacyProvider(req)) {
109                                 String legacyName = getHostNameFromDN(credential.getSubjectX500Principal());
110                                 if (legacyName == null) {
111                                         log.error("Unable to extract legacy requester name from certificate subject.");
112                                 }
113
114                                 log.info("Request from legacy service provider: (" + legacyName + ").");
115                                 return legacyName;
116
117                         } else {
118
119                                 // See if we have metadata for this provider
120                                 EntityDescriptor provider = support.lookup(relyingParty.getProviderId());
121                                 if (provider == null) {
122                                         log.info("No metadata found for provider: (" + relyingParty.getProviderId() + ").");
123                                         log.info("Treating remote provider as unauthenticated.");
124                                         return null;
125                                 }
126                                 RoleDescriptor role = provider.getSPSSODescriptor("urn:oasis:names:tc:SAML:1.1:protocol");
127                                 if (role == null) {
128                                         log.info("SPSSO role not found in metadata for provider: (" + relyingParty.getProviderId() + ").");
129                                         log.info("Treating remote provider as unauthenticated.");
130                                         return null;
131                                 }
132
133                                 // Make sure that the suppplied credential is valid for the
134                                 // selected relying party
135                                 if (support.getTrust().validate(role,
136                                                 (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate"),
137                                                 KeyDescriptor.ENCRYPTION)) {
138                                         log.info("Supplied credential validated for this provider.");
139                                         log.info("Request from service provider: (" + relyingParty.getProviderId() + ").");
140                                         return relyingParty.getProviderId();
141
142                                 } else {
143                                         log.error("Supplied credential ("
144                                                         + credential.getSubjectX500Principal().getName(X500Principal.RFC2253)
145                                                         + ") is NOT valid for provider (" + relyingParty.getProviderId() + ").");
146                                         throw new InvalidProviderCredentialException("Invalid credential.");
147                                 }
148                         }
149                 }
150         }
151
152         /*
153          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
154          *      javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
155          *      edu.internet2.middleware.shibboleth.idp.ProtocolSupport)
156          */
157         public SAMLResponse processRequest(HttpServletRequest request, HttpServletResponse response,
158                         SAMLRequest samlRequest, IdPProtocolSupport support) throws SAMLException, IOException, ServletException {
159
160                 if (samlRequest.getQuery() == null || !(samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
161                         log.error("Protocol Handler can only respond to SAML Attribute Queries.");
162                         throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
163                 }
164
165                 RelyingParty relyingParty = null;
166
167                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
168
169                 if (!fromLegacyProvider(request)) {
170                         log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
171                 }
172
173                 // This is the requester name that will be passed to subsystems
174                 String effectiveName = null;
175
176                 X509Certificate credential = getCredentialFromProvider(request);
177                 if (credential == null || credential.getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
178                         log.info("Request is from an unauthenticated service provider.");
179                 } else {
180
181                         // Identify a Relying Party
182                         relyingParty = support.getServiceProviderMapper().getRelyingParty(attributeQuery.getResource());
183
184                         try {
185                                 effectiveName = getEffectiveName(request, relyingParty, support);
186                         } catch (InvalidProviderCredentialException ipc) {
187                                 throw new SAMLException(SAMLException.REQUESTER, "Invalid credentials for request.");
188                         }
189                 }
190
191                 if (effectiveName == null) {
192                         log.debug("Using default Relying Party for unauthenticated provider.");
193                         relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
194                 }
195
196                 // Fail if we can't honor SAML Subject Confirmation
197                 if (!fromLegacyProvider(request)) {
198                         Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
199                         boolean hasConfirmationMethod = false;
200                         while (iterator.hasNext()) {
201                                 log.info("Request contains SAML Subject Confirmation method: (" + (String) iterator.next() + ").");
202                         }
203                         if (hasConfirmationMethod) { throw new SAMLException(SAMLException.REQUESTER,
204                                         "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method."); }
205                 }
206
207                 // Map Subject to local principal
208                 Principal principal;
209                 try {
210                         principal = support.getNameMapper().getPrincipal(attributeQuery.getSubject().getName(), relyingParty,
211                                         relyingParty.getIdentityProvider());
212
213                         log.info("Request is for principal (" + principal.getName() + ").");
214
215                         SAMLAttribute[] attrs;
216                         Iterator requestedAttrsIterator = attributeQuery.getDesignators();
217                         if (requestedAttrsIterator.hasNext()) {
218                                 log.info("Request designates specific attributes, resolving this set.");
219                                 ArrayList requestedAttrs = new ArrayList();
220                                 while (requestedAttrsIterator.hasNext()) {
221                                         SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
222                                         try {
223                                                 log.debug("Designated attribute: (" + attribute.getName() + ")");
224                                                 requestedAttrs.add(new URI(attribute.getName()));
225                                         } catch (URISyntaxException use) {
226                                                 log.error("Request designated an attribute name that does not conform "
227                                                                 + "to the required URI syntax (" + attribute.getName() + ").  Ignoring this attribute");
228                                         }
229                                 }
230
231                                 attrs = support.getReleaseAttributes(principal, effectiveName, null, (URI[]) requestedAttrs
232                                                 .toArray(new URI[0]));
233                         } else {
234                                 log.info("Request does not designate specific attributes, resolving all available.");
235                                 attrs = support.getReleaseAttributes(principal, effectiveName, null);
236                         }
237
238                         log.info("Found " + attrs.length + " attribute(s) for " + principal.getName());
239
240                         SAMLResponse samlResponse = null;
241
242                         if (attrs == null || attrs.length == 0) {
243                                 // No attribute found
244                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, null, null);
245
246                         } else {
247                                 // Reference requested subject
248                                 SAMLSubject rSubject = (SAMLSubject) attributeQuery.getSubject().clone();
249
250                                 ArrayList audiences = new ArrayList();
251                                 if (relyingParty.getProviderId() != null) {
252                                         audiences.add(relyingParty.getProviderId());
253                                 }
254                                 if (relyingParty.getName() != null && !relyingParty.getName().equals(relyingParty.getProviderId())) {
255                                         audiences.add(relyingParty.getName());
256                                 }
257                                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
258
259                                 // Put all attributes into an assertion
260                                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, Arrays.asList(attrs));
261
262                                 // Set assertion expiration to longest attribute expiration
263                                 long max = 0;
264                                 for (int i = 0; i < attrs.length; i++) {
265                                         if (max < attrs[i].getLifetime()) {
266                                                 max = attrs[i].getLifetime();
267                                         }
268                                 }
269                                 Date now = new Date();
270                                 Date then = new Date(now.getTime() + (max * 1000)); // max is in
271                                 // seconds
272
273                                 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
274                                                 then, Collections.singleton(condition), null, Collections.singleton(statement));
275
276                                 // Sign the assertions, if necessary
277                                 boolean metaDataIndicatesSignAssertions = false;
278                                 EntityDescriptor descriptor = support.lookup(relyingParty.getProviderId());
279                                 if (descriptor != null) {
280                                         SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
281                                         if (sp != null) {
282                                                 if (sp.getWantAssertionsSigned()) {
283                                                         metaDataIndicatesSignAssertions = true;
284                                                 }
285                                         }
286                                 }
287                                 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
288                                         support.signAssertions(new SAMLAssertion[]{sAssertion}, relyingParty);
289                                 }
290
291                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), null);
292                         }
293
294                         if (log.isDebugEnabled()) { // This takes some processing, so only do it if we need to
295                                 try {
296                                         log.debug("Dumping generated SAML Response:"
297                                                         + System.getProperty("line.separator")
298                                                         + new String(
299                                                                         new BASE64Decoder().decodeBuffer(new String(samlResponse.toBase64(), "ASCII")),
300                                                                         "UTF8"));
301                                 } catch (SAMLException e) {
302                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
303                                 } catch (IOException e) {
304                                         log.error("Encountered an error while decoding SAMLReponse for logging purposes.");
305                                 }
306                         }
307
308                         log.info("Successfully created response for principal (" + principal.getName() + ").");
309
310                         if (effectiveName == null) {
311                                 if (fromLegacyProvider(request)) {
312                                         support.getTransactionLog().info(
313                                                         "Attribute assertion issued to anonymous legacy provider at (" + request.getRemoteAddr()
314                                                                         + ") on behalf of principal (" + principal.getName() + ").");
315                                 } else {
316                                         support.getTransactionLog().info(
317                                                         "Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
318                                                                         + ") on behalf of principal (" + principal.getName() + ").");
319                                 }
320                         } else {
321                                 if (fromLegacyProvider(request)) {
322                                         support.getTransactionLog().info(
323                                                         "Attribute assertion issued to legacy provider (" + effectiveName
324                                                                         + ") on behalf of principal (" + principal.getName() + ").");
325                                 } else {
326                                         support.getTransactionLog().info(
327                                                         "Attribute assertion issued to provider (" + effectiveName + ") on behalf of principal ("
328                                                                         + principal.getName() + ").");
329                                 }
330                         }
331
332                         return samlResponse;
333
334                 } catch (SAMLException e) {
335                         if (relyingParty.passThruErrors()) {
336                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.", e);
337                         } else {
338                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
339                         }
340
341                 } catch (InvalidNameIdentifierException e) {
342                         log.error("Could not associate the request's subject with a principal: " + e);
343                         if (relyingParty.passThruErrors()) {
344                                 throw new SAMLException(Arrays.asList(e.getSAMLErrorCodes()), "The supplied Subject was unrecognized.",
345                                                 e);
346                         } else {
347                                 throw new SAMLException(Arrays.asList(e.getSAMLErrorCodes()), "The supplied Subject was unrecognized.");
348                         }
349
350                 } catch (NameIdentifierMappingException e) {
351                         log.error("Encountered an error while mapping the name identifier from the request: " + e);
352                         if (relyingParty.passThruErrors()) {
353                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.", e);
354                         } else {
355                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
356                         }
357
358                 } catch (AAException e) {
359                         log.error("Encountered an error while resolving resolving attributes: " + e);
360                         if (relyingParty.passThruErrors()) {
361                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.", e);
362                         } else {
363                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
364                         }
365
366                 } catch (CloneNotSupportedException e) {
367                         log.error("Encountered an error while cloning request subject for use in response: " + e);
368                         if (relyingParty.passThruErrors()) {
369                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.", e);
370                         } else {
371                                 throw new SAMLException(SAMLException.RESPONDER, "General error processing request.");
372                         }
373                 }
374         }
375
376         private static boolean fromLegacyProvider(HttpServletRequest request) {
377
378                 String version = request.getHeader("Shibboleth");
379                 if (version != null) {
380                         log.debug("Request from Shibboleth version: " + version);
381                         return false;
382                 }
383                 log.debug("No version header found.");
384                 return true;
385         }
386
387 }