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