Fix usage of java.text.MessageFormat.
[java-idp.git] / src / main / java / edu / internet2 / middleware / shibboleth / idp / profile / AbstractSAMLProfileHandler.java
index 3627629..f0783fb 100644 (file)
@@ -36,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;
@@ -172,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
@@ -400,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
@@ -458,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)},
@@ -495,20 +683,12 @@ public abstract class AbstractSAMLProfileHandler extends
      */
     protected void encodeResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
         try {
-            SAMLMessageEncoder encoder = getMessageEncoders().get(requestContext.getPeerEntityEndpoint().getBinding());
-            if (encoder == null) {
-                log.error("No outbound message encoder configured for binding {}", requestContext
-                        .getPeerEntityEndpoint().getBinding());
-                throw new ProfileException("No outbound message encoder configured for binding "
-                        + requestContext.getPeerEntityEndpoint().getBinding());
-            }
+            SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
 
             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();
@@ -540,6 +720,105 @@ 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 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).
+     * </p>
+     * 
+     * @param requestContext current request context
+     * @return the message encoder to use
+     * @throws ProfileException if the encoder to use can not be resolved based on the request context
+     */
+    protected SAMLMessageEncoder getOutboundMessageEncoder(BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        SAMLMessageEncoder encoder = null;
+
+        Endpoint endpoint = requestContext.getPeerEntityEndpoint();
+        if (endpoint == null) {
+            log.warn("No peer endpoint available for peer. Unable to send response.");
+            throw new ProfileException("No peer endpoint available for peer. Unable to send response.");
+        }
+
+        if (endpoint != null) {
+            encoder = getMessageEncoders().get(endpoint.getBinding());
+            if (encoder == null) {
+                log.error("No outbound message encoder configured for binding: {}", requestContext
+                        .getPeerEntityEndpoint().getBinding());
+                throw new ProfileException("No outbound message encoder configured for binding: "
+                        + requestContext.getPeerEntityEndpoint().getBinding());
+            }
+        }
+        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>
+     * 
+     * <p>
+     * Subclasses may override to implement a different mechanism to determine the decoder to use.
+     * </p>
+     * 
+     * @param requestContext current request context
+     * @return the message decoder to use
+     * @throws ProfileException if the decoder to use can not be resolved based on the request context
+     */
+    protected SAMLMessageDecoder getInboundMessageDecoder(BaseSAMLProfileRequestContext requestContext)
+            throws ProfileException {
+        SAMLMessageDecoder decoder = null;
+
+        decoder = getMessageDecoders().get(getInboundBinding());
+        if (decoder == null) {
+            log.error("No inbound message decoder configured for binding: {}", getInboundBinding());
+            throw new ProfileException("No inbound message decoder configured for binding: " + getInboundBinding());
+        }
+        return decoder;
+    }
+
+    /**
      * Writes an audit log entry indicating the successful response to the attribute request.
      * 
      * @param context current request context