View Javadoc

1   /*
2    * Licensed to the University Corporation for Advanced Internet Development, 
3    * Inc. (UCAID) under one or more contributor license agreements.  See the 
4    * NOTICE file distributed with this work for additional information regarding
5    * copyright ownership. The UCAID licenses this file to You under the Apache 
6    * License, Version 2.0 (the "License"); you may not use this file except in 
7    * compliance with the License.  You may obtain a copy of the License at
8    *
9    *    http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package edu.internet2.middleware.shibboleth.idp.profile;
19  
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  
27  import javax.servlet.http.HttpServletRequest;
28  
29  import org.opensaml.common.IdentifierGenerator;
30  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
31  import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
32  import org.opensaml.saml1.core.NameIdentifier;
33  import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
34  import org.opensaml.saml2.metadata.AuthnAuthorityDescriptor;
35  import org.opensaml.saml2.metadata.Endpoint;
36  import org.opensaml.saml2.metadata.EntityDescriptor;
37  import org.opensaml.saml2.metadata.NameIDFormat;
38  import org.opensaml.saml2.metadata.PDPDescriptor;
39  import org.opensaml.saml2.metadata.RoleDescriptor;
40  import org.opensaml.saml2.metadata.SSODescriptor;
41  import org.opensaml.saml2.metadata.provider.MetadataProvider;
42  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
43  import org.opensaml.security.MetadataCredentialResolver;
44  import org.opensaml.security.MetadataCredentialResolverFactory;
45  import org.opensaml.ws.message.encoder.MessageEncodingException;
46  import org.opensaml.ws.security.SecurityPolicyResolver;
47  import org.opensaml.ws.transport.InTransport;
48  import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
49  import org.opensaml.xml.security.credential.Credential;
50  import org.opensaml.xml.util.DatatypeHelper;
51  import org.opensaml.xml.util.Pair;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  
55  import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
56  import edu.internet2.middleware.shibboleth.common.attribute.encoding.AttributeEncoder;
57  import edu.internet2.middleware.shibboleth.common.attribute.encoding.SAMLNameIdentifierEncoder;
58  import edu.internet2.middleware.shibboleth.common.log.AuditLogEntry;
59  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
60  import edu.internet2.middleware.shibboleth.common.profile.provider.AbstractShibbolethProfileHandler;
61  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
62  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
63  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartySecurityPolicyResolver;
64  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.AbstractSAMLProfileConfiguration;
65  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.CryptoOperationRequirementLevel;
66  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
67  import edu.internet2.middleware.shibboleth.idp.session.Session;
68  
69  /**
70   * Base class for SAML profile handlers.
71   */
72  public abstract class AbstractSAMLProfileHandler extends
73          AbstractShibbolethProfileHandler<SAMLMDRelyingPartyConfigurationManager, Session> {
74  
75      /** SAML message audit log. */
76      private final Logger auditLog = LoggerFactory.getLogger(AuditLogEntry.AUDIT_LOGGER_NAME);
77  
78      /** Class logger. */
79      private final Logger log = LoggerFactory.getLogger(AbstractSAMLProfileHandler.class);
80  
81      /** Generator of IDs which may be used for SAML assertions, requests, etc. */
82      private IdentifierGenerator idGenerator;
83  
84      /** All the SAML message decoders configured for the IdP. */
85      private Map<String, SAMLMessageDecoder> messageDecoders;
86  
87      /** All the SAML message encoders configured for the IdP. */
88      private Map<String, SAMLMessageEncoder> messageEncoders;
89  
90      /** SAML message binding used by inbound messages. */
91      private String inboundBinding;
92  
93      /** SAML message bindings that may be used by outbound messages. */
94      private List<String> supportedOutboundBindings;
95  
96      /** Resolver used to determine active security policy for an incoming request. */
97      private SecurityPolicyResolver securityPolicyResolver;
98  
99      /** Credential resolver for resolving keys from metadata. */
100     private MetadataCredentialResolver metadataCredentialResolver;
101 
102     /** Constructor. */
103     protected AbstractSAMLProfileHandler() {
104         super();
105     }
106 
107     /**
108      * Gets the resolver used to determine active security policy for an incoming request.
109      * 
110      * @return resolver used to determine active security policy for an incoming request
111      */
112     public SecurityPolicyResolver getSecurityPolicyResolver() {
113         if (securityPolicyResolver == null) {
114             setSecurityPolicyResolver(new RelyingPartySecurityPolicyResolver(getRelyingPartyConfigurationManager()));
115         }
116 
117         return securityPolicyResolver;
118     }
119 
120     /**
121      * Sets the resolver used to determine active security policy for an incoming request.
122      * 
123      * @param resolver resolver used to determine active security policy for an incoming request
124      */
125     public void setSecurityPolicyResolver(SecurityPolicyResolver resolver) {
126         securityPolicyResolver = resolver;
127     }
128 
129     /**
130      * Gets the audit log for this handler.
131      * 
132      * @return audit log for this handler
133      */
134     protected Logger getAduitLog() {
135         return auditLog;
136     }
137 
138     /**
139      * Gets an ID generator which may be used for SAML assertions, requests, etc.
140      * 
141      * @return ID generator
142      */
143     public IdentifierGenerator getIdGenerator() {
144         return idGenerator;
145     }
146 
147     /**
148      * Gets the SAML message binding used by inbound messages.
149      * 
150      * @return SAML message binding used by inbound messages
151      */
152     public String getInboundBinding() {
153         return inboundBinding;
154     }
155 
156     /**
157      * Gets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
158      * 
159      * @return SAML message decoders configured for the IdP indexed by SAML binding URI
160      */
161     public Map<String, SAMLMessageDecoder> getMessageDecoders() {
162         return messageDecoders;
163     }
164 
165     /**
166      * Gets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
167      * 
168      * @return SAML message encoders configured for the IdP indexed by SAML binding URI
169      */
170     public Map<String, SAMLMessageEncoder> getMessageEncoders() {
171         return messageEncoders;
172     }
173 
174     /**
175      * A convenience method for retrieving the SAML metadata provider from the relying party manager.
176      * 
177      * @return the metadata provider or null
178      */
179     public MetadataProvider getMetadataProvider() {
180         SAMLMDRelyingPartyConfigurationManager rpcManager = getRelyingPartyConfigurationManager();
181         if (rpcManager != null) {
182             return rpcManager.getMetadataProvider();
183         }
184 
185         return null;
186     }
187 
188     /**
189      * A convenience method for obtaining a metadata credential resolver for the current metadata provider.
190      * 
191      * @return the metadata credential resolver or null
192      */
193     public MetadataCredentialResolver getMetadataCredentialResolver() {
194         // It's advisable to cache the metadata cred resolver instance from the factory
195         // for the life of the profile handler. See SIDP-428.
196         synchronized (this) {
197             if (metadataCredentialResolver == null) {
198                 MetadataCredentialResolverFactory mcrFactory = MetadataCredentialResolverFactory.getFactory();
199                 MetadataProvider metadataProvider = getMetadataProvider();
200                 metadataCredentialResolver = mcrFactory.getInstance(metadataProvider);
201             }
202         }
203         return metadataCredentialResolver;
204     }
205 
206     /**
207      * Gets the SAML message bindings that may be used by outbound messages.
208      * 
209      * @return SAML message bindings that may be used by outbound messages
210      */
211     public List<String> getSupportedOutboundBindings() {
212         return supportedOutboundBindings;
213     }
214 
215     /**
216      * Gets the user's session, if there is one.
217      * 
218      * @param inTransport current inbound transport
219      * 
220      * @return user's session
221      */
222     protected Session getUserSession(InTransport inTransport) {
223         HttpServletRequest rawRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
224         return (Session) rawRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
225     }
226 
227     /**
228      * Gets the user's session based on their principal name.
229      * 
230      * @param principalName user's principal name
231      * 
232      * @return the user's session
233      */
234     protected Session getUserSession(String principalName) {
235         return getSessionManager().getSession(principalName);
236     }
237 
238     /**
239      * Gets an ID generator which may be used for SAML assertions, requests, etc.
240      * 
241      * @param generator an ID generator which may be used for SAML assertions, requests, etc
242      */
243     public void setIdGenerator(IdentifierGenerator generator) {
244         idGenerator = generator;
245     }
246 
247     /**
248      * Sets the SAML message binding used by inbound messages.
249      * 
250      * @param binding SAML message binding used by inbound messages
251      */
252     public void setInboundBinding(String binding) {
253         inboundBinding = binding;
254     }
255 
256     /**
257      * Sets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
258      * 
259      * @param decoders SAML message decoders configured for the IdP indexed by SAML binding URI
260      */
261     public void setMessageDecoders(Map<String, SAMLMessageDecoder> decoders) {
262         messageDecoders = decoders;
263     }
264 
265     /**
266      * Sets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
267      * 
268      * @param encoders SAML message encoders configured for the IdP indexed by SAML binding URI
269      */
270     public void setMessageEncoders(Map<String, SAMLMessageEncoder> encoders) {
271         messageEncoders = encoders;
272     }
273 
274     /**
275      * Sets the SAML message bindings that may be used by outbound messages.
276      * 
277      * @param bindings SAML message bindings that may be used by outbound messages
278      */
279     public void setSupportedOutboundBindings(List<String> bindings) {
280         supportedOutboundBindings = bindings;
281     }
282 
283     /** {@inheritDoc} */
284     public RelyingPartyConfiguration getRelyingPartyConfiguration(String relyingPartyId) {
285         try {
286             if (getMetadataProvider().getEntityDescriptor(relyingPartyId) == null) {
287                 log.warn("No metadata for relying party {}, treating party as anonymous", relyingPartyId);
288                 return getRelyingPartyConfigurationManager().getAnonymousRelyingConfiguration();
289             }
290         } catch (MetadataProviderException e) {
291             log.error("Unable to look up relying party metadata", e);
292             return null;
293         }
294 
295         return super.getRelyingPartyConfiguration(relyingPartyId);
296     }
297 
298     /**
299      * Populates the request context with information.
300      * 
301      * This method requires the the following request context properties to be populated: inbound message transport,
302      * peer entity ID, metadata provider
303      * 
304      * This methods populates the following request context properties: user's session, user's principal name, service
305      * authentication method, peer entity metadata, relying party configuration, local entity ID, outbound message
306      * issuer, local entity metadata
307      * 
308      * @param requestContext current request context
309      * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
310      */
311     protected void populateRequestContext(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
312         populateRelyingPartyInformation(requestContext);
313         populateAssertingPartyInformation(requestContext);
314         populateSAMLMessageInformation(requestContext);
315         populateProfileInformation(requestContext);
316         populateUserInformation(requestContext);
317     }
318 
319     /**
320      * Populates the request context with information about the relying party.
321      * 
322      * This method requires the the following request context properties to be populated: inbound message issuer
323      * 
324      * This methods populates the following request context properties: peer entityID, peer entity metadata,
325      * relying party configuration
326      * 
327      * @param requestContext current request context
328      * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
329      */
330     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
331             throws ProfileException {
332         MetadataProvider metadataProvider = requestContext.getMetadataProvider();
333         String relyingPartyId = requestContext.getInboundMessageIssuer();
334         requestContext.setPeerEntityId(relyingPartyId);
335 
336         EntityDescriptor relyingPartyMetadata;
337         try {
338             relyingPartyMetadata = metadataProvider.getEntityDescriptor(relyingPartyId);
339             requestContext.setPeerEntityMetadata(relyingPartyMetadata);
340         } catch (MetadataProviderException e) {
341             log.error("Error looking up metadata for relying party " + relyingPartyId, e);
342             throw new ProfileException("Error looking up metadata for relying party " + relyingPartyId);
343         }
344 
345         RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
346         if (rpConfig == null) {
347             log.error("Unable to retrieve relying party configuration data for entity with ID {}", relyingPartyId);
348             throw new ProfileException("Unable to retrieve relying party configuration data for entity with ID "
349                     + relyingPartyId);
350         }
351         requestContext.setRelyingPartyConfiguration(rpConfig);
352     }
353 
354     /**
355      * Populates the request context with information about the asserting party. Unless overridden,
356      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
357      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)} has already been invoked and the
358      * properties it provides are available in the request context.
359      * 
360      * This method requires the the following request context properties to be populated: metadata provider, relying
361      * party configuration
362      * 
363      * This methods populates the following request context properties: local entity ID, outbound message issuer, local
364      * entity metadata
365      * 
366      * @param requestContext current request context
367      * @throws ProfileException thrown if there is a problem looking up the asserting party's metadata
368      */
369     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
370             throws ProfileException {
371         String assertingPartyId = requestContext.getRelyingPartyConfiguration().getProviderId();
372         requestContext.setLocalEntityId(assertingPartyId);
373         requestContext.setOutboundMessageIssuer(assertingPartyId);
374 
375         try {
376             EntityDescriptor localEntityDescriptor = requestContext.getMetadataProvider().getEntityDescriptor(
377                     assertingPartyId);
378             if (localEntityDescriptor != null) {
379                 requestContext.setLocalEntityMetadata(localEntityDescriptor);
380             }
381         } catch (MetadataProviderException e) {
382             log.error("Error looking up metadata for asserting party " + assertingPartyId, e);
383             throw new ProfileException("Error looking up metadata for asserting party " + assertingPartyId);
384         }
385     }
386 
387     /**
388      * Populates the request context with information from the inbound SAML message. Unless overridden,
389      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
390      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},and
391      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
392      * properties they provide are available in the request context.
393      * 
394      * 
395      * @param requestContext current request context
396      * 
397      * @throws ProfileException thrown if there is a problem populating the request context with information
398      */
399     protected abstract void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext)
400             throws ProfileException;
401 
402     /**
403      * Populates the request context with the information about the profile. Unless overridden,
404      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
405      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
406      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)}, and
407      * {@link #populateSAMLMessageInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
408      * properties they provide are available in the request context.
409      * 
410      * This method requires the the following request context properties to be populated: relying party configuration
411      * 
412      * This methods populates the following request context properties: communication profile ID, profile configuration,
413      * outbound message artifact type, peer entity endpoint
414      * 
415      * @param requestContext current request context
416      * 
417      * @throws ProfileException thrown if there is a problem populating the profile information
418      */
419     protected void populateProfileInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
420         AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
421                 .getRelyingPartyConfiguration().getProfileConfiguration(getProfileId());
422         if (profileConfig != null) {
423             requestContext.setProfileConfiguration(profileConfig);
424             requestContext.setOutboundMessageArtifactType(profileConfig.getOutboundArtifactType());
425         }
426 
427         Endpoint endpoint = selectEndpoint(requestContext);
428         if (endpoint == null) {
429             log.error("No return endpoint available for relying party {}", requestContext.getInboundMessageIssuer());
430             throw new ProfileException("No peer endpoint available to which to send SAML response");
431         }
432         requestContext.setPeerEntityEndpoint(endpoint);
433     }
434 
435     /**
436      * Attempts to select the most fitting name identifier attribute, and associated encoder, for a request. If no
437      * attributes for the request subject are available no name identifier is constructed. If a specific name format is
438      * required, as returned by {@link #getRequiredNameIDFormat(BaseSAMLProfileRequestContext)}, then either an
439      * attribute with an encoder supporting that format is selected or an exception is thrown. If no specific format is
440      * required then an attribute supporting a format listed as supported by the relying party is used. If the relying
441      * party does not list any supported formats then any attribute supporting the correct name identifier type is used.
442      * 
443      * @param <T> type of name identifier encoder the attribute must support
444      * @param nameIdEncoderType type of name identifier encoder the attribute must support
445      * @param requestContext the current request context
446      * 
447      * @return the select attribute, and its encoder, to be used to build the name identifier
448      * 
449      * @throws ProfileException thrown if a specific name identifier format was required but not supported
450      */
451     protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoder(
452             Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext) throws ProfileException {
453 
454         Collection<BaseAttribute<?>> principalAttributes;
455         if (requestContext.getAttributes() == null) {
456             principalAttributes = Collections.emptyList();
457         } else {
458             principalAttributes = new ArrayList<BaseAttribute<?>>(requestContext.getAttributes().values());
459         }
460 
461         filterNameIDAttributesByProtocol(principalAttributes, nameIdEncoderType);
462 
463         String requiredNameFormat = DatatypeHelper.safeTrimOrNullString(getRequiredNameIDFormat(requestContext));
464         if (requiredNameFormat != null) {
465             log.debug(
466                     "Attempting to select name identifier attribute for relying party '{}' that requires format '{}'",
467                     requestContext.getInboundMessageIssuer(), requiredNameFormat);
468             filterNameIDAttributesByFormats(principalAttributes, Collections.singleton(requiredNameFormat));
469             if (principalAttributes.isEmpty()) {
470                 String requiredNameFormatErr = "No attribute of principal '" + requestContext.getPrincipalName()
471                         + "' can be encoded in to a NameIdentifier of " + "required format '" + requiredNameFormat
472                         + "' for relying party '" + requestContext.getInboundMessageIssuer() + "'";
473                 log.warn(requiredNameFormatErr);
474                 throw new ProfileException(requiredNameFormatErr);
475             }
476         } else {
477             filterNameIDAttributesByFormats(principalAttributes, getSupportedNameFormats(requestContext));
478         }
479 
480         Pair<BaseAttribute, T> nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(principalAttributes,
481                 nameIdEncoderType, requestContext.getRelyingPartyConfiguration().getNameIdFormatPrecedence());
482         if (nameIdAttributeAndEncoder != null) {
483             log.debug("Name identifier for relying party '{}' will be built from attribute '{}'",
484                     requestContext.getInboundMessageIssuer(), nameIdAttributeAndEncoder.getFirst().getId());
485         } else {
486             log.debug(
487                     "No attributes for principal '{}' support encoding into a supported name identifier format for relying party '{}'",
488                     requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
489         }
490         return nameIdAttributeAndEncoder;
491     }
492 
493     /**
494      * Filters a collection of attributes removing those attributes which do not have an associated encoder of a given
495      * type.
496      * 
497      * @param <T> the type of the encoder
498      * @param attributes the attributes to be filtered, may not contain null values
499      * @param nameIdEncoderType the type of the encoder, may not be null
500      * 
501      * @throws ProfileException
502      */
503     protected <T extends SAMLNameIdentifierEncoder> void filterNameIDAttributesByProtocol(
504             Collection<BaseAttribute<?>> attributes, Class<T> nameIdEncoderType) {
505         if(attributes.isEmpty()){
506             return;
507         }
508         
509         log.debug("Filtering out potential name identifier attributes which can not be encoded by {}",
510                 nameIdEncoderType.getName());
511 
512         BaseAttribute<?> attribute;
513 
514         Iterator<BaseAttribute<?>> attributeItr = attributes.iterator();
515         ATTRIBS: while (attributeItr.hasNext()) {
516             attribute = attributeItr.next();
517             for (AttributeEncoder encoder : attribute.getEncoders()) {
518                 if (encoder == null) {
519                     continue;
520                 }
521 
522                 if (nameIdEncoderType.isInstance(encoder)) {
523                     log.debug("Retaining attribute {} which may be encoded to via {}", attribute.getId(),
524                             nameIdEncoderType.getName());
525                     continue ATTRIBS;
526                 }
527             }
528             log.debug("Removing attribute {}, it can not be encoded via {}", attribute.getId(),
529                     nameIdEncoderType.getName());
530             attributeItr.remove();
531         }
532     }
533 
534     /**
535      * Filters a collection of attributes removing those attributes that can not be encoded in to a name identifier of
536      * an acceptable format.
537      * 
538      * @param attributes the attributes to be filtered, may not contain null values
539      * @param acceptableFormats name identifier formats which are acceptable, a null or empty collection means any
540      *            format is acceptable
541      */
542     protected void filterNameIDAttributesByFormats(Collection<BaseAttribute<?>> attributes,
543             Collection<String> acceptableFormats) {
544         if (attributes.isEmpty() || acceptableFormats == null || acceptableFormats.isEmpty()) {
545             return;
546         }
547 
548         log.debug(
549                 "Filtering out potential name identifier attributes which do not support one of the following formats: {}",
550                 acceptableFormats);
551 
552         BaseAttribute<?> attribute;
553         SAMLNameIdentifierEncoder nameIdEncoder;
554 
555         Iterator<BaseAttribute<?>> attributeItr = attributes.iterator();
556         ATTRIBS: while (attributeItr.hasNext()) {
557             attribute = attributeItr.next();
558             for (AttributeEncoder encoder : attribute.getEncoders()) {
559                 if (encoder == null) {
560                     continue;
561                 }
562 
563                 if (encoder instanceof SAMLNameIdentifierEncoder) {
564                     nameIdEncoder = (SAMLNameIdentifierEncoder) encoder;
565                     if (acceptableFormats.contains(nameIdEncoder.getNameFormat())) {
566 
567                         log.debug("Retaining attribute {} which may be encoded as a name identifier of format {}",
568                                 attribute.getId(), nameIdEncoder.getNameFormat());
569                         continue ATTRIBS;
570                     }
571                 }
572             }
573             log.debug("Removing attribute {}, it can not be encoded in to a name identifier of an acceptable format",
574                     attribute.getId());
575             attributeItr.remove();
576         }
577     }
578 
579     /**
580      * Gets the name identifier format required to be sent back to the relying party.
581      * 
582      * This implementation of this method returns null. Profile handler implementations should override this method if
583      * an incoming request is capable of requiring a specific format.
584      * 
585      * @param requestContext current request context
586      * 
587      * @return the required name ID format or null if no specific format is required
588      */
589     protected String getRequiredNameIDFormat(BaseSAMLProfileRequestContext requestContext) {
590         return null;
591     }
592 
593     /**
594      * Gets the name identifier formats to use when creating identifiers for the relying party.
595      * 
596      * @param requestContext current request context
597      * 
598      * @return list of formats, in preference order, that may be used with the relying party, or an empty list for no
599      *         preference
600      * 
601      * @throws ProfileException thrown if there is a problem determining the name identifier format to use
602      */
603     protected List<String> getSupportedNameFormats(BaseSAMLProfileRequestContext requestContext)
604             throws ProfileException {
605         ArrayList<String> nameFormats = new ArrayList<String>();
606 
607         RoleDescriptor relyingPartyRole = requestContext.getPeerEntityRoleMetadata();
608         if (relyingPartyRole != null) {
609             List<String> relyingPartySupportedFormats = getEntitySupportedFormats(relyingPartyRole);
610             if (relyingPartySupportedFormats != null && !relyingPartySupportedFormats.isEmpty()) {
611                 nameFormats.addAll(relyingPartySupportedFormats);
612             }
613         }
614 
615         // If metadata contains the unspecified name format this means that any format is acceptable
616         if (nameFormats.contains(NameIdentifier.UNSPECIFIED)) {
617             nameFormats.clear();
618         }
619 
620         return nameFormats;
621     }
622 
623     /**
624      * Gets the list of name identifier formats supported for a given role.
625      * 
626      * @param role the role to get the list of supported name identifier formats
627      * 
628      * @return list of supported name identifier formats
629      */
630     protected List<String> getEntitySupportedFormats(RoleDescriptor role) {
631         List<NameIDFormat> nameIDFormats = null;
632 
633         if (role instanceof SSODescriptor) {
634             nameIDFormats = ((SSODescriptor) role).getNameIDFormats();
635         } else if (role instanceof AuthnAuthorityDescriptor) {
636             nameIDFormats = ((AuthnAuthorityDescriptor) role).getNameIDFormats();
637         } else if (role instanceof PDPDescriptor) {
638             nameIDFormats = ((PDPDescriptor) role).getNameIDFormats();
639         } else if (role instanceof AttributeAuthorityDescriptor) {
640             nameIDFormats = ((AttributeAuthorityDescriptor) role).getNameIDFormats();
641         }
642 
643         ArrayList<String> supportedFormats = new ArrayList<String>();
644         if (nameIDFormats != null) {
645             for (NameIDFormat format : nameIDFormats) {
646                 supportedFormats.add(format.getFormat());
647             }
648         }
649 
650         return supportedFormats;
651     }
652 
653     /**
654      * Selects a name identifier attribute from a collection of attributes. If an ordered precedence of name identifier
655      * formats is given then the attribute that produces a name identifier with the highest precedence is selected. If
656      * no precedence is given, or no attribute support a format listed in the precedence list then the first attribute
657      * which can be encoded in to name identifier is chosen.
658      * 
659      * @param <T> type name identifier
660      * @param attributes attributes from which the identifier is selected, may not contain null values
661      * @param nameIdEncoderType encoder to be used to encode the selected attribute
662      * @param formatPrecedence precedence of name identifier formats, may not contain null values
663      * 
664      * @return the attribute to be encoded and the encoder to use
665      */
666     protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoder(
667             Collection<BaseAttribute<?>> attributes, Class<T> nameIdEncoderType, String[] formatPrecedence) {
668         if (attributes.isEmpty()) {
669             return null;
670         }
671         
672         log.debug("Selecting attribute to be encoded as a name identifier by encoder of type {}",
673                 nameIdEncoderType.getName());
674 
675         T nameIdEncoder;
676 
677         if (formatPrecedence != null) {
678             log.debug("Attempting to select name identifier with highest precedence");
679             for (String format : formatPrecedence) {
680                 for (BaseAttribute<?> attribute : attributes) {
681                     for (AttributeEncoder encoder : attribute.getEncoders()) {
682                         if (encoder == null) {
683                             continue;
684                         }
685 
686                         if (nameIdEncoderType.isInstance(encoder)) {
687                             nameIdEncoder = (T) encoder;
688                             if (DatatypeHelper.safeEquals(format, nameIdEncoder.getNameFormat())) {
689                                 return new Pair<BaseAttribute, T>(attribute, nameIdEncoder);
690                             }
691                         }
692                     }
693                 }
694                 log.debug("No attribute can be encoded as a name identifier with format {}", format);
695             }
696             log.debug("No attribute can be encoded in to a name identifer with a format given in the precdence list.");
697         }
698 
699         log.debug("Selecting the first attribute that can be encoded in to a name identifier");
700         BaseAttribute<?> attribute = attributes.iterator().next();
701         for (AttributeEncoder encoder : attribute.getEncoders()) {
702             if (encoder == null) {
703                 continue;
704             }
705 
706             if (nameIdEncoderType.isInstance(encoder)) {
707                 nameIdEncoder = (T) encoder;
708                 return new Pair<BaseAttribute, T>(attribute, nameIdEncoder);
709             }
710         }
711 
712         return null;
713     }
714 
715     /**
716      * Populates the request context with the information about the user if they have an existing session. Unless
717      * overridden, {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
718      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
719      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)},
720      * {@link #populateProfileInformation(BaseSAMLProfileRequestContext)}, and
721      * {@link #populateSAMLMessageInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
722      * properties they provide are available in the request context.
723      * 
724      * This method should populate: user's session, user's principal name, and service authentication method
725      * 
726      * @param requestContext current request context
727      * 
728      * @throws ProfileException thrown if there is a problem populating the user's information
729      */
730     protected abstract void populateUserInformation(BaseSAMLProfileRequestContext requestContext)
731             throws ProfileException;
732 
733     /**
734      * Selects the appropriate endpoint for the relying party and stores it in the request context.
735      * 
736      * @param requestContext current request context
737      * 
738      * @return Endpoint selected from the information provided in the request context
739      * 
740      * @throws ProfileException thrown if there is a problem selecting a response endpoint
741      */
742     protected abstract Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) throws ProfileException;
743 
744     /**
745      * Encodes the request's SAML response and writes it to the servlet response.
746      * 
747      * @param requestContext current request context
748      * 
749      * @throws ProfileException thrown if no message encoder is registered for this profiles binding
750      */
751     protected void encodeResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
752         try {
753             SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
754 
755             AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
756                     .getProfileConfiguration();
757             if (profileConfig != null) {
758                 if (isSignResponse(requestContext)) {
759                     Credential signingCredential = profileConfig.getSigningCredential();
760                     if (signingCredential == null) {
761                         signingCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
762                     }
763 
764                     if (signingCredential == null) {
765                         throw new ProfileException(
766                                 "Signing of responses is required but no signing credential is available");
767                     }
768 
769                     if (signingCredential.getPrivateKey() == null) {
770                         throw new ProfileException(
771                                 "Signing of response is required but signing credential does not have a private key");
772                     }
773 
774                     requestContext.setOutboundSAMLMessageSigningCredential(signingCredential);
775                 }
776             }
777 
778             log.debug("Encoding response to SAML request {} from relying party {}",
779                     requestContext.getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
780 
781             requestContext.setMessageEncoder(encoder);
782             encoder.encode(requestContext);
783         } catch (MessageEncodingException e) {
784             throw new ProfileException("Unable to encode response to relying party: "
785                     + requestContext.getInboundMessageIssuer(), e);
786         }
787     }
788 
789     /**
790      * Determine whether responses should be signed.
791      * 
792      * @param requestContext the current request context
793      * @return true if responses should be signed, false otherwise
794      * @throws ProfileException if there is a problem determining whether responses should be signed
795      */
796     protected boolean isSignResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
797 
798         SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
799 
800         AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
801                 .getProfileConfiguration();
802 
803         if (profileConfig != null) {
804             try {
805                 return profileConfig.getSignResponses() == CryptoOperationRequirementLevel.always
806                         || (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.conditional && !encoder
807                                 .providesMessageIntegrity(requestContext));
808             } catch (MessageEncodingException e) {
809                 log.error("Unable to determine if outbound encoding '{}' provides message integrity protection",
810                         encoder.getBindingURI());
811                 throw new ProfileException("Unable to determine if outbound response should be signed");
812             }
813         } else {
814             return false;
815         }
816 
817     }
818 
819     /**
820      * Get the outbound message encoder to use.
821      * 
822      * <p>
823      * The default implementation uses the binding URI from the
824      * {@link org.opensaml.common.binding.SAMLMessageContext#getPeerEntityEndpoint()} to lookup the encoder from the
825      * supported message encoders defined in {@link #getMessageEncoders()}.
826      * </p>
827      * 
828      * <p>
829      * Subclasses may override to implement a different mechanism to determine the encoder to use, such as for example
830      * cases where an active intermediary actor sits between this provider and the peer entity endpoint (e.g. the SAML 2
831      * ECP case).
832      * </p>
833      * 
834      * @param requestContext current request context
835      * @return the message encoder to use
836      * @throws ProfileException if the encoder to use can not be resolved based on the request context
837      */
838     protected SAMLMessageEncoder getOutboundMessageEncoder(BaseSAMLProfileRequestContext requestContext)
839             throws ProfileException {
840         SAMLMessageEncoder encoder = null;
841 
842         Endpoint endpoint = requestContext.getPeerEntityEndpoint();
843         if (endpoint == null) {
844             log.warn("No peer endpoint available for peer. Unable to send response.");
845             throw new ProfileException("No peer endpoint available for peer. Unable to send response.");
846         }
847 
848         if (endpoint != null) {
849             encoder = getMessageEncoders().get(endpoint.getBinding());
850             if (encoder == null) {
851                 log.error("No outbound message encoder configured for binding: {}", requestContext
852                         .getPeerEntityEndpoint().getBinding());
853                 throw new ProfileException("No outbound message encoder configured for binding: "
854                         + requestContext.getPeerEntityEndpoint().getBinding());
855             }
856         }
857         return encoder;
858     }
859 
860     /**
861      * Get the inbound message decoder to use.
862      * 
863      * <p>
864      * The default implementation uses the binding URI from {@link #getInboundBinding()} to lookup the decoder from the
865      * supported message decoders defined in {@link #getMessageDecoders()}.
866      * </p>
867      * 
868      * <p>
869      * Subclasses may override to implement a different mechanism to determine the decoder to use.
870      * </p>
871      * 
872      * @param requestContext current request context
873      * @return the message decoder to use
874      * @throws ProfileException if the decoder to use can not be resolved based on the request context
875      */
876     protected SAMLMessageDecoder getInboundMessageDecoder(BaseSAMLProfileRequestContext requestContext)
877             throws ProfileException {
878         SAMLMessageDecoder decoder = null;
879 
880         decoder = getMessageDecoders().get(getInboundBinding());
881         if (decoder == null) {
882             log.error("No inbound message decoder configured for binding: {}", getInboundBinding());
883             throw new ProfileException("No inbound message decoder configured for binding: " + getInboundBinding());
884         }
885         return decoder;
886     }
887 
888     /**
889      * Writes an audit log entry indicating the successful response to the attribute request.
890      * 
891      * @param context current request context
892      */
893     protected void writeAuditLogEntry(BaseSAMLProfileRequestContext context) {
894         AuditLogEntry auditLogEntry = new AuditLogEntry();
895         auditLogEntry.setMessageProfile(getProfileId());
896         auditLogEntry.setPrincipalAuthenticationMethod(context.getPrincipalAuthenticationMethod());
897         auditLogEntry.setPrincipalName(context.getPrincipalName());
898         auditLogEntry.setAssertingPartyId(context.getLocalEntityId());
899         auditLogEntry.setRelyingPartyId(context.getInboundMessageIssuer());
900         auditLogEntry.setRequestBinding(context.getMessageDecoder().getBindingURI());
901         auditLogEntry.setRequestId(context.getInboundSAMLMessageId());
902         auditLogEntry.setResponseBinding(context.getMessageEncoder().getBindingURI());
903         auditLogEntry.setResponseId(context.getOutboundSAMLMessageId());
904         if (context.getReleasedAttributes() != null) {
905             auditLogEntry.getReleasedAttributes().addAll(context.getReleasedAttributes());
906         }
907 
908         getAduitLog().info(auditLogEntry.toString());
909     }
910 }