View Javadoc

    1   /*
    2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
    3    *
    4    * Licensed under the Apache License, Version 2.0 (the "License");
    5    * you may not use this file except in compliance with the License.
    6    * You may obtain a copy of the License at
    7    *
    8    * http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16
    17  package edu.internet2.middleware.shibboleth.idp.profile;
    18
    19  import java.util.ArrayList;
    20  import java.util.List;
    21  import java.util.Map;
    22
    23  import javax.servlet.http.HttpServletRequest;
    24
    25  import org.opensaml.common.IdentifierGenerator;
    26  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
    27  import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
    28  import org.opensaml.saml1.core.NameIdentifier;
    29  import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
    30  import org.opensaml.saml2.metadata.AuthnAuthorityDescriptor;
    31  import org.opensaml.saml2.metadata.Endpoint;
    32  import org.opensaml.saml2.metadata.EntityDescriptor;
    33  import org.opensaml.saml2.metadata.NameIDFormat;
    34  import org.opensaml.saml2.metadata.PDPDescriptor;
    35  import org.opensaml.saml2.metadata.RoleDescriptor;
    36  import org.opensaml.saml2.metadata.SSODescriptor;
    37  import org.opensaml.saml2.metadata.provider.MetadataProvider;
    38  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
    39  import org.opensaml.ws.message.encoder.MessageEncodingException;
    40  import org.opensaml.ws.security.SecurityPolicyResolver;
    41  import org.opensaml.ws.transport.InTransport;
    42  import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
    43  import org.opensaml.xml.security.credential.Credential;
    44  import org.slf4j.Logger;
    45  import org.slf4j.LoggerFactory;
    46
    47  import edu.internet2.middleware.shibboleth.common.log.AuditLogEntry;
    48  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
    49  import edu.internet2.middleware.shibboleth.common.profile.provider.AbstractShibbolethProfileHandler;
    50  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
    51  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
    52  import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartySecurityPolicyResolver;
    53  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.AbstractSAMLProfileConfiguration;
    54  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.CryptoOperationRequirementLevel;
    55  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
    56  import edu.internet2.middleware.shibboleth.idp.session.Session;
    57
    58  /**
    59   * Base class for SAML profile handlers.
    60   */
    61  public abstract class AbstractSAMLProfileHandler extends
    62          AbstractShibbolethProfileHandler<SAMLMDRelyingPartyConfigurationManager, Session> {
    63
    64      /** SAML message audit log. */
    65      private final Logger auditLog = LoggerFactory.getLogger(AuditLogEntry.AUDIT_LOGGER_NAME);
    66
    67      /** Class logger. */
    68      private final Logger log = LoggerFactory.getLogger(AbstractSAMLProfileHandler.class);
    69
    70      /** Generator of IDs which may be used for SAML assertions, requests, etc. */
    71      private IdentifierGenerator idGenerator;
    72
    73      /** All the SAML message decoders configured for the IdP. */
    74      private Map<String, SAMLMessageDecoder> messageDecoders;
    75
    76      /** All the SAML message encoders configured for the IdP. */
    77      private Map<String, SAMLMessageEncoder> messageEncoders;
    78
    79      /** SAML message binding used by inbound messages. */
    80      private String inboundBinding;
    81
    82      /** SAML message bindings that may be used by outbound messages. */
    83      private List<String> supportedOutboundBindings;
    84
    85      /** Resolver used to determine active security policy for an incoming request. */
    86      private SecurityPolicyResolver securityPolicyResolver;
    87
    88      /** Constructor. */
    89      protected AbstractSAMLProfileHandler() {
    90          super();
    91      }
    92
    93      /**
    94       * Gets the resolver used to determine active security policy for an incoming request.
    95       *
    96       * @return resolver used to determine active security policy for an incoming request
    97       */
    98      public SecurityPolicyResolver getSecurityPolicyResolver() {
    99          if (securityPolicyResolver == null) {
    100             setSecurityPolicyResolver(new RelyingPartySecurityPolicyResolver(getRelyingPartyConfigurationManager()));
    101         }
    102
    103         return securityPolicyResolver;
    104     }
    105
    106     /**
    107      * Sets the resolver used to determine active security policy for an incoming request.
    108      *
    109      * @param resolver resolver used to determine active security policy for an incoming request
    110      */
    111     public void setSecurityPolicyResolver(SecurityPolicyResolver resolver) {
    112         securityPolicyResolver = resolver;
    113     }
    114
    115     /**
    116      * Gets the audit log for this handler.
    117      *
    118      * @return audit log for this handler
    119      */
    120     protected Logger getAduitLog() {
    121         return auditLog;
    122     }
    123
    124     /**
    125      * Gets an ID generator which may be used for SAML assertions, requests, etc.
    126      *
    127      * @return ID generator
    128      */
    129     public IdentifierGenerator getIdGenerator() {
    130         return idGenerator;
    131     }
    132
    133     /**
    134      * Gets the SAML message binding used by inbound messages.
    135      *
    136      * @return SAML message binding used by inbound messages
    137      */
    138     public String getInboundBinding() {
    139         return inboundBinding;
    140     }
    141
    142     /**
    143      * Gets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
    144      *
    145      * @return SAML message decoders configured for the IdP indexed by SAML binding URI
    146      */
    147     public Map<String, SAMLMessageDecoder> getMessageDecoders() {
    148         return messageDecoders;
    149     }
    150
    151     /**
    152      * Gets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
    153      *
    154      * @return SAML message encoders configured for the IdP indexed by SAML binding URI
    155      */
    156     public Map<String, SAMLMessageEncoder> getMessageEncoders() {
    157         return messageEncoders;
    158     }
    159
    160     /**
    161      * A convenience method for retrieving the SAML metadata provider from the relying party manager.
    162      *
    163      * @return the metadata provider or null
    164      */
    165     public MetadataProvider getMetadataProvider() {
    166         SAMLMDRelyingPartyConfigurationManager rpcManager = getRelyingPartyConfigurationManager();
    167         if (rpcManager != null) {
    168             return rpcManager.getMetadataProvider();
    169         }
    170
    171         return null;
    172     }
    173
    174     /**
    175      * Gets the SAML message bindings that may be used by outbound messages.
    176      *
    177      * @return SAML message bindings that may be used by outbound messages
    178      */
    179     public List<String> getSupportedOutboundBindings() {
    180         return supportedOutboundBindings;
    181     }
    182
    183     /**
    184      * Gets the user's session, if there is one.
    185      *
    186      * @param inTransport current inbound transport
    187      *
    188      * @return user's session
    189      */
    190     protected Session getUserSession(InTransport inTransport) {
    191         HttpServletRequest rawRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
    192         return (Session) rawRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
    193     }
    194
    195     /**
    196      * Gets the user's session based on their principal name.
    197      *
    198      * @param principalName user's principal name
    199      *
    200      * @return the user's session
    201      */
    202     protected Session getUserSession(String principalName) {
    203         return getSessionManager().getSession(principalName);
    204     }
    205
    206     /**
    207      * Gets an ID generator which may be used for SAML assertions, requests, etc.
    208      *
    209      * @param generator an ID generator which may be used for SAML assertions, requests, etc
    210      */
    211     public void setIdGenerator(IdentifierGenerator generator) {
    212         idGenerator = generator;
    213     }
    214
    215     /**
    216      * Sets the SAML message binding used by inbound messages.
    217      *
    218      * @param binding SAML message binding used by inbound messages
    219      */
    220     public void setInboundBinding(String binding) {
    221         inboundBinding = binding;
    222     }
    223
    224     /**
    225      * Sets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
    226      *
    227      * @param decoders SAML message decoders configured for the IdP indexed by SAML binding URI
    228      */
    229     public void setMessageDecoders(Map<String, SAMLMessageDecoder> decoders) {
    230         messageDecoders = decoders;
    231     }
    232
    233     /**
    234      * Sets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
    235      *
    236      * @param encoders SAML message encoders configured for the IdP indexed by SAML binding URI
    237      */
    238     public void setMessageEncoders(Map<String, SAMLMessageEncoder> encoders) {
    239         messageEncoders = encoders;
    240     }
    241
    242     /**
    243      * Sets the SAML message bindings that may be used by outbound messages.
    244      *
    245      * @param bindings SAML message bindings that may be used by outbound messages
    246      */
    247     public void setSupportedOutboundBindings(List<String> bindings) {
    248         supportedOutboundBindings = bindings;
    249     }
    250
    251     /** {@inheritDoc} */
    252     public RelyingPartyConfiguration getRelyingPartyConfiguration(String relyingPartyId) {
    253         try {
    254             if (getMetadataProvider().getEntityDescriptor(relyingPartyId) == null) {
    255                 log.warn("No metadata for relying party {}, treating party as anonymous", relyingPartyId);
    256                 return getRelyingPartyConfigurationManager().getAnonymousRelyingConfiguration();
    257             }
    258         } catch (MetadataProviderException e) {
    259             log.error("Unable to look up relying party metadata", e);
    260             return null;
    261         }
    262
    263         return super.getRelyingPartyConfiguration(relyingPartyId);
    264     }
    265
    266     /**
    267      * Populates the request context with information.
    268      *
    269      * This method requires the the following request context properties to be populated: inbound message transport,
    270      * peer entity ID, metadata provider
    271      *
    272      * This methods populates the following request context properties: user's session, user's principal name, service
    273      * authentication method, peer entity metadata, relying party configuration, local entity ID, outbound message
    274      * issuer, local entity metadata
    275      *
    276      * @param requestContext current request context
    277      * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
    278      */
    279     protected void populateRequestContext(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    280         populateRelyingPartyInformation(requestContext);
    281         populateAssertingPartyInformation(requestContext);
    282         populateSAMLMessageInformation(requestContext);
    283         populateProfileInformation(requestContext);
    284         populateUserInformation(requestContext);
    285     }
    286
    287     /**
    288      * Populates the request context with information about the relying party.
    289      *
    290      * This method requires the the following request context properties to be populated: peer entity ID
    291      *
    292      * This methods populates the following request context properties: peer entity metadata, relying party
    293      * configuration
    294      *
    295      * @param requestContext current request context
    296      * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
    297      */
    298     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    299             throws ProfileException {
    300         MetadataProvider metadataProvider = requestContext.getMetadataProvider();
    301         String relyingPartyId = requestContext.getInboundMessageIssuer();
    302
    303         EntityDescriptor relyingPartyMetadata;
    304         try {
    305             relyingPartyMetadata = metadataProvider.getEntityDescriptor(relyingPartyId);
    306             requestContext.setPeerEntityMetadata(relyingPartyMetadata);
    307         } catch (MetadataProviderException e) {
    308             log.error("Error looking up metadata for relying party " + relyingPartyId, e);
    309             throw new ProfileException("Error looking up metadata for relying party " + relyingPartyId);
    310         }
    311
    312         RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
    313         if (rpConfig == null) {
    314             log.error("Unable to retrieve relying party configuration data for entity with ID {}", relyingPartyId);
    315             throw new ProfileException("Unable to retrieve relying party configuration data for entity with ID "
    316                     + relyingPartyId);
    317         }
    318         requestContext.setRelyingPartyConfiguration(rpConfig);
    319     }
    320
    321     /**
    322      * Populates the request context with information about the asserting party. Unless overridden,
    323      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
    324      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)} has already been invoked and the
    325      * properties it provides are available in the request context.
    326      *
    327      * This method requires the the following request context properties to be populated: metadata provider, relying
    328      * party configuration
    329      *
    330      * This methods populates the following request context properties: local entity ID, outbound message issuer, local
    331      * entity metadata
    332      *
    333      * @param requestContext current request context
    334      * @throws ProfileException thrown if there is a problem looking up the asserting party's metadata
    335      */
    336     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
    337             throws ProfileException {
    338         String assertingPartyId = requestContext.getRelyingPartyConfiguration().getProviderId();
    339         requestContext.setLocalEntityId(assertingPartyId);
    340         requestContext.setOutboundMessageIssuer(assertingPartyId);
    341
    342         try {
    343             EntityDescriptor localEntityDescriptor = requestContext.getMetadataProvider().getEntityDescriptor(
    344                     assertingPartyId);
    345             if (localEntityDescriptor != null) {
    346                 requestContext.setLocalEntityMetadata(localEntityDescriptor);
    347             }
    348         } catch (MetadataProviderException e) {
    349             log.error("Error looking up metadata for asserting party " + assertingPartyId, e);
    350             throw new ProfileException("Error looking up metadata for asserting party " + assertingPartyId);
    351         }
    352     }
    353
    354     /**
    355      * Populates the request context with information from the inbound SAML message. Unless overridden,
    356      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
    357      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},and
    358      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
    359      * properties they provide are available in the request context.
    360      *
    361      *
    362      * @param requestContext current request context
    363      *
    364      * @throws ProfileException thrown if there is a problem populating the request context with information
    365      */
    366     protected abstract void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext)
    367             throws ProfileException;
    368
    369     /**
    370      * Populates the request context with the information about the profile. Unless overridden,
    371      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
    372      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
    373      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)}, and
    374      * {@link #populateSAMLMessageInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
    375      * properties they provide are available in the request context.
    376      *
    377      * This method requires the the following request context properties to be populated: relying party configuration
    378      *
    379      * This methods populates the following request context properties: communication profile ID, profile configuration,
    380      * outbound message artifact type, peer entity endpoint
    381      *
    382      * @param requestContext current request context
    383      *
    384      * @throws ProfileException thrown if there is a problem populating the profile information
    385      */
    386     protected void populateProfileInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    387         AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
    388                 .getRelyingPartyConfiguration().getProfileConfiguration(getProfileId());
    389         if (profileConfig != null) {
    390             requestContext.setProfileConfiguration(profileConfig);
    391             requestContext.setOutboundMessageArtifactType(profileConfig.getOutboundArtifactType());
    392         }
    393
    394         Endpoint endpoint = selectEndpoint(requestContext);
    395         if (endpoint == null) {
    396             log.error("No return endpoint available for relying party {}", requestContext.getInboundMessageIssuer());
    397             throw new ProfileException("No peer endpoint available to which to send SAML response");
    398         }
    399         requestContext.setPeerEntityEndpoint(endpoint);
    400     }
    401
    402     /**
    403      * Gets the name identifier formats to use when creating identifiers for the relying party.
    404      *
    405      * @param requestContext current request context
    406      *
    407      * @return list of formats that may be used with the relying party, or an empty list for no preference
    408      *
    409      * @throws ProfileException thrown if there is a problem determining the name identifier format to use
    410      */
    411     protected List<String> getNameFormats(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    412         ArrayList<String> nameFormats = new ArrayList<String>();
    413
    414         RoleDescriptor relyingPartyRole = requestContext.getPeerEntityRoleMetadata();
    415         if (relyingPartyRole != null) {
    416             List<String> relyingPartySupportedFormats = getEntitySupportedFormats(relyingPartyRole);
    417             if (relyingPartySupportedFormats != null && !relyingPartySupportedFormats.isEmpty()) {
    418                 nameFormats.addAll(relyingPartySupportedFormats);
    419             }
    420         }
    421
    422         // If metadata contains the unspecified name format this means that any are supported
    423         if (nameFormats.contains(NameIdentifier.UNSPECIFIED)) {
    424             nameFormats.clear();
    425         }
    426
    427         return nameFormats;
    428     }
    429
    430     /**
    431      * Gets the list of name identifier formats supported for a given role.
    432      *
    433      * @param role the role to get the list of supported name identifier formats
    434      *
    435      * @return list of supported name identifier formats
    436      */
    437     protected List<String> getEntitySupportedFormats(RoleDescriptor role) {
    438         List<NameIDFormat> nameIDFormats = null;
    439
    440         if (role instanceof SSODescriptor) {
    441             nameIDFormats = ((SSODescriptor) role).getNameIDFormats();
    442         } else if (role instanceof AuthnAuthorityDescriptor) {
    443             nameIDFormats = ((AuthnAuthorityDescriptor) role).getNameIDFormats();
    444         } else if (role instanceof PDPDescriptor) {
    445             nameIDFormats = ((PDPDescriptor) role).getNameIDFormats();
    446         } else if (role instanceof AttributeAuthorityDescriptor) {
    447             nameIDFormats = ((AttributeAuthorityDescriptor) role).getNameIDFormats();
    448         }
    449
    450         ArrayList<String> supportedFormats = new ArrayList<String>();
    451         if (nameIDFormats != null) {
    452             for (NameIDFormat format : nameIDFormats) {
    453                 supportedFormats.add(format.getFormat());
    454             }
    455         }
    456
    457         return supportedFormats;
    458     }
    459
    460     /**
    461      * Populates the request context with the information about the user if they have an existing session. Unless
    462      * overridden, {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
    463      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
    464      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)},
    465      * {@link #populateProfileInformation(BaseSAMLProfileRequestContext)}, and
    466      * {@link #populateSAMLMessageInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
    467      * properties they provide are available in the request context.
    468      *
    469      * This method should populate: user's session, user's principal name, and service authentication method
    470      *
    471      * @param requestContext current request context
    472      *
    473      * @throws ProfileException thrown if there is a problem populating the user's information
    474      */
    475     protected abstract void populateUserInformation(BaseSAMLProfileRequestContext requestContext)
    476             throws ProfileException;
    477
    478     /**
    479      * Selects the appropriate endpoint for the relying party and stores it in the request context.
    480      *
    481      * @param requestContext current request context
    482      *
    483      * @return Endpoint selected from the information provided in the request context
    484      *
    485      * @throws ProfileException thrown if there is a problem selecting a response endpoint
    486      */
    487     protected abstract Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) throws ProfileException;
    488
    489     /**
    490      * Encodes the request's SAML response and writes it to the servlet response.
    491      *
    492      * @param requestContext current request context
    493      *
    494      * @throws ProfileException thrown if no message encoder is registered for this profiles binding
    495      */
    496     protected void encodeResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
    497         try {
    498             SAMLMessageEncoder encoder = getMessageEncoders().get(requestContext.getPeerEntityEndpoint().getBinding());
    499             if (encoder == null) {
    500                 log.error("No outbound message encoder configured for binding {}", requestContext
    501                         .getPeerEntityEndpoint().getBinding());
    502                 throw new ProfileException("No outbound message encoder configured for binding "
    503                         + requestContext.getPeerEntityEndpoint().getBinding());
    504             }
    505
    506             AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
    507                     .getProfileConfiguration();
    508             if (profileConfig != null) {
    509                 if (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.always
    510                         || (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.conditional && !encoder
    511                                 .providesMessageIntegrity(requestContext))) {
    512                     Credential signingCredential = null;
    513                     if (profileConfig.getSigningCredential() != null) {
    514                         signingCredential = profileConfig.getSigningCredential();
    515                     } else if (requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential() != null) {
    516                         signingCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
    517                     }
    518
    519                     if (signingCredential == null) {
    520                         throw new ProfileException(
    521                                 "Signing of responses is required but no signing credential is available");
    522                     }
    523
    524                     requestContext.setOutboundSAMLMessageSigningCredential(signingCredential);
    525                 }
    526             }
    527
    528             log.debug("Encoding response to SAML request {} from relying party {}", requestContext
    529                     .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
    530
    531             requestContext.setMessageEncoder(encoder);
    532             encoder.encode(requestContext);
    533         } catch (MessageEncodingException e) {
    534             throw new ProfileException("Unable to encode response to relying party: "
    535                     + requestContext.getInboundMessageIssuer(), e);
    536         }
    537     }
    538
    539     /**
    540      * Writes an audit log entry indicating the successful response to the attribute request.
    541      *
    542      * @param context current request context
    543      */
    544     protected void writeAuditLogEntry(BaseSAMLProfileRequestContext context) {
    545         AuditLogEntry auditLogEntry = new AuditLogEntry();
    546         auditLogEntry.setMessageProfile(getProfileId());
    547         auditLogEntry.setPrincipalAuthenticationMethod(context.getPrincipalAuthenticationMethod());
    548         auditLogEntry.setPrincipalName(context.getPrincipalName());
    549         auditLogEntry.setAssertingPartyId(context.getLocalEntityId());
    550         auditLogEntry.setRelyingPartyId(context.getInboundMessageIssuer());
    551         auditLogEntry.setRequestBinding(context.getMessageDecoder().getBindingURI());
    552         auditLogEntry.setRequestId(context.getInboundSAMLMessageId());
    553         auditLogEntry.setResponseBinding(context.getMessageEncoder().getBindingURI());
    554         auditLogEntry.setResponseId(context.getOutboundSAMLMessageId());
    555         if (context.getReleasedAttributes() != null) {
    556             auditLogEntry.getReleasedAttributes().addAll(context.getReleasedAttributes());
    557         }
    558
    559         getAduitLog().info(auditLogEntry.toString());
    560     }
    561 }