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 }