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 java.text.MessageFormat;
21  
22  import org.joda.time.DateTime;
23  import org.opensaml.common.SAMLObject;
24  import org.opensaml.common.SAMLObjectBuilder;
25  import org.opensaml.common.binding.BasicEndpointSelector;
26  import org.opensaml.common.binding.artifact.SAMLArtifactMap;
27  import org.opensaml.common.binding.artifact.SAMLArtifactMap.SAMLArtifactMapEntry;
28  import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
29  import org.opensaml.common.xml.SAMLConstants;
30  import org.opensaml.saml2.binding.SAML2ArtifactMessageContext;
31  import org.opensaml.saml2.core.ArtifactResolve;
32  import org.opensaml.saml2.core.ArtifactResponse;
33  import org.opensaml.saml2.core.NameID;
34  import org.opensaml.saml2.core.Status;
35  import org.opensaml.saml2.core.StatusCode;
36  import org.opensaml.saml2.metadata.AssertionConsumerService;
37  import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
38  import org.opensaml.saml2.metadata.Endpoint;
39  import org.opensaml.saml2.metadata.EntityDescriptor;
40  import org.opensaml.saml2.metadata.SPSSODescriptor;
41  import org.opensaml.saml2.metadata.provider.MetadataProvider;
42  import org.opensaml.ws.message.decoder.MessageDecodingException;
43  import org.opensaml.ws.transport.http.HTTPInTransport;
44  import org.opensaml.ws.transport.http.HTTPOutTransport;
45  import org.opensaml.xml.security.SecurityException;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
50  import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
51  import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.ArtifactResolutionConfiguration;
52  
53  /**
54   * SAML 2.0 Artifact resolution profile handler.
55   */
56  public class ArtifactResolution extends AbstractSAML2ProfileHandler {
57  
58      /** Class logger. */
59      private final Logger log = LoggerFactory.getLogger(ArtifactResolution.class);
60  
61      /** Map artifacts to SAML messages. */
62      private SAMLArtifactMap artifactMap;
63  
64      /** Artifact response object builder. */
65      private SAMLObjectBuilder<ArtifactResponse> responseBuilder;
66  
67      /** Builder of assertion consumer service endpoints. */
68      private SAMLObjectBuilder<AssertionConsumerService> acsEndpointBuilder;
69  
70      /**
71       * Constructor.
72       * 
73       * @param map ArtifactMap used to lookup artifacts to be resolved.
74       */
75      public ArtifactResolution(SAMLArtifactMap map) {
76          super();
77  
78          artifactMap = map;
79  
80          responseBuilder = (SAMLObjectBuilder<ArtifactResponse>) getBuilderFactory().getBuilder(
81                  ArtifactResponse.DEFAULT_ELEMENT_NAME);
82          acsEndpointBuilder = (SAMLObjectBuilder<AssertionConsumerService>) getBuilderFactory().getBuilder(
83                  AssertionConsumerService.DEFAULT_ELEMENT_NAME);
84      }
85  
86      /** {@inheritDoc} */
87      public String getProfileId() {
88          return ArtifactResolutionConfiguration.PROFILE_ID;
89      }
90  
91      /** {@inheritDoc} */
92      public void processRequest(HTTPInTransport inTransport, HTTPOutTransport outTransport) throws ProfileException {
93          ArtifactResponse samlResponse;
94  
95          ArtifactResolutionRequestContext requestContext = new ArtifactResolutionRequestContext();
96          try {
97              decodeRequest(requestContext, inTransport, outTransport);
98  
99              if (requestContext.getProfileConfiguration() == null) {
100                 String msg = MessageFormat.format(
101                         "SAML 2 Artifact Resolve profile is not configured for relying party ''{0}''", requestContext
102                                 .getInboundMessageIssuer());
103                 requestContext
104                         .setFailureStatus(buildStatus(StatusCode.SUCCESS_URI, StatusCode.REQUEST_DENIED_URI, msg));
105                 log.warn(msg);
106                 throw new ProfileException(msg);
107             }
108 
109             checkSamlVersion(requestContext);
110 
111             SAMLArtifactMapEntry artifactEntry = artifactMap.get(requestContext.getArtifact());
112             if (artifactEntry == null || artifactEntry.isExpired()) {
113                 String msg = MessageFormat.format("Unknown artifact ''{0}'' from relying party ''{1}''", requestContext
114                         .getArtifact(), requestContext.getInboundMessageIssuer());
115                 log.error(msg);
116                 requestContext
117                         .setFailureStatus(buildStatus(StatusCode.SUCCESS_URI, StatusCode.REQUEST_DENIED_URI, msg));
118             }
119 
120             if (!artifactEntry.getIssuerId().equals(requestContext.getLocalEntityId())) {
121                 String msg = MessageFormat.format(
122                         "Artifact issuer mismatch.  Artifact issued by ''{0}'' but IdP has entity ID of ''{1}''",
123                         artifactEntry.getIssuerId(), requestContext.getLocalEntityId());
124                 log.warn(msg);
125                 requestContext
126                         .setFailureStatus(buildStatus(StatusCode.SUCCESS_URI, StatusCode.REQUEST_DENIED_URI, msg));
127                 return;
128             }
129 
130             if (!artifactEntry.getRelyingPartyId().equals(requestContext.getInboundMessageIssuer())) {
131                 String msg = MessageFormat
132                         .format(
133                                 "Artifact requester mismatch. Artifact was issued to ''{0}'' but the resolve request came from ''{1}''",
134                                 artifactEntry.getRelyingPartyId(), requestContext.getInboundMessageIssuer());
135                 log.warn(msg);
136                 requestContext
137                         .setFailureStatus(buildStatus(StatusCode.SUCCESS_URI, StatusCode.REQUEST_DENIED_URI, msg));
138                 return;
139             }
140 
141             // create the SAML response
142             requestContext.setReferencedMessage(artifactEntry.getSamlMessage());
143             samlResponse = buildArtifactResponse(requestContext);
144         } catch (ProfileException e) {
145             samlResponse = buildArtifactErrorResponse(requestContext);
146         }
147 
148         requestContext.setOutboundSAMLMessage(samlResponse);
149         requestContext.setOutboundSAMLMessageId(samlResponse.getID());
150         requestContext.setOutboundSAMLMessageIssueInstant(samlResponse.getIssueInstant());
151 
152         encodeResponse(requestContext);
153         writeAuditLogEntry(requestContext);
154     }
155 
156     /**
157      * Decodes an incoming request and populates a created request context with the resultant information.
158      * 
159      * @param inTransport inbound message transport
160      * @param outTransport outbound message transport
161      * @param requestContext request context to which decoded information should be added
162      * 
163      * @throws ProfileException throw if there is a problem decoding the request
164      */
165     protected void decodeRequest(ArtifactResolutionRequestContext requestContext, HTTPInTransport inTransport,
166             HTTPOutTransport outTransport) throws ProfileException {
167         if (log.isDebugEnabled()) {
168             log.debug("Decoding message with decoder binding '{}'",
169                     getInboundMessageDecoder(requestContext).getBindingURI());
170         }
171 
172         requestContext.setCommunicationProfileId(getProfileId());
173 
174         MetadataProvider metadataProvider = getMetadataProvider();
175         requestContext.setMetadataProvider(metadataProvider);
176 
177         requestContext.setInboundMessageTransport(inTransport);
178         requestContext.setInboundSAMLProtocol(SAMLConstants.SAML20P_NS);
179         requestContext.setSecurityPolicyResolver(getSecurityPolicyResolver());
180         requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
181 
182         requestContext.setOutboundMessageTransport(outTransport);
183         requestContext.setOutboundSAMLProtocol(SAMLConstants.SAML20P_NS);
184 
185         try {
186             SAMLMessageDecoder decoder = getInboundMessageDecoder(requestContext);
187             requestContext.setMessageDecoder(decoder);
188             decoder.decode(requestContext);
189             log.debug("Decoded request from relying party '{}'", requestContext.getInboundMessageIssuer());
190         } catch (MessageDecodingException e) {
191             String msg = "Error decoding artifact resolve message";
192             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, msg));
193             log.warn(msg, e);
194             throw new ProfileException(msg);
195         } catch (SecurityException e) {
196             String msg = "Message did not meet security requirements";
197             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI, msg));
198             log.warn(msg, e);
199             throw new ProfileException(msg, e);
200         } finally {
201             populateRequestContext(requestContext);
202         }
203     }
204 
205     /** {@inheritDoc} */
206     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
207             throws ProfileException {
208         super.populateRelyingPartyInformation(requestContext);
209 
210         EntityDescriptor relyingPartyMetadata = requestContext.getPeerEntityMetadata();
211         if (relyingPartyMetadata != null) {
212             requestContext.setPeerEntityRole(SPSSODescriptor.DEFAULT_ELEMENT_NAME);
213             requestContext.setPeerEntityRoleMetadata(relyingPartyMetadata.getSPSSODescriptor(SAMLConstants.SAML20P_NS));
214         }
215     }
216 
217     /** {@inheritDoc} */
218     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
219             throws ProfileException {
220         super.populateAssertingPartyInformation(requestContext);
221 
222         EntityDescriptor localEntityDescriptor = requestContext.getLocalEntityMetadata();
223         if (localEntityDescriptor != null) {
224             requestContext.setLocalEntityRole(AttributeAuthorityDescriptor.DEFAULT_ELEMENT_NAME);
225             requestContext.setLocalEntityRoleMetadata(localEntityDescriptor
226                     .getAttributeAuthorityDescriptor(SAMLConstants.SAML20P_NS));
227         }
228     }
229 
230     /**
231      * Populates the request context with information from the inbound SAML message.
232      * 
233      * This method requires the the following request context properties to be populated: inbound saml message
234      * 
235      * This methods populates the following request context properties: subject name identifier
236      * 
237      * @param requestContext current request context
238      * 
239      * @throws ProfileException thrown if the inbound SAML message or subject identifier is null
240      */
241     protected void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
242         ArtifactResolve samlMessage = (ArtifactResolve) requestContext.getInboundSAMLMessage();
243         if (samlMessage != null && samlMessage.getArtifact() != null) {
244             ((ArtifactResolutionRequestContext) requestContext).setArtifact(samlMessage.getArtifact().getArtifact());
245         }
246     }
247 
248     /**
249      * Selects the appropriate endpoint for the relying party and stores it in the request context.
250      * 
251      * @param requestContext current request context
252      * 
253      * @return Endpoint selected from the information provided in the request context
254      */
255     protected Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) {
256         Endpoint endpoint;
257 
258         if (getInboundBinding().equals(SAMLConstants.SAML2_SOAP11_BINDING_URI)) {
259             endpoint = acsEndpointBuilder.buildObject();
260             endpoint.setBinding(SAMLConstants.SAML2_SOAP11_BINDING_URI);
261         } else {
262             BasicEndpointSelector endpointSelector = new BasicEndpointSelector();
263             endpointSelector.setEndpointType(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
264             endpointSelector.setMetadataProvider(getMetadataProvider());
265             endpointSelector.setEntityMetadata(requestContext.getPeerEntityMetadata());
266             endpointSelector.setEntityRoleMetadata(requestContext.getPeerEntityRoleMetadata());
267             endpointSelector.setSamlRequest(requestContext.getInboundSAMLMessage());
268             endpointSelector.getSupportedIssuerBindings().addAll(getSupportedOutboundBindings());
269             endpoint = endpointSelector.selectEndpoint();
270         }
271 
272         return endpoint;
273     }
274 
275     /**
276      * Constructs a artifact resolution response with the derferenced SAML message inside.
277      * 
278      * @param requestContext current request context
279      * 
280      * @return constructed response
281      */
282     protected ArtifactResponse buildArtifactResponse(ArtifactResolutionRequestContext requestContext) {
283         DateTime issueInstant = new DateTime();
284 
285         // create the SAML response and add the assertion
286         ArtifactResponse samlResponse = responseBuilder.buildObject();
287         samlResponse.setIssueInstant(issueInstant);
288         populateStatusResponse(requestContext, samlResponse);
289 
290         if (requestContext.getFailureStatus() == null) {
291             Status status = buildStatus(StatusCode.SUCCESS_URI, null, null);
292             samlResponse.setStatus(status);
293             samlResponse.setMessage(requestContext.getReferencedMessage());
294         } else {
295             samlResponse.setStatus(requestContext.getFailureStatus());
296         }
297 
298         return samlResponse;
299     }
300 
301     /**
302      * Constructs an artifact resolution response with an error status as content.
303      * 
304      * @param requestContext current request context
305      * 
306      * @return constructed response
307      */
308     protected ArtifactResponse buildArtifactErrorResponse(ArtifactResolutionRequestContext requestContext) {
309         ArtifactResponse samlResponse = responseBuilder.buildObject();
310         samlResponse.setIssueInstant(new DateTime());
311         populateStatusResponse(requestContext, samlResponse);
312 
313         samlResponse.setStatus(requestContext.getFailureStatus());
314 
315         return samlResponse;
316     }
317 
318     /** Represents the internal state of a SAML 2.0 Artifact resolver request while it's being processed by the IdP. */
319     public class ArtifactResolutionRequestContext extends
320             BaseSAML2ProfileRequestContext<ArtifactResolve, ArtifactResponse, ArtifactResolutionConfiguration>
321             implements SAML2ArtifactMessageContext<ArtifactResolve, ArtifactResponse, NameID> {
322 
323         /** Artifact to be resolved. */
324         private String artifact;
325 
326         /** Message referenced by the SAML artifact. */
327         private SAMLObject referencedMessage;
328 
329         /** {@inheritDoc} */
330         public String getArtifact() {
331             return artifact;
332         }
333 
334         /** {@inheritDoc} */
335         public void setArtifact(String saml2Artifact) {
336             this.artifact = saml2Artifact;
337         }
338 
339         /** {@inheritDoc} */
340         public SAMLObject getReferencedMessage() {
341             return referencedMessage;
342         }
343 
344         /** {@inheritDoc} */
345         public void setReferencedMessage(SAMLObject message) {
346             referencedMessage = message;
347         }
348     }
349 }