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;
}
/**
+ * 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
}
/**
+ * 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
}
/**
+ * 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)},
*/
protected void encodeResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
try {
- SAMLMessageEncoder encoder = null;
-
- Endpoint endpoint = requestContext.getPeerEntityEndpoint();
- if (endpoint == null) {
- log.error("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());
- }
- }
+ 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();
}
/**
+ * 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