View Javadoc

    1   /*
    2    * Copyright 2011 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.saml2;
    18
    19  import org.joda.time.DateTime;
    20  import org.joda.time.chrono.ISOChronology;
    21  import org.opensaml.Configuration;
    22  import org.opensaml.common.IdentifierGenerator;
    23  import org.opensaml.common.SAMLObjectBuilder;
    24  import org.opensaml.common.binding.BasicEndpointSelector;
    25  import org.opensaml.common.binding.SAMLMessageContext;
    26  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
    27  import org.opensaml.common.xml.SAMLConstants;
    28  import org.opensaml.saml2.binding.decoding.BaseSAML2MessageDecoder;
    29  import org.opensaml.saml2.core.AuthnRequest;
    30  import org.opensaml.saml2.core.Issuer;
    31  import org.opensaml.saml2.core.NameIDPolicy;
    32  import org.opensaml.saml2.metadata.AssertionConsumerService;
    33  import org.opensaml.saml2.metadata.Endpoint;
    34  import org.opensaml.saml2.metadata.SPSSODescriptor;
    35  import org.opensaml.saml2.metadata.provider.MetadataProvider;
    36  import org.opensaml.saml2.metadata.provider.MetadataProviderException;
    37  import org.opensaml.ws.message.MessageContext;
    38  import org.opensaml.ws.message.decoder.MessageDecodingException;
    39  import org.opensaml.ws.transport.http.HTTPInTransport;
    40  import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
    41  import org.opensaml.xml.XMLObjectBuilderFactory;
    42  import org.opensaml.xml.util.DatatypeHelper;
    43  import org.slf4j.Logger;
    44  import org.slf4j.LoggerFactory;
    45
    46  import edu.internet2.middleware.shibboleth.idp.profile.saml2.SSOProfileHandler.SSORequestContext;
    47
    48  /**
    49   * Shibboleth 2.x HTTP request parameter-based SSO authentication request message decoder.
    50   *
    51   * <p>
    52   * This decoder understands and processes a set of defined HTTP request parameters representing a logical
    53   * SAML 2 SSO authentication request, and builds a corresponding {@link AuthnRequest} message.
    54   * This message is then stored in the {@link SAMLMessageContext} so that it may be processed
    55   * by other components (e.g. profile handler) that process standard AuthnRequest messages.
    56   * </p>
    57   * .
    58   */
    59  public class UnsolicitedSSODecoder extends BaseSAML2MessageDecoder implements SAMLMessageDecoder {
    60
    61      /** Class logger. */
    62      private final Logger log = LoggerFactory.getLogger(UnsolicitedSSODecoder.class);
    63
    64      /** The binding URI default value. */
    65      public String defaultBinding;
    66
    67      /** AuthnRequest builder. */
    68      private SAMLObjectBuilder<AuthnRequest> authnRequestBuilder;
    69
    70      /** Issuer builder. */
    71      private SAMLObjectBuilder<Issuer> issuerBuilder;
    72
    73      /** NameIDPolicy builder. */
    74      private SAMLObjectBuilder<NameIDPolicy> nipBuilder;
    75
    76      /** Identifier generator. */
    77      private IdentifierGenerator idGenerator;
    78
    79      /**
    80       * Constructor.
    81       *
    82       * @param identifierGenerator the IdentifierGenerator instance to use.
    83       */
    84      @SuppressWarnings("unchecked")
    85      public UnsolicitedSSODecoder(IdentifierGenerator identifierGenerator) {
    86          super();
    87
    88          XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
    89
    90          authnRequestBuilder =
    91              (SAMLObjectBuilder<AuthnRequest>) builderFactory.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
    92          issuerBuilder =
    93              (SAMLObjectBuilder<Issuer>) builderFactory.getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
    94          nipBuilder =
    95              (SAMLObjectBuilder<NameIDPolicy>) builderFactory.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME);
    96
    97          idGenerator = identifierGenerator;
    98          defaultBinding = SAMLConstants.SAML2_POST_BINDING_URI;
    99      }
    100
    101     /** {@inheritDoc} */
    102     public String getBindingURI() {
    103         return "urn:mace:shibboleth:2.0:profiles:AuthnRequest";
    104     }
    105
    106     /** {@inheritDoc} */
    107     @SuppressWarnings("unchecked")
    108     protected boolean isIntendedDestinationEndpointURIRequired(SAMLMessageContext samlMsgCtx) {
    109         return false;
    110     }
    111
    112     /** {@inheritDoc} */
    113     @SuppressWarnings("unchecked")
    114     protected String getIntendedDestinationEndpointURI(SAMLMessageContext samlMsgCtx) throws MessageDecodingException {
    115         // Not relevant in this binding/profile, there is neither SAML message
    116         // nor binding parameter with this information
    117         return null;
    118     }
    119
    120     /**
    121      * Returns the default ACS binding.
    122      * @return  default binding URI
    123      */
    124     public String getDefaultBinding() {
    125         return defaultBinding;
    126     }
    127
    128     /**
    129      * Sets the default ACS binding.
    130      * @param binding default binding URI
    131      */
    132     public void setDefaultBinding(String binding) {
    133         defaultBinding = binding;
    134     }
    135
    136     /** {@inheritDoc} */
    137     @SuppressWarnings("unchecked")
    138     protected void doDecode(MessageContext messageContext) throws MessageDecodingException {
    139         if (!(messageContext instanceof SSORequestContext)) {
    140             log.warn("Invalid message context type, this decoder only supports SSORequestContext");
    141             throw new MessageDecodingException(
    142                     "Invalid message context type, this decoder only supports SSORequestContext");
    143         }
    144
    145         if (!(messageContext.getInboundMessageTransport() instanceof HTTPInTransport)) {
    146             log.warn("Invalid inbound message transport type, this decoder only support HTTPInTransport");
    147             throw new MessageDecodingException(
    148                     "Invalid inbound message transport type, this decoder only support HTTPInTransport");
    149         }
    150
    151         SSORequestContext requestContext = (SSORequestContext) messageContext;
    152         HTTPInTransport transport = (HTTPInTransport) messageContext.getInboundMessageTransport();
    153
    154         String providerId = DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("providerId"));
    155         if (providerId == null) {
    156             log.warn("No providerId parameter given in unsolicited SSO authentication request.");
    157             throw new MessageDecodingException(
    158                     "No providerId parameter given in unsolicited SSO authentication request.");
    159         }
    160
    161         requestContext.setRelayState(DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("target")));
    162
    163         String timeStr = DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("time"));
    164         String sessionID = ((HttpServletRequestAdapter) transport).getWrappedRequest().getRequestedSessionId();
    165
    166         String binding = null;
    167         String acsURL = DatatypeHelper.safeTrimOrNullString(transport.getParameterValue("shire"));
    168         if (acsURL == null) {
    169             acsURL = lookupACSURL(requestContext.getMetadataProvider(), providerId);
    170             if (acsURL == null) {
    171                 log.warn("Unable to resolve SP ACS URL for AuthnRequest construction for entityID: {}",
    172                         providerId);
    173                 throw new MessageDecodingException("Unable to resolve SP ACS URL for AuthnRequest construction");
    174             }
    175             binding = defaultBinding;
    176         }
    177
    178         AuthnRequest authnRequest = buildAuthnRequest(providerId, acsURL, binding, timeStr, sessionID);
    179         requestContext.setInboundMessage(authnRequest);
    180         requestContext.setInboundSAMLMessage(authnRequest);
    181         log.debug("Mocked up SAML message");
    182
    183         populateMessageContext(requestContext);
    184
    185         requestContext.setUnsolicited(true);
    186     }
    187
    188     /**
    189      * Build a SAML 2 AuthnRequest from the parameters specified in the inbound transport.
    190      *
    191      * @param entityID the requester identity
    192      * @param acsURL the ACS URL
    193      * @param acsBinding the ACS binding URI
    194      * @param timeStr the request timestamp
    195      * @param sessionID the container session, if any
    196      * @return a newly constructed AuthnRequest instance
    197      */
    198     @SuppressWarnings("unchecked")
    199     private AuthnRequest buildAuthnRequest(String entityID, String acsURL, String acsBinding, String timeStr, String sessionID) {
    200
    201         AuthnRequest authnRequest = authnRequestBuilder.buildObject();
    202         authnRequest.setAssertionConsumerServiceURL(acsURL);
    203         if (acsBinding != null) {
    204             authnRequest.setProtocolBinding(acsBinding);
    205         }
    206
    207         Issuer issuer = issuerBuilder.buildObject();
    208         issuer.setValue(entityID);
    209         authnRequest.setIssuer(issuer);
    210
    211         // Matches the default semantic a typical SP would have.
    212         NameIDPolicy nip = nipBuilder.buildObject();
    213         nip.setAllowCreate(true);
    214         authnRequest.setNameIDPolicy(nip);
    215
    216         if (timeStr != null) {
    217             authnRequest.setIssueInstant(
    218                     new DateTime(Long.parseLong(timeStr) * 1000, ISOChronology.getInstanceUTC()));
    219             if (sessionID != null) {
    220                 // Construct a pseudo message ID by combining the timestamp
    221                 // and a client-specific ID (the Java session ID).
    222                 // This allows for replay detection if the
    223                 authnRequest.setID('_' + sessionID + '!' + timeStr);
    224             } else {
    225                 authnRequest.setID(idGenerator.generateIdentifier());
    226             }
    227         } else {
    228             authnRequest.setID(idGenerator.generateIdentifier());
    229             authnRequest.setIssueInstant(new DateTime());
    230         }
    231
    232         return authnRequest;
    233     }
    234
    235     /**
    236      * Lookup the ACS URL for the specified SP entityID and binding URI.
    237      *
    238      * @param mdProvider the SAML message context's metadata source
    239      * @param entityId the SP entityID
    240      * @return the resolved ACS URL endpoint
    241      * @throws MessageDecodingException if there is an error resolving the ACS URL
    242      */
    243     @SuppressWarnings("unchecked")
    244     private String lookupACSURL(MetadataProvider mdProvider, String entityId)
    245             throws MessageDecodingException {
    246         SPSSODescriptor spssoDesc = null;
    247         try {
    248             spssoDesc = (SPSSODescriptor) mdProvider.getRole(entityId, SPSSODescriptor.DEFAULT_ELEMENT_NAME,
    249                     SAMLConstants.SAML20P_NS);
    250         } catch (MetadataProviderException e) {
    251             throw new MessageDecodingException("Error resolving metadata role for SP entityId: " + entityId, e);
    252         }
    253
    254         if (spssoDesc == null) {
    255             throw new MessageDecodingException(
    256                     "SAML 2 SPSSODescriptor could not be resolved from metadata for SP entityID: " + entityId);
    257         }
    258
    259         BasicEndpointSelector selector = new BasicEndpointSelector();
    260         selector.setEntityRoleMetadata(spssoDesc);
    261         selector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
    262         selector.getSupportedIssuerBindings().add(defaultBinding);
    263
    264         Endpoint endpoint = selector.selectEndpoint();
    265         if (endpoint == null || endpoint.getLocation() == null) {
    266             throw new MessageDecodingException(
    267                     "SAML 2 ACS endpoint could not be resolved from metadata for SP entityID and binding: " + entityId
    268                             + " -- " + defaultBinding);
    269         }
    270
    271         return endpoint.getLocation();
    272     }
    273
    274 }