2 * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package edu.internet2.middleware.shibboleth.idp.profile;
19 import java.util.ArrayList;
20 import java.util.List;
23 import javax.servlet.http.HttpServletRequest;
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;
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;
59 * Base class for SAML profile handlers.
61 public abstract class AbstractSAMLProfileHandler extends
62 AbstractShibbolethProfileHandler<SAMLMDRelyingPartyConfigurationManager, Session> {
64 /** SAML message audit log. */
65 private final Logger auditLog = LoggerFactory.getLogger(AuditLogEntry.AUDIT_LOGGER_NAME);
68 private final Logger log = LoggerFactory.getLogger(AbstractSAMLProfileHandler.class);
70 /** Generator of IDs which may be used for SAML assertions, requests, etc. */
71 private IdentifierGenerator idGenerator;
73 /** All the SAML message decoders configured for the IdP. */
74 private Map<String, SAMLMessageDecoder> messageDecoders;
76 /** All the SAML message encoders configured for the IdP. */
77 private Map<String, SAMLMessageEncoder> messageEncoders;
79 /** SAML message binding used by inbound messages. */
80 private String inboundBinding;
82 /** SAML message bindings that may be used by outbound messages. */
83 private List<String> supportedOutboundBindings;
85 /** Resolver used to determine active security policy for an incoming request. */
86 private SecurityPolicyResolver securityPolicyResolver;
89 protected AbstractSAMLProfileHandler() {
94 * Gets the resolver used to determine active security policy for an incoming request.
96 * @return resolver used to determine active security policy for an incoming request
98 public SecurityPolicyResolver getSecurityPolicyResolver() {
99 if (securityPolicyResolver == null) {
100 setSecurityPolicyResolver(new RelyingPartySecurityPolicyResolver(getRelyingPartyConfigurationManager()));
103 return securityPolicyResolver;
107 * Sets the resolver used to determine active security policy for an incoming request.
109 * @param resolver resolver used to determine active security policy for an incoming request
111 public void setSecurityPolicyResolver(SecurityPolicyResolver resolver) {
112 securityPolicyResolver = resolver;
116 * Gets the audit log for this handler.
118 * @return audit log for this handler
120 protected Logger getAduitLog() {
125 * Gets an ID generator which may be used for SAML assertions, requests, etc.
127 * @return ID generator
129 public IdentifierGenerator getIdGenerator() {
134 * Gets the SAML message binding used by inbound messages.
136 * @return SAML message binding used by inbound messages
138 public String getInboundBinding() {
139 return inboundBinding;
143 * Gets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
145 * @return SAML message decoders configured for the IdP indexed by SAML binding URI
147 public Map<String, SAMLMessageDecoder> getMessageDecoders() {
148 return messageDecoders;
152 * Gets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
154 * @return SAML message encoders configured for the IdP indexed by SAML binding URI
156 public Map<String, SAMLMessageEncoder> getMessageEncoders() {
157 return messageEncoders;
161 * A convenience method for retrieving the SAML metadata provider from the relying party manager.
163 * @return the metadata provider or null
165 public MetadataProvider getMetadataProvider() {
166 SAMLMDRelyingPartyConfigurationManager rpcManager = getRelyingPartyConfigurationManager();
167 if (rpcManager != null) {
168 return rpcManager.getMetadataProvider();
175 * Gets the SAML message bindings that may be used by outbound messages.
177 * @return SAML message bindings that may be used by outbound messages
179 public List<String> getSupportedOutboundBindings() {
180 return supportedOutboundBindings;
184 * Gets the user's session, if there is one.
186 * @param inTransport current inbound transport
188 * @return user's session
190 protected Session getUserSession(InTransport inTransport) {
191 HttpServletRequest rawRequest = ((HttpServletRequestAdapter) inTransport).getWrappedRequest();
192 return (Session) rawRequest.getAttribute(Session.HTTP_SESSION_BINDING_ATTRIBUTE);
196 * Gets the user's session based on their principal name.
198 * @param principalName user's principal name
200 * @return the user's session
202 protected Session getUserSession(String principalName) {
203 return getSessionManager().getSession(principalName);
207 * Gets an ID generator which may be used for SAML assertions, requests, etc.
209 * @param generator an ID generator which may be used for SAML assertions, requests, etc
211 public void setIdGenerator(IdentifierGenerator generator) {
212 idGenerator = generator;
216 * Sets the SAML message binding used by inbound messages.
218 * @param binding SAML message binding used by inbound messages
220 public void setInboundBinding(String binding) {
221 inboundBinding = binding;
225 * Sets all the SAML message decoders configured for the IdP indexed by SAML binding URI.
227 * @param decoders SAML message decoders configured for the IdP indexed by SAML binding URI
229 public void setMessageDecoders(Map<String, SAMLMessageDecoder> decoders) {
230 messageDecoders = decoders;
234 * Sets all the SAML message encoders configured for the IdP indexed by SAML binding URI.
236 * @param encoders SAML message encoders configured for the IdP indexed by SAML binding URI
238 public void setMessageEncoders(Map<String, SAMLMessageEncoder> encoders) {
239 messageEncoders = encoders;
243 * Sets the SAML message bindings that may be used by outbound messages.
245 * @param bindings SAML message bindings that may be used by outbound messages
247 public void setSupportedOutboundBindings(List<String> bindings) {
248 supportedOutboundBindings = bindings;
252 public RelyingPartyConfiguration getRelyingPartyConfiguration(String relyingPartyId) {
254 if (getMetadataProvider().getEntityDescriptor(relyingPartyId) == null) {
255 log.warn("No metadata for relying party {}, treating party as anonymous", relyingPartyId);
256 return getRelyingPartyConfigurationManager().getAnonymousRelyingConfiguration();
258 } catch (MetadataProviderException e) {
259 log.error("Unable to look up relying party metadata", e);
263 return super.getRelyingPartyConfiguration(relyingPartyId);
267 * Populates the request context with information.
269 * This method requires the the following request context properties to be populated: inbound message transport,
270 * peer entity ID, metadata provider
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
276 * @param requestContext current request context
277 * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
279 protected void populateRequestContext(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
280 populateRelyingPartyInformation(requestContext);
281 populateAssertingPartyInformation(requestContext);
282 populateSAMLMessageInformation(requestContext);
283 populateProfileInformation(requestContext);
284 populateUserInformation(requestContext);
288 * Populates the request context with information about the relying party.
290 * This method requires the the following request context properties to be populated: peer entity ID
292 * This methods populates the following request context properties: peer entity metadata, relying party
295 * @param requestContext current request context
296 * @throws ProfileException thrown if there is a problem looking up the relying party's metadata
298 protected void populateRelyingPartyInformation(BaseSAMLProfileRequestContext requestContext)
299 throws ProfileException {
300 MetadataProvider metadataProvider = requestContext.getMetadataProvider();
301 String relyingPartyId = requestContext.getInboundMessageIssuer();
303 EntityDescriptor relyingPartyMetadata;
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);
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 "
318 requestContext.setRelyingPartyConfiguration(rpConfig);
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.
327 * This method requires the the following request context properties to be populated: metadata provider, relying
328 * party configuration
330 * This methods populates the following request context properties: local entity ID, outbound message issuer, local
333 * @param requestContext current request context
334 * @throws ProfileException thrown if there is a problem looking up the asserting party's metadata
336 protected void populateAssertingPartyInformation(BaseSAMLProfileRequestContext requestContext)
337 throws ProfileException {
338 String assertingPartyId = requestContext.getRelyingPartyConfiguration().getProviderId();
339 requestContext.setLocalEntityId(assertingPartyId);
340 requestContext.setOutboundMessageIssuer(assertingPartyId);
343 EntityDescriptor localEntityDescriptor = requestContext.getMetadataProvider().getEntityDescriptor(
345 if (localEntityDescriptor != null) {
346 requestContext.setLocalEntityMetadata(localEntityDescriptor);
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);
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.
362 * @param requestContext current request context
364 * @throws ProfileException thrown if there is a problem populating the request context with information
366 protected abstract void populateSAMLMessageInformation(BaseSAMLProfileRequestContext requestContext)
367 throws ProfileException;
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.
377 * This method requires the the following request context properties to be populated: relying party configuration
379 * This methods populates the following request context properties: communication profile ID, profile configuration,
380 * outbound message artifact type, peer entity endpoint
382 * @param requestContext current request context
384 * @throws ProfileException thrown if there is a problem populating the profile information
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());
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");
399 requestContext.setPeerEntityEndpoint(endpoint);
403 * Gets the name identifier formats to use when creating identifiers for the relying party.
405 * @param requestContext current request context
407 * @return list of formats that may be used with the relying party, or an empty list for no preference
409 * @throws ProfileException thrown if there is a problem determining the name identifier format to use
411 protected List<String> getNameFormats(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
412 ArrayList<String> nameFormats = new ArrayList<String>();
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);
422 // If metadata contains the unspecified name format this means that any are supported
423 if (nameFormats.contains(NameIdentifier.UNSPECIFIED)) {
431 * Gets the list of name identifier formats supported for a given role.
433 * @param role the role to get the list of supported name identifier formats
435 * @return list of supported name identifier formats
437 protected List<String> getEntitySupportedFormats(RoleDescriptor role) {
438 List<NameIDFormat> nameIDFormats = null;
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();
450 ArrayList<String> supportedFormats = new ArrayList<String>();
451 if (nameIDFormats != null) {
452 for (NameIDFormat format : nameIDFormats) {
453 supportedFormats.add(format.getFormat());
457 return supportedFormats;
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.
469 * This method should populate: user's session, user's principal name, and service authentication method
471 * @param requestContext current request context
473 * @throws ProfileException thrown if there is a problem populating the user's information
475 protected abstract void populateUserInformation(BaseSAMLProfileRequestContext requestContext)
476 throws ProfileException;
479 * Selects the appropriate endpoint for the relying party and stores it in the request context.
481 * @param requestContext current request context
483 * @return Endpoint selected from the information provided in the request context
485 * @throws ProfileException thrown if there is a problem selecting a response endpoint
487 protected abstract Endpoint selectEndpoint(BaseSAMLProfileRequestContext requestContext) throws ProfileException;
490 * Encodes the request's SAML response and writes it to the servlet response.
492 * @param requestContext current request context
494 * @throws ProfileException thrown if no message encoder is registered for this profiles binding
496 protected void encodeResponse(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
498 SAMLMessageEncoder encoder = getMessageEncoders().get(requestContext.getPeerEntityEndpoint().getBinding());
499 if (encoder == null) {
500 log.error("No outbound message encoder configured for binding {}", requestContext
501 .getPeerEntityEndpoint().getBinding());
502 throw new ProfileException("No outbound message encoder configured for binding "
503 + requestContext.getPeerEntityEndpoint().getBinding());
506 AbstractSAMLProfileConfiguration profileConfig = (AbstractSAMLProfileConfiguration) requestContext
507 .getProfileConfiguration();
508 if (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.always
509 || (profileConfig.getSignResponses() == CryptoOperationRequirementLevel.conditional && !encoder
510 .providesMessageIntegrity(requestContext))) {
511 Credential signingCredential = null;
512 if (profileConfig.getSigningCredential() != null) {
513 signingCredential = profileConfig.getSigningCredential();
514 } else if (requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential() != null) {
515 signingCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
518 if (signingCredential == null) {
519 throw new ProfileException(
520 "Signing of responses is required but no signing credential is available");
523 requestContext.setOutboundSAMLMessageSigningCredential(signingCredential);
526 log.debug("Encoding response to SAML request {} from relying party {}", requestContext
527 .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
529 requestContext.setMessageEncoder(encoder);
530 encoder.encode(requestContext);
531 } catch (MessageEncodingException e) {
532 throw new ProfileException("Unable to encode response to relying party: "
533 + requestContext.getInboundMessageIssuer(), e);