76078d9fec15f1dbd1a717180dcead3e8759b167
[java-idp.git] / src / main / java / edu / internet2 / middleware / shibboleth / idp / profile / AbstractSAMLProfileHandler.java
1 /*
2  * Copyright [2007] [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;
18
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Map;
22
23 import javax.servlet.http.HttpServletRequest;
24
25 import org.opensaml.common.IdentifierGenerator;
26 import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
27 import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
28 import org.opensaml.saml1.core.NameIdentifier;
29 import org.opensaml.saml2.metadata.AttributeAuthorityDescriptor;
30 import org.opensaml.saml2.metadata.AuthnAuthorityDescriptor;
31 import org.opensaml.saml2.metadata.Endpoint;
32 import org.opensaml.saml2.metadata.EntityDescriptor;
33 import org.opensaml.saml2.metadata.NameIDFormat;
34 import org.opensaml.saml2.metadata.PDPDescriptor;
35 import org.opensaml.saml2.metadata.RoleDescriptor;
36 import org.opensaml.saml2.metadata.SSODescriptor;
37 import org.opensaml.saml2.metadata.provider.MetadataProvider;
38 import org.opensaml.saml2.metadata.provider.MetadataProviderException;
39 import org.opensaml.ws.message.encoder.MessageEncodingException;
40 import org.opensaml.ws.security.SecurityPolicyResolver;
41 import org.opensaml.ws.transport.InTransport;
42 import org.opensaml.ws.transport.http.HttpServletRequestAdapter;
43 import org.opensaml.xml.security.credential.Credential;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 import edu.internet2.middleware.shibboleth.common.log.AuditLogEntry;
48 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
49 import edu.internet2.middleware.shibboleth.common.profile.provider.AbstractShibbolethProfileHandler;
50 import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
51 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartyConfiguration;
52 import edu.internet2.middleware.shibboleth.common.relyingparty.RelyingPartySecurityPolicyResolver;
53 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.AbstractSAMLProfileConfiguration;
54 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.CryptoOperationRequirementLevel;
55 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.SAMLMDRelyingPartyConfigurationManager;
56 import edu.internet2.middleware.shibboleth.idp.session.Session;
57
58 /**
59  * Base class for SAML profile handlers.
60  */
61 public abstract class AbstractSAMLProfileHandler extends
62         AbstractShibbolethProfileHandler<SAMLMDRelyingPartyConfigurationManager, Session> {
63
64     /** SAML message audit log. */
65     private final Logger auditLog = LoggerFactory.getLogger(AuditLogEntry.AUDIT_LOGGER_NAME);
66
67     /** Class logger. */
68     private final Logger log = LoggerFactory.getLogger(AbstractSAMLProfileHandler.class);
69
70     /** Generator of IDs which may be used for SAML assertions, requests, etc. */
71     private IdentifierGenerator idGenerator;
72
73     /** All the SAML message decoders configured for the IdP. */
74     private Map<String, SAMLMessageDecoder> messageDecoders;
75
76     /** All the SAML message encoders configured for the IdP. */
77     private Map<String, SAMLMessageEncoder> messageEncoders;
78
79     /** SAML message binding used by inbound messages. */
80     private String inboundBinding;
81
82     /** SAML message bindings that may be used by outbound messages. */
83     private List<String> supportedOutboundBindings;
84
85     /** Resolver used to determine active security policy for an incoming request. */
86     private SecurityPolicyResolver securityPolicyResolver;
87
88     /** Constructor. */
89     protected AbstractSAMLProfileHandler() {
90         super();
91     }
92
93     /**
94      * Gets the resolver used to determine active security policy for an incoming request.
95      * 
96      * @return resolver used to determine active security policy for an incoming request
97      */
98     public SecurityPolicyResolver getSecurityPolicyResolver() {
99         if (securityPolicyResolver == null) {
100             setSecurityPolicyResolver(new RelyingPartySecurityPolicyResolver(getRelyingPartyConfigurationManager()));
101         }
102
103         return securityPolicyResolver;
104     }
105
106     /**
107      * Sets the resolver used to determine active security policy for an incoming request.
108      * 
109      * @param resolver resolver used to determine active security policy for an incoming request
110      */
111     public void setSecurityPolicyResolver(SecurityPolicyResolver resolver) {
112         securityPolicyResolver = resolver;
113     }
114
115     /**
116      * Gets the audit log for this handler.
117      * 
118      * @return audit log for this handler
119      */
120     protected Logger getAduitLog() {
121         return auditLog;
122     }
123
124     /**
125      * Gets an ID generator which may be used for SAML assertions, requests, etc.
126      * 
127      * @return ID generator
128      */
129     public IdentifierGenerator getIdGenerator() {
130         return idGenerator;
131     }
132
133     /**
134      * Gets the SAML message binding used by inbound messages.
135      * 
136      * @return SAML message binding used by inbound messages
137      */
138     public String getInboundBinding() {
139         return inboundBinding;
140     }
141
142     /**
143      * Gets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
144      * 
145      * @return SAML message decoders configured for the IdP indexed by SAML binding URI
146      */
147     public Map<String, SAMLMessageDecoder> getMessageDecoders() {
148         return messageDecoders;
149     }
150
151     /**
152      * Gets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
153      * 
154      * @return SAML message encoders configured for the IdP indexed by SAML binding URI
155      */
156     public Map<String, SAMLMessageEncoder> getMessageEncoders() {
157         return messageEncoders;
158     }
159
160     /**
161      * A convenience method for retrieving the SAML metadata provider from the relying party manager.
162      * 
163      * @return the metadata provider or null
164      */
165     public MetadataProvider getMetadataProvider() {
166         SAMLMDRelyingPartyConfigurationManager rpcManager = getRelyingPartyConfigurationManager();
167         if (rpcManager != null) {
168             return rpcManager.getMetadataProvider();
169         }
170
171         return null;
172     }
173
174     /**
175      * Gets the SAML message bindings that may be used by outbound messages.
176      * 
177      * @return SAML message bindings that may be used by outbound messages
178      */
179     public List<String> getSupportedOutboundBindings() {
180         return supportedOutboundBindings;
181     }
182
183     /**
184      * Gets the user's session, if there is one.
185      * 
186      * @param inTransport current inbound transport
187      * 
188      * @return user's session
189      */
190     protected Session getUserSession(InTransport inTransport) {
191         HttpServletRequest rawRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
192         return (Session) rawRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
193     }
194
195     /**
196      * Gets the user's session based on their principal name.
197      * 
198      * @param principalName user's principal name
199      * 
200      * @return the user's session
201      */
202     protected Session getUserSession(String principalName) {
203         return getSessionManager().getSession(principalName);
204     }
205
206     /**
207      * Gets an ID generator which may be used for SAML assertions, requests, etc.
208      * 
209      * @param generator an ID generator which may be used for SAML assertions, requests, etc
210      */
211     public void setIdGenerator(IdentifierGenerator generator) {
212         idGenerator = generator;
213     }
214
215     /**
216      * Sets the SAML message binding used by inbound messages.
217      * 
218      * @param binding SAML message binding used by inbound messages
219      */
220     public void setInboundBinding(String binding) {
221         inboundBinding = binding;
222     }
223
224     /**
225      * Sets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
226      * 
227      * @param decoders SAML message decoders configured for the IdP indexed by SAML binding URI
228      */
229     public void setMessageDecoders(Map<String, SAMLMessageDecoder> decoders) {
230         messageDecoders = decoders;
231     }
232
233     /**
234      * Sets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
235      * 
236      * @param encoders SAML message encoders configured for the IdP indexed by SAML binding URI
237      */
238     public void setMessageEncoders(Map<String, SAMLMessageEncoder> encoders) {
239         messageEncoders = encoders;
240     }
241
242     /**
243      * Sets the SAML message bindings that may be used by outbound messages.
244      * 
245      * @param bindings SAML message bindings that may be used by outbound messages
246      */
247     public void setSupportedOutboundBindings(List<String> bindings) {
248         supportedOutboundBindings = bindings;
249     }
250
251     /** {@inheritDoc} */
252     public RelyingPartyConfiguration getRelyingPartyConfiguration(String relyingPartyId) {
253         try {
254             if (getMetadataProvider().getEntityDescriptor(relyingPartyId) == null) {
255                 log.warn("No metadata for relying party {}, treating party as anonymous", relyingPartyId);
256                 return getRelyingPartyConfigurationManager().getAnonymousRelyingConfiguration();
257             }
258         } catch (MetadataProviderException e) {
259             log.error("Unable to look up relying party metadata", e);
260             return null;
261         }
262
263         return super.getRelyingPartyConfiguration(relyingPartyId);
264     }
265
266     /**
267      * Populates the request context with information.
268      * 
269      * This method requires the the following request context properties to be populated: inbound message transport,
270      * peer entity ID, metadata provider
271      * 
272      * This methods populates the following request context properties: user's session, user's principal name, service
273      * authentication method, peer entity metadata, relying party configuration, local entity ID, outbound message
274      * issuer, local entity metadata
275      * 
276      * @param requestContext current request context
277      * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
278      */
279     protected void populateRequestContext(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
280         populateRelyingPartyInformation(requestContext);
281         populateAssertingPartyInformation(requestContext);
282         populateSAMLMessageInformation(requestContext);
283         populateProfileInformation(requestContext);
284         populateUserInformation(requestContext);
285     }
286
287     /**
288      * Populates the request context with information about the relying party.
289      * 
290      * This method requires the the following request context properties to be populated: peer entity ID
291      * 
292      * This methods populates the following request context properties: peer entity metadata, relying party
293      * configuration
294      * 
295      * @param requestContext current request context
296      * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
297      */
298     protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
299             throws ProfileException {
300         MetadataProvider metadataProvider = requestContext.getMetadataProvider();
301         String relyingPartyId = requestContext.getInboundMessageIssuer();
302
303         EntityDescriptor relyingPartyMetadata;
304         try {
305             relyingPartyMetadata = metadataProvider.getEntityDescriptor(relyingPartyId);
306             requestContext.setPeerEntityMetadata(relyingPartyMetadata);
307         } catch (MetadataProviderException e) {
308             log.error("Error looking up metadata for relying party " + relyingPartyId, e);
309             throw new ProfileException("Error looking up metadata for relying party " + relyingPartyId);
310         }
311
312         RelyingPartyConfiguration rpConfig = getRelyingPartyConfiguration(relyingPartyId);
313         if (rpConfig == null) {
314             log.error("Unable to retrieve relying party configuration data for entity with ID {}", relyingPartyId);
315             throw new ProfileException("Unable to retrieve relying party configuration data for entity with ID "
316                     + relyingPartyId);
317         }
318         requestContext.setRelyingPartyConfiguration(rpConfig);
319     }
320
321     /**
322      * Populates the request context with information about the asserting party. Unless overridden,
323      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
324      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)} has already been invoked and the
325      * properties it provides are available in the request context.
326      * 
327      * This method requires the the following request context properties to be populated: metadata provider, relying
328      * party configuration
329      * 
330      * This methods populates the following request context properties: local entity ID, outbound message issuer, local
331      * entity metadata
332      * 
333      * @param requestContext current request context
334      * @throws ProfileException thrown if there is a problem looking up the asserting party's metadata
335      */
336     protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
337             throws ProfileException {
338         String assertingPartyId = requestContext.getRelyingPartyConfiguration().getProviderId();
339         requestContext.setLocalEntityId(assertingPartyId);
340         requestContext.setOutboundMessageIssuer(assertingPartyId);
341
342         try {
343             EntityDescriptor localEntityDescriptor = requestContext.getMetadataProvider().getEntityDescriptor(
344                     assertingPartyId);
345             if (localEntityDescriptor != null) {
346                 requestContext.setLocalEntityMetadata(localEntityDescriptor);
347             }
348         } catch (MetadataProviderException e) {
349             log.error("Error looking up metadata for asserting party " + assertingPartyId, e);
350             throw new ProfileException("Error looking up metadata for asserting party " + assertingPartyId);
351         }
352     }
353
354     /**
355      * Populates the request context with information from the inbound SAML message. Unless overridden,
356      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
357      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},and
358      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
359      * properties they provide are available in the request context.
360      * 
361      * 
362      * @param requestContext current request context
363      * 
364      * @throws ProfileException thrown if there is a problem populating the request context with information
365      */
366     protected abstract void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext)
367             throws ProfileException;
368
369     /**
370      * Populates the request context with the information about the profile. Unless overridden,
371      * {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
372      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
373      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)}, and
374      * {@link #populateSAMLMessageInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
375      * properties they provide are available in the request context.
376      * 
377      * This method requires the the following request context properties to be populated: relying party configuration
378      * 
379      * This methods populates the following request context properties: communication profile ID, profile configuration,
380      * outbound message artifact type, peer entity endpoint
381      * 
382      * @param requestContext current request context
383      * 
384      * @throws ProfileException thrown if there is a problem populating the profile information
385      */
386     protected void populateProfileInformation(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
387         AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
388                 .getRelyingPartyConfiguration().getProfileConfiguration(getProfileId());
389         if (profileConfig != null) {
390             requestContext.setProfileConfiguration(profileConfig);
391             requestContext.setOutboundMessageArtifactType(profileConfig.getOutboundArtifactType());
392         }
393
394         Endpoint endpoint = selectEndpoint(requestContext);
395         if (endpoint == null) {
396             log.error("No return endpoint available for relying party {}", requestContext.getInboundMessageIssuer());
397             throw new ProfileException("No peer endpoint available to which to send SAML response");
398         }
399         requestContext.setPeerEntityEndpoint(endpoint);
400     }
401
402     /**
403      * Gets the name identifier formats to use when creating identifiers for the relying party.
404      * 
405      * @param requestContext current request context
406      * 
407      * @return list of formats that may be used with the relying party, or an empty list for no preference
408      * 
409      * @throws ProfileException thrown if there is a problem determining the name identifier format to use
410      */
411     protected List<String> getNameFormats(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
412         ArrayList<String> nameFormats = new ArrayList<String>();
413
414         RoleDescriptor relyingPartyRole = requestContext.getPeerEntityRoleMetadata();
415         if (relyingPartyRole != null) {
416             List<String> relyingPartySupportedFormats = getEntitySupportedFormats(relyingPartyRole);
417             if (relyingPartySupportedFormats != null && !relyingPartySupportedFormats.isEmpty()) {
418                 nameFormats.addAll(relyingPartySupportedFormats);
419             }
420         }
421
422         // If metadata contains the unspecified name format this means that any are supported
423         if (nameFormats.contains(NameIdentifier.UNSPECIFIED)) {
424             nameFormats.clear();
425         }
426
427         return nameFormats;
428     }
429
430     /**
431      * Gets the list of name identifier formats supported for a given role.
432      * 
433      * @param role the role to get the list of supported name identifier formats
434      * 
435      * @return list of supported name identifier formats
436      */
437     protected List<String> getEntitySupportedFormats(RoleDescriptor role) {
438         List<NameIDFormat> nameIDFormats = null;
439
440         if (role instanceof SSODescriptor) {
441             nameIDFormats = ((SSODescriptor) role).getNameIDFormats();
442         } else if (role instanceof AuthnAuthorityDescriptor) {
443             nameIDFormats = ((AuthnAuthorityDescriptor) role).getNameIDFormats();
444         } else if (role instanceof PDPDescriptor) {
445             nameIDFormats = ((PDPDescriptor) role).getNameIDFormats();
446         } else if (role instanceof AttributeAuthorityDescriptor) {
447             nameIDFormats = ((AttributeAuthorityDescriptor) role).getNameIDFormats();
448         }
449
450         ArrayList<String> supportedFormats = new ArrayList<String>();
451         if (nameIDFormats != null) {
452             for (NameIDFormat format : nameIDFormats) {
453                 supportedFormats.add(format.getFormat());
454             }
455         }
456
457         return supportedFormats;
458     }
459
460     /**
461      * Populates the request context with the information about the user if they have an existing session. Unless
462      * overridden, {@link #populateRequestContext(BaseSAMLProfileRequestContext)} has already invoked
463      * {@link #populateRelyingPartyInformation(BaseSAMLProfileRequestContext)},
464      * {@link #populateAssertingPartyInformation(BaseSAMLProfileRequestContext)},
465      * {@link #populateProfileInformation(BaseSAMLProfileRequestContext)}, and
466      * {@link #populateSAMLMessageInformation(BaseSAMLProfileRequestContext)} have already been invoked and the
467      * properties they provide are available in the request context.
468      * 
469      * This method should populate: user's session, user's principal name, and service authentication method
470      * 
471      * @param requestContext current request context
472      * 
473      * @throws ProfileException thrown if there is a problem populating the user's information
474      */
475     protected abstract void populateUserInformation(BaseSAMLProfileRequestContext requestContext)
476             throws ProfileException;
477
478     /**
479      * Selects the appropriate endpoint for the relying party and stores it in the request context.
480      * 
481      * @param requestContext current request context
482      * 
483      * @return Endpoint selected from the information provided in the request context
484      * 
485      * @throws ProfileException thrown if there is a problem selecting a response endpoint
486      */
487     protected abstract Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) throws ProfileException;
488
489     /**
490      * Encodes the request's SAML response and writes it to the servlet response.
491      * 
492      * @param requestContext current request context
493      * 
494      * @throws ProfileException thrown if no message encoder is registered for this profiles binding
495      */
496     protected void encodeResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
497         try {
498             SAMLMessageEncoder encoder = null;
499
500             Endpoint endpoint = requestContext.getPeerEntityEndpoint();
501             if (endpoint == null) {
502                 log.warn("No peer endpoint available for peer. Unable to send response.");
503                 throw new ProfileException("No peer endpoint available for peer. Unable to send response.");
504             }
505
506             if (endpoint != null) {
507                 encoder = getMessageEncoders().get(endpoint.getBinding());
508                 if (encoder == null) {
509                     log.error("No outbound message encoder configured for binding: {}", requestContext
510                             .getPeerEntityEndpoint().getBinding());
511                     throw new ProfileException("No outbound message encoder configured for binding: "
512                             + requestContext.getPeerEntityEndpoint().getBinding());
513                 }
514             }
515
516             AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
517                     .getProfileConfiguration();
518             if (profileConfig != null) {
519                 if (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.always
520                         || (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.conditional && !encoder
521                                 .providesMessageIntegrity(requestContext))) {
522                     Credential signingCredential = profileConfig.getSigningCredential();
523                     if (signingCredential == null) {
524                         signingCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
525                     }
526
527                     if (signingCredential == null) {
528                         throw new ProfileException(
529                                 "Signing of responses is required but no signing credential is available");
530                     }
531
532                     if (signingCredential.getPrivateKey() == null) {
533                         throw new ProfileException(
534                                 "Signing of response is required but signing credential does not have a private key");
535                     }
536
537                     requestContext.setOutboundSAMLMessageSigningCredential(signingCredential);
538                 }
539             }
540
541             log.debug("Encoding response to SAML request {} from relying party {}", requestContext
542                     .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
543
544             requestContext.setMessageEncoder(encoder);
545             encoder.encode(requestContext);
546         } catch (MessageEncodingException e) {
547             throw new ProfileException("Unable to encode response to relying party: "
548                     + requestContext.getInboundMessageIssuer(), e);
549         }
550     }
551
552     /**
553      * Writes an audit log entry indicating the successful response to the attribute request.
554      * 
555      * @param context current request context
556      */
557     protected void writeAuditLogEntry(BaseSAMLProfileRequestContext context) {
558         AuditLogEntry auditLogEntry = new AuditLogEntry();
559         auditLogEntry.setMessageProfile(getProfileId());
560         auditLogEntry.setPrincipalAuthenticationMethod(context.getPrincipalAuthenticationMethod());
561         auditLogEntry.setPrincipalName(context.getPrincipalName());
562         auditLogEntry.setAssertingPartyId(context.getLocalEntityId());
563         auditLogEntry.setRelyingPartyId(context.getInboundMessageIssuer());
564         auditLogEntry.setRequestBinding(context.getMessageDecoder().getBindingURI());
565         auditLogEntry.setRequestId(context.getInboundSAMLMessageId());
566         auditLogEntry.setResponseBinding(context.getMessageEncoder().getBindingURI());
567         auditLogEntry.setResponseId(context.getOutboundSAMLMessageId());
568         if (context.getReleasedAttributes() != null) {
569             auditLogEntry.getReleasedAttributes().addAll(context.getReleasedAttributes());
570         }
571
572         getAduitLog().info(auditLogEntry.toString());
573     }
574 }