Create parser pool interface and move current pool to an implementation of this inter...
[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.net.URI;
20 import java.net.URISyntaxException;
21 import java.security.Principal;
22 import java.security.cert.X509Certificate;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Date;
28 import java.util.Iterator;
29
30 import javax.security.auth.x500.X500Principal;
31 import javax.servlet.ServletException;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34
35 import org.apache.log4j.Logger;
36 import org.opensaml.SAMLAssertion;
37 import org.opensaml.SAMLAttribute;
38 import org.opensaml.SAMLAttributeDesignator;
39 import org.opensaml.SAMLAttributeQuery;
40 import org.opensaml.SAMLAttributeStatement;
41 import org.opensaml.SAMLAudienceRestrictionCondition;
42 import org.opensaml.SAMLCondition;
43 import org.opensaml.SAMLException;
44 import org.opensaml.SAMLNameIdentifier;
45 import org.opensaml.SAMLRequest;
46 import org.opensaml.SAMLResponse;
47 import org.opensaml.SAMLStatement;
48 import org.opensaml.SAMLSubject;
49 import org.opensaml.XML;
50 import org.opensaml.saml2.metadata.EntityDescriptor;
51 import org.opensaml.saml2.metadata.RoleDescriptor;
52 import org.opensaml.saml2.metadata.SPSSODescriptor;
53 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
54 import org.opensaml.security.X509EntityCredential;
55 import org.opensaml.security.impl.HttpX509EntityCredential;
56 import org.w3c.dom.Element;
57
58 import edu.internet2.middleware.shibboleth.aa.AAException;
59 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
60 import edu.internet2.middleware.shibboleth.common.NameIdentifierMapping;
61 import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException;
62 import edu.internet2.middleware.shibboleth.common.RelyingParty;
63 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
64 import edu.internet2.middleware.shibboleth.idp.IdPProtocolHandler;
65 import edu.internet2.middleware.shibboleth.idp.IdPProtocolSupport;
66 import edu.internet2.middleware.shibboleth.idp.RequestHandlingException;
67
68 /**
69  * @author Walter Hoehn
70  */
71 public class SAMLv1_AttributeQueryHandler extends SAMLv1_Base_QueryHandler implements IdPProtocolHandler {
72
73         static Logger log = Logger.getLogger(SAMLv1_AttributeQueryHandler.class.getName());
74
75         /**
76          * Required DOM-based constructor.
77          */
78         public SAMLv1_AttributeQueryHandler(Element config) throws ShibbolethConfigurationException {
79
80                 super(config);
81         }
82
83         /*
84          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#getHandlerName()
85          */
86         public String getHandlerName() {
87
88                 return "SAML v1.1 Attribute Query";
89         }
90
91         private String authenticateAs(String assertedId, X509Credential credential, IdPProtocolSupport support)
92                         throws InvalidProviderCredentialException {
93
94                 // See if we have metadata for this provider
95                 EntityDescriptor provider = null;
96                 try {
97                         provider = support.getEntityDescriptor(assertedId);
98                 } catch (MetadataProviderException e) {
99                         log.error("Encountered an error while looking up metadata: " + e);
100                 }
101                 if (provider == null) {
102                         log.info("No metadata found for providerId: (" + assertedId + ").");
103                         return null;
104                 } else {
105                         log.info("Metadata found for providerId: (" + assertedId + ").");
106                 }
107                 // TODO pulled this code out for now because we don't have an extension interface to support it.
108                 // Need to add it back with current draft profile as of release time.
109                 /*
110                  * RoleDescriptor ar_role = provider.getAttributeRequesterDescriptor(XML.SAML11_PROTOCOL_ENUM);
111                  */
112                 RoleDescriptor ar_role = null;
113                 SPSSODescriptor sp_role = provider.getSPSSODescriptor(XML.SAML11_PROTOCOL_ENUM);
114                 if (ar_role == null && sp_role == null) {
115                         log.info("SPSSO and Stand-Alone Requester roles not found in metadata for provider: (" + assertedId + ").");
116                         return null;
117                 }
118
119                 // Make sure that the supplied credential is valid for the selected provider role.
120                 if ((ar_role != null && support.getTrustEngine().validate(credential, ar_role))
121                                 || (sp_role != null && support.getTrustEngine().validate(credential, sp_role))) {
122                         log.info("Supplied credentials validated for this provider.");
123                         return assertedId;
124                 } else {
125                         log.error("Supplied credentials ("
126                                         + credential.getEntityCertificate().getSubjectX500Principal().getName(X500Principal.RFC2253)
127                                         + ") are NOT valid for provider (" + assertedId + ").");
128                         throw new InvalidProviderCredentialException("Invalid credentials.");
129                 }
130         }
131
132         /*
133          * @see edu.internet2.middleware.shibboleth.idp.ProtocolHandler#processRequest(javax.servlet.http.HttpServletRequest,
134          *      javax.servlet.http.HttpServletResponse, org.opensaml.SAMLRequest,
135          *      edu.internet2.middleware.shibboleth.idp.ProtocolSupport)
136          */
137         public void processRequest(HttpServletRequest request, HttpServletResponse response, IdPProtocolSupport support)
138                         throws RequestHandlingException, ServletException {
139
140                 SAMLRequest samlRequest = parseSAMLRequest(request);
141
142                 if (samlRequest == null || samlRequest.getQuery() == null
143                                 || !(samlRequest.getQuery() instanceof SAMLAttributeQuery)) {
144                         log.error("Protocol Handler can only respond to SAML Attribute Queries.");
145                         respondWithError(response, samlRequest, new SAMLException("General error processing request."));
146                         return;
147                 }
148
149                 RelyingParty relyingParty = null;
150                 SAMLAttributeQuery attributeQuery = (SAMLAttributeQuery) samlRequest.getQuery();
151
152                 // This is the requester name that will be passed to subsystems
153                 String effectiveName = null;
154
155                 // Log the physical credential supplied, if any.
156                 X509Certificate[] credentials = (X509Certificate[]) request
157                                 .getAttribute("javax.servlet.request.X509Certificate");
158                 if (credentials == null || credentials.length == 0
159                                 || credentials[0].getSubjectX500Principal().getName(X500Principal.RFC2253).equals("")) {
160                         log.info("Request contained no credentials, treating as an unauthenticated service provider.");
161                 } else {
162                         log.info("Request contains credentials: ("
163                                         + credentials[0].getSubjectX500Principal().getName(X500Principal.RFC2253) + ").");
164
165                         // Try and authenticate the requester as any of the potentially relevant identifiers we know.
166                         try {
167                                 if (attributeQuery.getResource() != null) {
168                                         log.info("Remote provider has identified itself as: (" + attributeQuery.getResource() + ").");
169                                         effectiveName = authenticateAs(attributeQuery.getResource(), new HttpX509EntityCredential(request),
170                                                         support);
171                                 }
172
173                                 if (effectiveName == null) {
174                                         log.info("Remote provider not yet identified, attempting to "
175                                                         + "derive requesting provider from credentials.");
176
177                                         // Try the additional candidates.
178                                         String[] candidateNames = getCredentialNames(credentials[0]);
179                                         for (int c = 0; effectiveName == null && c < candidateNames.length; c++) {
180                                                 effectiveName = authenticateAs(candidateNames[c], new HttpX509EntityCredential(request),
181                                                                 support);
182                                         }
183                                 }
184                         } catch (InvalidProviderCredentialException ipc) {
185                                 respondWithError(response, samlRequest, new SAMLException(SAMLException.REQUESTER,
186                                                 "Invalid credentials for request."));
187                                 return;
188                         }
189                 }
190
191                 if (effectiveName == null) {
192                         log.info("Unable to locate metadata about provider, treating as an unauthenticated service provider.");
193                         relyingParty = support.getServiceProviderMapper().getRelyingParty(null);
194                         if (log.isDebugEnabled()) {
195                                 log.debug("Using default Relying Party, " + relyingParty.getName() + " for unauthenticated provider.");
196                         }
197                 } else {
198                         // Identify a Relying Party
199                         log.debug("Mapping authenticated provider (" + effectiveName + ") to Relying Party.");
200                         relyingParty = support.getServiceProviderMapper().getRelyingParty(effectiveName);
201                 }
202
203                 // Fail if we can't honor SAML Subject Confirmation unless the only one supplied is
204                 // bearer, in which case this is probably a Shib 1.1 query, and we'll let it slide for now.
205                 boolean hasConfirmationMethod = false;
206                 Iterator iterator = attributeQuery.getSubject().getConfirmationMethods();
207                 while (iterator.hasNext()) {
208                         String method = (String) iterator.next();
209                         log.info("Request contains SAML Subject Confirmation method: (" + method + ").");
210                         hasConfirmationMethod = true;
211                 }
212                 if (hasConfirmationMethod) {
213                         respondWithError(
214                                         response,
215                                         samlRequest,
216                                         new SAMLException(SAMLException.REQUESTER,
217                                                         "This SAML authority cannot honor requests containing the supplied SAML Subject Confirmation Method(s)."));
218                         return;
219                 }
220
221                 try {
222                         // Map Subject to local principal
223                         Principal principal = null;
224
225                         SAMLNameIdentifier nameId = attributeQuery.getSubject().getNameIdentifier();
226                         log.debug("Name Identifier format: (" + nameId.getFormat() + ").");
227                         NameIdentifierMapping mapping = null;
228                         try {
229                                 mapping = support.getNameMapper().getNameIdentifierMapping(new URI(nameId.getFormat()));
230                         } catch (URISyntaxException e) {
231                                 log.error("Invalid Name Identifier format.");
232                         }
233                         if (mapping == null) { throw new NameIdentifierMappingException("Name Identifier format not registered."); }
234
235                         // Don't honor the request if the active relying party configuration does not contain a mapping with the
236                         // name identifier format from the request
237                         if (!Arrays.asList(relyingParty.getNameMapperIds()).contains(mapping.getId())) { throw new NameIdentifierMappingException(
238                                         "Name Identifier format not valid for this relying party."); }
239
240                         principal = mapping.getPrincipal(nameId, relyingParty, relyingParty.getIdentityProvider());
241                         log.info("Request is for principal (" + principal.getName() + ").");
242
243                         // Get attributes from resolver
244                         Collection<? extends SAMLAttribute> attrs;
245                         Iterator requestedAttrsIterator = attributeQuery.getDesignators();
246                         if (requestedAttrsIterator.hasNext()) {
247                                 log.info("Request designates specific attributes, resolving this set.");
248                                 ArrayList<URI> requestedAttrs = new ArrayList<URI>();
249                                 while (requestedAttrsIterator.hasNext()) {
250                                         SAMLAttributeDesignator attribute = (SAMLAttributeDesignator) requestedAttrsIterator.next();
251                                         try {
252                                                 log.debug("Designated attribute: (" + attribute.getName() + ")");
253                                                 requestedAttrs.add(new URI(attribute.getName()));
254                                         } catch (URISyntaxException use) {
255                                                 log.error("Request designated an attribute name that does not conform "
256                                                                 + "to the required URI syntax (" + attribute.getName() + ").  Ignoring this attribute");
257                                         }
258                                 }
259
260                                 attrs = support.getReleaseAttributes(principal, relyingParty, effectiveName, requestedAttrs);
261                         } else {
262                                 log.info("Request does not designate specific attributes, resolving all available.");
263                                 attrs = support.getReleaseAttributes(principal, relyingParty, effectiveName);
264                         }
265
266                         log.info("Found " + attrs.size() + " 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.size() > 0) {
270                                 StringBuffer attrNameBuffer = new StringBuffer();
271                                 for (SAMLAttribute attr : attrs) {
272                                         attrNameBuffer.append("(" + attr.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.size() == 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<String> audiences = new ArrayList<String>();
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
299                                 SAMLCondition condition = new SAMLAudienceRestrictionCondition(audiences);
300
301                                 // Put all attributes into an assertion
302                                 SAMLStatement statement = new SAMLAttributeStatement(rSubject, attrs);
303
304                                 // Set assertion expiration to longest attribute expiration
305                                 long max = 0;
306                                 for (SAMLAttribute attr : attrs) {
307                                         if (max < attr.getLifetime()) {
308                                                 max = attr.getLifetime();
309                                         }
310                                 }
311                                 Date now = new Date();
312                                 Date then = new Date(now.getTime() + (max * 1000)); // max is in
313                                 // seconds
314
315                                 SAMLAssertion sAssertion = new SAMLAssertion(relyingParty.getIdentityProvider().getProviderId(), now,
316                                                 then, Collections.singleton(condition), null, Collections.singleton(statement));
317
318                                 // Sign the assertions, if necessary
319                                 boolean metaDataIndicatesSignAssertions = false;
320                                 EntityDescriptor descriptor = support.getEntityDescriptor((relyingParty.getProviderId()));
321                                 if (descriptor != null) {
322                                         // TODO pulled this code out for now because we don't have an extension interface to support it.
323                                         // Need to add it back with current draft profile as of release time.
324                                         /*
325                                          * AttributeRequesterDescriptor ar = descriptor
326                                          * .getAttributeRequesterDescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM); if (ar != null) { if
327                                          * (ar.getWantAssertionsSigned()) { metaDataIndicatesSignAssertions = true; } }
328                                          */
329                                         if (!metaDataIndicatesSignAssertions) {
330                                                 SPSSODescriptor sp = descriptor.getSPSSODescriptor(org.opensaml.XML.SAML11_PROTOCOL_ENUM);
331                                                 if (sp != null) {
332                                                         if (sp.getWantAssertionsSigned()) {
333                                                                 metaDataIndicatesSignAssertions = true;
334                                                         }
335                                                 }
336                                         }
337                                 }
338                                 if (relyingParty.wantsAssertionsSigned() || metaDataIndicatesSignAssertions) {
339                                         support.signAssertions(new SAMLAssertion[]{sAssertion}, relyingParty);
340                                 }
341
342                                 samlResponse = new SAMLResponse(samlRequest.getId(), null, Collections.singleton(sAssertion), null);
343                         }
344
345                         if (log.isDebugEnabled()) { // This takes some processing, so only do it if we need to
346                                 log.debug("Dumping generated SAML Response:" + System.getProperty("line.separator")
347                                                 + samlResponse.toString());
348                         }
349
350                         log.info("Successfully created response for principal (" + principal.getName() + ").");
351
352                         if (effectiveName == null) {
353                                 support.getTransactionLog().info(
354                                                 "Attribute assertion issued to anonymous provider at (" + request.getRemoteAddr()
355                                                                 + ") on behalf of principal (" + principal.getName() + ").");
356                         } else {
357                                 support.getTransactionLog().info(
358                                                 "Attribute assertion issued to provider (" + effectiveName + ") on behalf of principal ("
359                                                                 + principal.getName() + ").");
360                         }
361
362                         binding.respond(response, samlResponse, null);
363
364                 } catch (SAMLException e) {
365                         if (relyingParty.passThruErrors()) {
366                                 respondWithError(response, samlRequest, new SAMLException("General error processing request.", e));
367                         } else {
368                                 respondWithError(response, samlRequest, new SAMLException("General error processing request."));
369                         }
370                 } catch (MetadataProviderException e) {
371                         log.error("Encountered an error while looking up metadata: " + e);
372                         if (relyingParty.passThruErrors()) {
373                                 respondWithError(response, samlRequest, new SAMLException("General error processing request.", e));
374                         } else {
375                                 respondWithError(response, samlRequest, new SAMLException("General error processing request."));
376                         }
377
378                 } catch (InvalidNameIdentifierException e) {
379                         log.error("Could not associate the request's subject with a principal: " + e);
380                         if (relyingParty.passThruErrors()) {
381                                 respondWithError(response, samlRequest, new SAMLException(Arrays.asList(e.getSAMLErrorCodes()),
382                                                 "The supplied Subject was unrecognized.", e));
383                         } else {
384                                 respondWithError(response, samlRequest, new SAMLException(Arrays.asList(e.getSAMLErrorCodes()),
385                                                 "The supplied Subject was unrecognized."));
386                         }
387
388                 } catch (NameIdentifierMappingException e) {
389                         log.error("Encountered an error while mapping the name identifier from the request: " + e);
390                         if (relyingParty.passThruErrors()) {
391                                 respondWithError(response, samlRequest, new SAMLException("General error processing request.", e));
392                         } else {
393                                 respondWithError(response, samlRequest, new SAMLException("General error processing request."));
394                         }
395
396                 } catch (AAException e) {
397                         log.error("Encountered an error while resolving resolving attributes: " + e);
398                         if (relyingParty.passThruErrors()) {
399                                 respondWithError(response, samlRequest, new SAMLException("General error processing request.", e));
400                         } else {
401                                 respondWithError(response, samlRequest, new SAMLException("General error processing request."));
402                         }
403
404                 } catch (CloneNotSupportedException e) {
405                         log.error("Encountered an error while cloning request subject for use in response: " + e);
406                         if (relyingParty.passThruErrors()) {
407                                 respondWithError(response, samlRequest, new SAMLException("General error processing request.", e));
408                         } else {
409                                 respondWithError(response, samlRequest, new SAMLException("General error processing request."));
410                         }
411                 }
412         }
413 }