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