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 }