Fix usage of java.text.MessageFormat.
[java-idp.git] / src / main / java / edu / internet2 / middleware / shibboleth / idp / profile / AbstractSAMLProfileHandler.java
index 6e37287..f0783fb 100644 (file)
@@ -23,7 +23,6 @@ import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 
 import org.opensaml.common.IdentifierGenerator;
-import org.opensaml.common.binding.SAMLMessageContext;
 import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
 import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
 import org.opensaml.saml1.core.NameIdentifier;
@@ -37,14 +36,21 @@ import org.opensaml.saml2.metadata.RoleDescriptor;
 import org.opensaml.saml2.metadata.SSODescriptor;
 import org.opensaml.saml2.metadata.provider.MetadataProvider;
 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
+import org.opensaml.security.MetadataCredentialResolver;
+import org.opensaml.security.MetadataCredentialResolverFactory;
 import org.opensaml.ws.message.encoder.MessageEncodingException;
 import org.opensaml.ws.security.SecurityPolicyResolver;
 import org.opensaml.ws.transport.InTransport;
 import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
 import org.opensaml.xml.security.credential.Credential;
+import org.opensaml.xml.util.DatatypeHelper;
+import org.opensaml.xml.util.Pair;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
+import edu.internet2.middleware.shibboleth.common.attribute.encoding.AttributeEncoder;
+import edu.internet2.middleware.shibboleth.common.attribute.encoding.SAMLNameIdentifierEncoder;
 import edu.internet2.middleware.shibboleth.common.log.AuditLogEntry;
 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
 import edu.internet2.middleware.shibboleth.common.profile.provider.AbstractShibbolethProfileHandler;
@@ -173,6 +179,17 @@ public abstract class AbstractSAMLProfileHandler extends
     }
 
     /**
+     * A convenience method for obtaining a metadata credential resolver for the current metadata provider.
+     *
+     * @return the metadata credential resolver or null
+     */
+    public MetadataCredentialResolver getMetadataCredentialResolver() {
+        MetadataCredentialResolverFactory mcrFactory = MetadataCredentialResolverFactory.getFactory();
+        MetadataProvider metadataProvider = getMetadataProvider();
+        return mcrFactory.getInstance(metadataProvider);
+    }
+
+    /**
      * Gets the SAML message bindings that may be used by outbound messages.
      * 
      * @return SAML message bindings that may be used by outbound messages
@@ -401,6 +418,96 @@ public abstract class AbstractSAMLProfileHandler extends
     }
 
     /**
+     * Attempts to select the most fitting name identifier attribute, and associated encoder, for a request. If no
+     * attributes for the request subject are available no name identifier is constructed. If a specific name format is
+     * required, as returned by {@link #getRequiredNameIDFormat(BaseSAMLProfileRequestContext)}, then either an
+     * attribute with an encoder supporting that format is selected or an exception is thrown. If no specific format is
+     * required then an attribute supporting a format listed as supported by the relying party is used. If the relying
+     * party does not list any supported formats then any attribute supporting the correct name identifier type is used.
+     * 
+     * @param <T> type of name identifier encoder the attribute must support
+     * @param nameIdEncoderType type of name identifier encoder the attribute must support
+     * @param requestContext the current request context
+     * 
+     * @return the select attribute, and its encoder, to be used to build the name identifier
+     * 
+     * @throws ProfileException thrown if a specific name identifier format was required but not supported
+     */
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoder(
+            Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext) throws ProfileException {
+
+        String requiredNameFormat = DatatypeHelper.safeTrimOrNullString(getRequiredNameIDFormat(requestContext));
+        if (requiredNameFormat != null) {
+            log.debug("Attempting to build name identifier for relying party'{}' that requires format '{}'",
+                    requestContext.getInboundMessageIssuer(), requiredNameFormat);
+            return selectNameIDAttributeAndEncoderByRequiredFormat(requiredNameFormat, nameIdEncoderType,
+                    requestContext);
+        }
+
+        List<String> supportedNameFormats = getNameFormats(requestContext);
+        if (supportedNameFormats.isEmpty()) {
+            log.debug("Attempting to build name identifier for relying party '{}' that supports any format",
+                    requestContext.getInboundMessageIssuer());
+        } else {
+            log.debug("Attempting to build name identifier for relying party '{}' that supports the formats: {}",
+                    requestContext.getInboundMessageIssuer(), supportedNameFormats);
+        }
+        return selectNameIDAttributeAndEncoderBySupportedFormats(supportedNameFormats, nameIdEncoderType,
+                requestContext);
+    }
+
+    /**
+     * Gets the name identifier format required to be sent back to the relying party.
+     * 
+     * This implementation of this method returns null. Profile handler implementations should override this method if
+     * an incoming request is capable of requiring a specific format.
+     * 
+     * @param requestContext current request context
+     * 
+     * @return the required name ID format or null if no specific format is required
+     */
+    protected String getRequiredNameIDFormat(BaseSAMLProfileRequestContext requestContext) {
+        return null;
+    }
+
+    /**
+     * Selects the principal attribute that can be encoded in to the required name identifier format.
+     * 
+     * @param <T> type of name identifier encoder the attribute must support
+     * @param requiredNameFormat required name identifier format type
+     * @param nameIdEncoderType type of name identifier encoder the attribute must support
+     * @param requestContext the current request context
+     * 
+     * @return the select attribute, and its encoder, to be used to build the name identifier
+     * 
+     * @throws ProfileException thrown if a specific name identifier format was required but not supported
+     */
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderByRequiredFormat(
+            String requiredNameFormat, Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        String requiredNameFormatErr = "No attribute of principal '" + requestContext.getPrincipalName()
+                + "' can be encoded in to a NameIdentifier of " + "required format '" + requiredNameFormat
+                + "' for relying party '" + requestContext.getInboundMessageIssuer() + "'";
+
+        Map<String, BaseAttribute> principalAttributes = requestContext.getAttributes();
+        if (principalAttributes == null || principalAttributes.isEmpty()) {
+            log.debug("No attributes for principal '{}', no name identifier will be created for relying party '{}'",
+                    requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
+            log.warn(requiredNameFormatErr);
+            throw new ProfileException(requiredNameFormatErr);
+        }
+
+        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(nameIdEncoderType,
+                principalAttributes, java.util.Collections.singletonList(requiredNameFormat));
+        if (nameIdAttributeAndEncoder == null) {
+            log.warn(requiredNameFormatErr);
+            throw new ProfileException(requiredNameFormatErr);
+        }
+
+        return nameIdAttributeAndEncoder;
+    }
+
+    /**
      * Gets the name identifier formats to use when creating identifiers for the relying party.
      * 
      * @param requestContext current request context
@@ -459,6 +566,86 @@ public abstract class AbstractSAMLProfileHandler extends
     }
 
     /**
+     * Selects the principal attribute that can be encoded in to one of the supported name identifier formats.
+     * 
+     * @param <T> type of name identifier encoder the attribute must support
+     * @param supportedNameFormats name identifier formats supported by the relaying part, or an empty list if all
+     *            formats are supported
+     * @param nameIdEncoderType type of name identifier encoder the attribute must support
+     * @param requestContext the current request context
+     * 
+     * @return the select attribute, and its encoder, to be used to build the name identifier
+     * 
+     * @throws ProfileException thrown if there is a problem selecting the attribute
+     */
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoderBySupportedFormats(
+            List<String> supportedNameFormats, Class<T> nameIdEncoderType, BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        Map<String, BaseAttribute> principalAttributes = requestContext.getAttributes();
+        if (principalAttributes == null || principalAttributes.isEmpty()) {
+            log.debug("No attributes for principal '{}', no name identifier will be created for relying party '{}'",
+                    requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
+            return null;
+        }
+
+        Pair<BaseAttribute, T> nameIdAttributeAndEncoder = null;
+        nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(nameIdEncoderType, principalAttributes,
+                supportedNameFormats);
+        if (nameIdAttributeAndEncoder == null) {
+            log
+                    .debug(
+                            "No attributes for principal '{}' support encoding into a supported name identifier format for relying party '{}'",
+                            requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
+        }
+
+        return nameIdAttributeAndEncoder;
+    }
+
+    /**
+     * Selects an attribute, resolved previously, to encode as a NameID.
+     * 
+     * @param <T> type of name identifier encoder the attribute must support
+     * @param nameIdEncoderType type of name identifier encoder the attribute must support
+     * @param principalAttributes resolved attributes
+     * @param supportedNameFormats NameID formats supported by the relying party or an empty list if all formats are
+     *            acceptable
+     * 
+     * @return the attribute and its associated NameID encoder
+     * 
+     * @throws ProfileException thrown if no attribute can be encoded in to a NameID of the required type
+     */
+    protected <T extends SAMLNameIdentifierEncoder> Pair<BaseAttribute, T> selectNameIDAttributeAndEncoder(
+            Class<T> nameIdEncoderType, Map<String, BaseAttribute> principalAttributes,
+            List<String> supportedNameFormats) throws ProfileException {
+
+        T nameIdEncoder = null;
+
+        if (principalAttributes != null) {
+            for (BaseAttribute<?> attribute : principalAttributes.values()) {
+                if (attribute == null) {
+                    continue;
+                }
+
+                for (AttributeEncoder encoder : attribute.getEncoders()) {
+                    if (encoder == null) {
+                        continue;
+                    }
+
+                    if (nameIdEncoderType.isInstance(encoder)) {
+                        nameIdEncoder = nameIdEncoderType.cast(encoder);
+                        if (supportedNameFormats.isEmpty()
+                                || supportedNameFormats.contains(nameIdEncoder.getNameFormat())) {
+                            return new Pair<BaseAttribute, T>(attribute, nameIdEncoder);
+                        }
+                    }
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
      * Populates the request context with the information about the user if they have an existing session. Unless
      * overridden, {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
@@ -501,9 +688,7 @@ public abstract class AbstractSAMLProfileHandler extends
             AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
                     .getProfileConfiguration();
             if (profileConfig != null) {
-                if (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.always
-                        || (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.conditional && !encoder
-                                .providesMessageIntegrity(requestContext))) {
+                if (isSignResponse(requestContext)) {
                     Credential signingCredential = profileConfig.getSigningCredential();
                     if (signingCredential == null) {
                         signingCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
@@ -535,17 +720,48 @@ public abstract class AbstractSAMLProfileHandler extends
     }
 
     /**
+     * Determine whether responses should be signed.
+     * 
+     * @param requestContext the current request context
+     * @return true if responses should be signed, false otherwise
+     * @throws ProfileException if there is a problem determining whether responses should be signed
+     */
+    protected boolean isSignResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
+
+        SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
+
+        AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
+                .getProfileConfiguration();
+
+        if (profileConfig != null) {
+            try {
+                return profileConfig.getSignResponses() == CryptoOperationRequirementLevel.always
+                        || (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.conditional && !encoder
+                                .providesMessageIntegrity(requestContext));
+            } catch (MessageEncodingException e) {
+                log.error("Unable to determine if outbound encoding '{}' provides message integrity protection",
+                        encoder.getBindingURI());
+                throw new ProfileException("Unable to determine if outbound response should be signed");
+            }
+        } else {
+            return false;
+        }
+
+    }
+
+    /**
      * Get the outbound message encoder to use.
      * 
-     * <p>The default implementation uses the binding URI from the 
-     * {@link SAMLMessageContext#getPeerEntityEndpoint()} to lookup
-     * the encoder from the supported message encoders defined in {@link #getMessageEncoders()}.
+     * <p>
+     * The default implementation uses the binding URI from the
+     * {@link org.opensaml.common.binding.SAMLMessageContext#getPeerEntityEndpoint()} to lookup the encoder from the
+     * supported message encoders defined in {@link #getMessageEncoders()}.
      * </p>
      * 
      * <p>
-     * Subclasses may override to implement a different mechanism to determine the 
-     * encoder to use, such as for example cases where an active intermediary actor
-     * sits between this provider and the peer entity endpoint (e.g. the SAML 2 ECP case).
+     * Subclasses may override to implement a different mechanism to determine the encoder to use, such as for example
+     * cases where an active intermediary actor sits between this provider and the peer entity endpoint (e.g. the SAML 2
+     * ECP case).
      * </p>
      * 
      * @param requestContext current request context
@@ -573,18 +789,17 @@ public abstract class AbstractSAMLProfileHandler extends
         }
         return encoder;
     }
-    
+
     /**
      * Get the inbound message decoder to use.
      * 
-     * <p>The default implementation uses the binding URI from
-     * {@link #getInboundBinding()} to lookup the decoder from the supported message decoders
-     * defined in {@link #getMessageDecoders()}.
+     * <p>
+     * The default implementation uses the binding URI from {@link #getInboundBinding()} to lookup the decoder from the
+     * supported message decoders defined in {@link #getMessageDecoders()}.
      * </p>
      * 
      * <p>
-     * Subclasses may override to implement a different mechanism to determine the 
-     * decoder to use.
+     * Subclasses may override to implement a different mechanism to determine the decoder to use.
      * </p>
      * 
      * @param requestContext current request context