View Javadoc

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