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.saml2;
19 import java.util.Collection;
20 import java.util.List;
23 import org.joda.time.DateTime;
24 import org.opensaml.Configuration;
25 import org.opensaml.common.SAMLObjectBuilder;
26 import org.opensaml.common.SAMLVersion;
27 import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
28 import org.opensaml.common.xml.SAMLConstants;
29 import org.opensaml.saml2.core.Assertion;
30 import org.opensaml.saml2.core.AttributeQuery;
31 import org.opensaml.saml2.core.AttributeStatement;
32 import org.opensaml.saml2.core.Audience;
33 import org.opensaml.saml2.core.AudienceRestriction;
34 import org.opensaml.saml2.core.AuthnRequest;
35 import org.opensaml.saml2.core.Conditions;
36 import org.opensaml.saml2.core.Issuer;
37 import org.opensaml.saml2.core.NameID;
38 import org.opensaml.saml2.core.NameIDPolicy;
39 import org.opensaml.saml2.core.ProxyRestriction;
40 import org.opensaml.saml2.core.Response;
41 import org.opensaml.saml2.core.Statement;
42 import org.opensaml.saml2.core.Status;
43 import org.opensaml.saml2.core.StatusCode;
44 import org.opensaml.saml2.core.StatusMessage;
45 import org.opensaml.saml2.core.StatusResponseType;
46 import org.opensaml.saml2.core.Subject;
47 import org.opensaml.saml2.core.SubjectConfirmation;
48 import org.opensaml.saml2.core.SubjectConfirmationData;
49 import org.opensaml.saml2.encryption.Encrypter;
50 import org.opensaml.saml2.encryption.Encrypter.KeyPlacement;
51 import org.opensaml.saml2.metadata.Endpoint;
52 import org.opensaml.saml2.metadata.SPSSODescriptor;
53 import org.opensaml.security.MetadataCredentialResolver;
54 import org.opensaml.security.MetadataCriteria;
55 import org.opensaml.ws.message.encoder.MessageEncodingException;
56 import org.opensaml.ws.transport.http.HTTPInTransport;
57 import org.opensaml.xml.XMLObjectBuilder;
58 import org.opensaml.xml.encryption.EncryptionException;
59 import org.opensaml.xml.encryption.EncryptionParameters;
60 import org.opensaml.xml.encryption.KeyEncryptionParameters;
61 import org.opensaml.xml.io.Marshaller;
62 import org.opensaml.xml.io.MarshallingException;
63 import org.opensaml.xml.security.CriteriaSet;
64 import org.opensaml.xml.security.SecurityConfiguration;
65 import org.opensaml.xml.security.SecurityException;
66 import org.opensaml.xml.security.SecurityHelper;
67 import org.opensaml.xml.security.credential.Credential;
68 import org.opensaml.xml.security.credential.UsageType;
69 import org.opensaml.xml.security.criteria.EntityIDCriteria;
70 import org.opensaml.xml.security.criteria.UsageCriteria;
71 import org.opensaml.xml.signature.Signature;
72 import org.opensaml.xml.signature.SignatureException;
73 import org.opensaml.xml.signature.Signer;
74 import org.opensaml.xml.util.DatatypeHelper;
75 import org.opensaml.xml.util.Pair;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
78 import org.slf4j.helpers.MessageFormatter;
80 import edu.internet2.middleware.shibboleth.common.attribute.AttributeRequestException;
81 import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
82 import edu.internet2.middleware.shibboleth.common.attribute.encoding.AttributeEncodingException;
83 import edu.internet2.middleware.shibboleth.common.attribute.encoding.SAML2NameIDEncoder;
84 import edu.internet2.middleware.shibboleth.common.attribute.provider.SAML2AttributeAuthority;
85 import edu.internet2.middleware.shibboleth.common.log.AuditLogEntry;
86 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
87 import edu.internet2.middleware.shibboleth.common.profile.provider.BaseSAMLProfileRequestContext;
88 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.CryptoOperationRequirementLevel;
89 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.AbstractSAML2ProfileConfiguration;
90 import edu.internet2.middleware.shibboleth.idp.profile.AbstractSAMLProfileHandler;
91 import edu.internet2.middleware.shibboleth.idp.session.ServiceInformation;
92 import edu.internet2.middleware.shibboleth.idp.session.Session;
94 /** Common implementation details for profile handlers. */
95 public abstract class AbstractSAML2ProfileHandler extends AbstractSAMLProfileHandler {
97 /** SAML Version for this profile handler. */
98 public static final SAMLVersion SAML_VERSION = SAMLVersion.VERSION_20;
101 private Logger log = LoggerFactory.getLogger(AbstractSAML2ProfileHandler.class);
103 /** For building response. */
104 private SAMLObjectBuilder<Response> responseBuilder;
106 /** For building status. */
107 private SAMLObjectBuilder<Status> statusBuilder;
109 /** For building statuscode. */
110 private SAMLObjectBuilder<StatusCode> statusCodeBuilder;
112 /** For building StatusMessages. */
113 private SAMLObjectBuilder<StatusMessage> statusMessageBuilder;
115 /** For building assertion. */
116 private SAMLObjectBuilder<Assertion> assertionBuilder;
118 /** For building issuer. */
119 private SAMLObjectBuilder<Issuer> issuerBuilder;
121 /** For building subject. */
122 private SAMLObjectBuilder<Subject> subjectBuilder;
124 /** For building subject confirmation. */
125 private SAMLObjectBuilder<SubjectConfirmation> subjectConfirmationBuilder;
127 /** For building subject confirmation data. */
128 private SAMLObjectBuilder<SubjectConfirmationData> subjectConfirmationDataBuilder;
130 /** For building conditions. */
131 private SAMLObjectBuilder<Conditions> conditionsBuilder;
133 /** For building audience restriction. */
134 private SAMLObjectBuilder<AudienceRestriction> audienceRestrictionBuilder;
136 /** For building proxy restrictions. */
137 private SAMLObjectBuilder<ProxyRestriction> proxyRestrictionBuilder;
139 /** For building audience. */
140 private SAMLObjectBuilder<Audience> audienceBuilder;
142 /** For building signature. */
143 private XMLObjectBuilder<Signature> signatureBuilder;
146 @SuppressWarnings("unchecked")
147 protected AbstractSAML2ProfileHandler() {
150 responseBuilder = (SAMLObjectBuilder<Response>) getBuilderFactory().getBuilder(Response.DEFAULT_ELEMENT_NAME);
151 statusBuilder = (SAMLObjectBuilder<Status>) getBuilderFactory().getBuilder(Status.DEFAULT_ELEMENT_NAME);
152 statusCodeBuilder = (SAMLObjectBuilder<StatusCode>) getBuilderFactory().getBuilder(
153 StatusCode.DEFAULT_ELEMENT_NAME);
154 statusMessageBuilder = (SAMLObjectBuilder<StatusMessage>) getBuilderFactory().getBuilder(
155 StatusMessage.DEFAULT_ELEMENT_NAME);
156 issuerBuilder = (SAMLObjectBuilder<Issuer>) getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME);
157 assertionBuilder = (SAMLObjectBuilder<Assertion>) getBuilderFactory()
158 .getBuilder(Assertion.DEFAULT_ELEMENT_NAME);
159 subjectBuilder = (SAMLObjectBuilder<Subject>) getBuilderFactory().getBuilder(Subject.DEFAULT_ELEMENT_NAME);
160 subjectConfirmationBuilder = (SAMLObjectBuilder<SubjectConfirmation>) getBuilderFactory().getBuilder(
161 SubjectConfirmation.DEFAULT_ELEMENT_NAME);
162 subjectConfirmationDataBuilder = (SAMLObjectBuilder<SubjectConfirmationData>) getBuilderFactory().getBuilder(
163 SubjectConfirmationData.DEFAULT_ELEMENT_NAME);
164 conditionsBuilder = (SAMLObjectBuilder<Conditions>) getBuilderFactory().getBuilder(
165 Conditions.DEFAULT_ELEMENT_NAME);
166 audienceRestrictionBuilder = (SAMLObjectBuilder<AudienceRestriction>) getBuilderFactory().getBuilder(
167 AudienceRestriction.DEFAULT_ELEMENT_NAME);
168 proxyRestrictionBuilder = (SAMLObjectBuilder<ProxyRestriction>) getBuilderFactory().getBuilder(
169 ProxyRestriction.DEFAULT_ELEMENT_NAME);
170 audienceBuilder = (SAMLObjectBuilder<Audience>) getBuilderFactory().getBuilder(Audience.DEFAULT_ELEMENT_NAME);
171 signatureBuilder = (XMLObjectBuilder<Signature>) getBuilderFactory().getBuilder(Signature.DEFAULT_ELEMENT_NAME);
175 protected void populateRequestContext(BaseSAMLProfileRequestContext requestContext) throws ProfileException {
176 BaseSAML2ProfileRequestContext saml2Request = (BaseSAML2ProfileRequestContext) requestContext;
178 super.populateRequestContext(requestContext);
179 } catch (ProfileException e) {
180 if (saml2Request.getFailureStatus() == null) {
181 saml2Request.setFailureStatus(buildStatus(StatusCode.REQUESTER_URI, null, e.getMessage()));
188 * Populates the request context with the information about the user.
190 * This method requires the the following request context properties to be populated: inbound message transport,
193 * This methods populates the following request context properties: user's session, user's principal name, and
194 * service authentication method
196 * @param requestContext current request context
198 protected void populateUserInformation(BaseSAMLProfileRequestContext requestContext) {
199 Session userSession = getUserSession(requestContext.getInboundMessageTransport());
200 if (userSession == null) {
201 NameID subject = (NameID) requestContext.getSubjectNameIdentifier();
202 if (subject != null && subject.getValue() != null) {
203 userSession = getUserSession(subject.getValue());
207 if (userSession != null) {
208 requestContext.setUserSession(userSession);
209 requestContext.setPrincipalName(userSession.getPrincipalName());
210 ServiceInformation serviceInfo = userSession.getServicesInformation().get(
211 requestContext.getInboundMessageIssuer());
212 if (serviceInfo != null) {
213 requestContext.setPrincipalAuthenticationMethod(serviceInfo.getAuthenticationMethod()
214 .getAuthenticationMethod());
220 * Checks that the SAML major version for a request is 2.
222 * @param requestContext current request context containing the SAML message
224 * @throws ProfileException thrown if the major version of the SAML request is not 2
226 protected void checkSamlVersion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
227 SAMLVersion version = requestContext.getInboundSAMLMessage().getVersion();
228 if (version.getMajorVersion() < 2) {
229 requestContext.setFailureStatus(buildStatus(StatusCode.VERSION_MISMATCH_URI,
230 StatusCode.REQUEST_VERSION_TOO_LOW_URI, null));
231 throw new ProfileException("SAML request version too low");
232 } else if (version.getMajorVersion() > 2 || version.getMinorVersion() > 0) {
233 requestContext.setFailureStatus(buildStatus(StatusCode.VERSION_MISMATCH_URI,
234 StatusCode.REQUEST_VERSION_TOO_HIGH_URI, null));
235 throw new ProfileException("SAML request version too high");
240 * Builds a response to the attribute query within the request context.
242 * @param requestContext current request context
243 * @param subjectConfirmationMethod confirmation method used for the subject
244 * @param statements the statements to include in the response
246 * @return the built response
248 * @throws ProfileException thrown if there is a problem creating the SAML response
250 protected Response buildResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
251 String subjectConfirmationMethod, List<Statement> statements) throws ProfileException {
253 DateTime issueInstant = new DateTime();
255 Response samlResponse = responseBuilder.buildObject();
256 samlResponse.setIssueInstant(issueInstant);
257 populateStatusResponse(requestContext, samlResponse);
259 Assertion assertion = null;
260 if (statements != null && !statements.isEmpty()) {
261 assertion = buildAssertion(requestContext, issueInstant);
262 assertion.getStatements().addAll(statements);
263 assertion.setSubject(buildSubject(requestContext, subjectConfirmationMethod, issueInstant));
265 postProcessAssertion(requestContext, assertion);
267 signAssertion(requestContext, assertion);
269 if (isEncryptAssertion(requestContext)) {
270 log.debug("Attempting to encrypt assertion to relying party '{}'", requestContext
271 .getInboundMessageIssuer());
273 Encrypter encrypter = getEncrypter(requestContext.getInboundMessageIssuer());
274 samlResponse.getEncryptedAssertions().add(encrypter.encrypt(assertion));
275 } catch (SecurityException e) {
276 log.error("Unable to construct encrypter", e);
277 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
278 "Unable to encrypt assertion"));
279 throw new ProfileException("Unable to construct encrypter", e);
280 } catch (EncryptionException e) {
281 log.error("Unable to encrypt assertion", e);
282 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
283 "Unable to encrypt assertion"));
284 throw new ProfileException("Unable to encrypt assertion", e);
287 samlResponse.getAssertions().add(assertion);
291 Status status = buildStatus(StatusCode.SUCCESS_URI, null, null);
292 samlResponse.setStatus(status);
294 postProcessResponse(requestContext, samlResponse);
300 * Determine whether issued assertions should be encrypted.
302 * @param requestContext the current request context
303 * @return true if assertions should be encrypted, false otherwise
304 * @throws ProfileException if there is a problem determining whether assertions should be encrypted
306 protected boolean isEncryptAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
307 throws ProfileException {
309 SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
311 return requestContext.getProfileConfiguration().getEncryptAssertion() == CryptoOperationRequirementLevel.always
312 || (requestContext.getProfileConfiguration().getEncryptAssertion() == CryptoOperationRequirementLevel.conditional && !encoder
313 .providesMessageConfidentiality(requestContext));
314 } catch (MessageEncodingException e) {
315 log.error("Unable to determine if outbound encoding '{}' can provide confidentiality", encoder
317 throw new ProfileException("Unable to determine if assertions should be encrypted");
322 * Extension point for for subclasses to post-process the Response before it is signed and encoded.
324 * @param requestContext the current request context
325 * @param samlResponse the SAML Response being built
327 * @throws ProfileException if there was an error processing the response
329 protected void postProcessResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Response samlResponse)
330 throws ProfileException {
334 * Extension point for for subclasses to post-process the Assertion before it is signed and encrypted.
336 * @param requestContext the current request context
337 * @param assertion the SAML Assertion being built
339 * @throws ProfileException if there is an error processing the assertion
341 protected void postProcessAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
342 throws ProfileException {
346 * Builds a basic assertion with its id, issue instant, SAML version, issuer, subject, and conditions populated.
348 * @param requestContext current request context
349 * @param issueInstant time to use as assertion issue instant
351 * @return the built assertion
353 protected Assertion buildAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, DateTime issueInstant) {
354 Assertion assertion = assertionBuilder.buildObject();
355 assertion.setID(getIdGenerator().generateIdentifier());
356 assertion.setIssueInstant(issueInstant);
357 assertion.setVersion(SAMLVersion.VERSION_20);
358 assertion.setIssuer(buildEntityIssuer(requestContext));
360 Conditions conditions = buildConditions(requestContext, issueInstant);
361 assertion.setConditions(conditions);
367 * Creates an {@link Issuer} populated with information about the relying party.
369 * @param requestContext current request context
371 * @return the built issuer
373 protected Issuer buildEntityIssuer(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
374 Issuer issuer = issuerBuilder.buildObject();
375 issuer.setFormat(Issuer.ENTITY);
376 issuer.setValue(requestContext.getLocalEntityId());
382 * Builds a SAML assertion condition set. The following fields are set; not before, not on or after, audience
383 * restrictions, and proxy restrictions.
385 * @param requestContext current request context
386 * @param issueInstant timestamp the assertion was created
388 * @return constructed conditions
390 protected Conditions buildConditions(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, DateTime issueInstant) {
391 AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
393 Conditions conditions = conditionsBuilder.buildObject();
394 conditions.setNotBefore(issueInstant);
395 conditions.setNotOnOrAfter(issueInstant.plus(profileConfig.getAssertionLifetime()));
397 Collection<String> audiences;
399 // add audience restrictions
400 AudienceRestriction audienceRestriction = audienceRestrictionBuilder.buildObject();
401 // TODO we should only do this for certain outgoing bindings, not globally
402 Audience audience = audienceBuilder.buildObject();
403 audience.setAudienceURI(requestContext.getInboundMessageIssuer());
404 audienceRestriction.getAudiences().add(audience);
405 audiences = profileConfig.getAssertionAudiences();
406 if (audiences != null && audiences.size() > 0) {
407 for (String audienceUri : audiences) {
408 audience = audienceBuilder.buildObject();
409 audience.setAudienceURI(audienceUri);
410 audienceRestriction.getAudiences().add(audience);
413 conditions.getAudienceRestrictions().add(audienceRestriction);
415 // add proxy restrictions
416 audiences = profileConfig.getProxyAudiences();
417 if (audiences != null && audiences.size() > 0) {
418 ProxyRestriction proxyRestriction = proxyRestrictionBuilder.buildObject();
419 for (String audienceUri : audiences) {
420 audience = audienceBuilder.buildObject();
421 audience.setAudienceURI(audienceUri);
422 proxyRestriction.getAudiences().add(audience);
425 proxyRestriction.setProxyCount(profileConfig.getProxyCount());
426 conditions.getConditions().add(proxyRestriction);
433 * Populates the response's id, in response to, issue instant, version, and issuer properties.
435 * @param requestContext current request context
436 * @param response the response to populate
438 protected void populateStatusResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
439 StatusResponseType response) {
440 response.setID(getIdGenerator().generateIdentifier());
442 response.setInResponseTo(requestContext.getInboundSAMLMessageId());
443 response.setIssuer(buildEntityIssuer(requestContext));
445 response.setVersion(SAMLVersion.VERSION_20);
449 * Resolves the attributes for the principal.
451 * @param requestContext current request context
453 * @throws ProfileException thrown if there is a problem resolved attributes
455 protected void resolveAttributes(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
456 AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
457 SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
459 log.debug("Resolving attributes for principal '{}' for SAML request from relying party '{}'",
460 requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
461 Map<String, BaseAttribute> principalAttributes = attributeAuthority.getAttributes(requestContext);
463 requestContext.setAttributes(principalAttributes);
464 } catch (AttributeRequestException e) {
467 "Error resolving attributes for principal '{}'. No name identifier or attribute statement will be included in response",
468 requestContext.getPrincipalName());
473 * Executes a query for attributes and builds a SAML attribute statement from the results.
475 * @param requestContext current request context
477 * @return attribute statement resulting from the query
479 * @throws ProfileException thrown if there is a problem making the query
481 protected AttributeStatement buildAttributeStatement(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
482 throws ProfileException {
483 if (requestContext.getAttributes() == null) {
487 log.debug("Creating attribute statement in response to SAML request '{}' from relying party '{}'",
488 requestContext.getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
490 AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
491 SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
493 if (requestContext.getInboundSAMLMessage() instanceof AttributeQuery) {
494 return attributeAuthority.buildAttributeStatement((AttributeQuery) requestContext
495 .getInboundSAMLMessage(), requestContext.getAttributes().values());
497 return attributeAuthority.buildAttributeStatement(null, requestContext.getAttributes().values());
499 } catch (AttributeRequestException e) {
500 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Error resolving attributes"));
501 String msg = MessageFormatter.format("Error encoding attributes for principal '{}'", requestContext
502 .getPrincipalName());
504 throw new ProfileException(msg, e);
509 * Resolves the principal name of the subject of the request.
511 * @param requestContext current request context
513 * @throws ProfileException thrown if the principal name can not be resolved
515 protected void resolvePrincipal(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
516 AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
517 if (profileConfiguration == null) {
518 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI,
519 "Error resolving principal"));
520 String msg = MessageFormatter.format(
521 "Unable to resolve principal, no SAML 2 profile configuration for relying party '{}'",
522 requestContext.getInboundMessageIssuer());
524 throw new ProfileException(msg);
526 SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
527 log.debug("Resolving principal name for subject of SAML request '{}' from relying party '{}'", requestContext
528 .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
531 String principal = attributeAuthority.getPrincipal(requestContext);
532 requestContext.setPrincipalName(principal);
533 } catch (AttributeRequestException e) {
534 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.UNKNOWN_PRINCIPAL_URI,
535 "Error resolving principal"));
536 String msg = MessageFormatter.arrayFormat(
537 "Error resolving principal name for SAML request '{}' from relying party '{}'. Cause: {}", new Object[]{requestContext
538 .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer(),e.getMessage()});
540 throw new ProfileException(msg, e);
545 * Signs the given assertion if either the current profile configuration or the relying party configuration contains
546 * signing credentials.
548 * @param requestContext current request context
549 * @param assertion assertion to sign
551 * @throws ProfileException thrown if the metadata can not be located for the relying party or, if signing is
552 * required, if a signing credential is not configured
554 protected void signAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
555 throws ProfileException {
556 log.debug("Determining if SAML assertion to relying party '{}' should be signed", requestContext
557 .getInboundMessageIssuer());
559 boolean signAssertion = isSignAssertion(requestContext);
561 if (!signAssertion) {
565 AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
567 log.debug("Determining signing credntial for assertion to relying party '{}'", requestContext
568 .getInboundMessageIssuer());
569 Credential signatureCredential = profileConfig.getSigningCredential();
570 if (signatureCredential == null) {
571 signatureCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
574 if (signatureCredential == null) {
575 String msg = MessageFormatter.format(
576 "No signing credential is specified for relying party configuration '{}'", requestContext
577 .getRelyingPartyConfiguration().getProviderId());
579 throw new ProfileException(msg);
582 log.debug("Signing assertion to relying party {}", requestContext.getInboundMessageIssuer());
583 Signature signature = signatureBuilder.buildObject(Signature.DEFAULT_ELEMENT_NAME);
585 signature.setSigningCredential(signatureCredential);
587 // TODO pull SecurityConfiguration from SAMLMessageContext? needs to be added
588 // TODO how to pull what keyInfoGenName to use?
589 SecurityHelper.prepareSignatureParams(signature, signatureCredential, null, null);
590 } catch (SecurityException e) {
591 String msg = "Error preparing signature for signing";
593 throw new ProfileException(msg, e);
596 assertion.setSignature(signature);
598 Marshaller assertionMarshaller = Configuration.getMarshallerFactory().getMarshaller(assertion);
600 assertionMarshaller.marshall(assertion);
601 Signer.signObject(signature);
602 } catch (MarshallingException e) {
603 String errMsg = "Unable to marshall assertion for signing";
604 log.error(errMsg, e);
605 throw new ProfileException(errMsg, e);
606 } catch (SignatureException e) {
607 String msg = "Unable to sign assertion";
609 throw new ProfileException(msg, e);
614 * Determine whether issued assertions should be signed.
616 * @param requestContext the current request context
617 * @return true if assertions should be signed, false otherwise
618 * @throws ProfileException if there is a problem determining whether assertions should be signed
620 protected boolean isSignAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
622 SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
623 AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
626 boolean signAssertion = profileConfig.getSignAssertions() == CryptoOperationRequirementLevel.always
627 || (profileConfig.getSignAssertions() == CryptoOperationRequirementLevel.conditional && !encoder
628 .providesMessageIntegrity(requestContext));
630 log.debug("IdP relying party configuration '{}' indicates to sign assertions: {}", requestContext
631 .getRelyingPartyConfiguration().getRelyingPartyId(), signAssertion);
633 if (!signAssertion && requestContext.getPeerEntityRoleMetadata() instanceof SPSSODescriptor) {
634 SPSSODescriptor ssoDescriptor = (SPSSODescriptor) requestContext.getPeerEntityRoleMetadata();
635 if (ssoDescriptor.getWantAssertionsSigned() != null) {
636 signAssertion = ssoDescriptor.getWantAssertionsSigned().booleanValue();
637 log.debug("Entity metadata for relying party '{} 'indicates to sign assertions: {}", requestContext
638 .getInboundMessageIssuer(), signAssertion);
642 return signAssertion;
643 } catch (MessageEncodingException e) {
644 log.error("Unable to determine if outbound encoding '{}' provides message integrity protection", encoder
646 throw new ProfileException("Unable to determine if outbound assertion should be signed");
651 * Build a status message, with an optional second-level failure message.
653 * @param topLevelCode The top-level status code. Should be from saml-core-2.0-os, sec. 3.2.2.2
654 * @param secondLevelCode An optional second-level failure code. Should be from saml-core-2.0-is, sec 3.2.2.2. If
655 * null, no second-level Status element will be set.
656 * @param failureMessage An optional second-level failure message
658 * @return a Status object.
660 protected Status buildStatus(String topLevelCode, String secondLevelCode, String failureMessage) {
661 Status status = statusBuilder.buildObject();
663 StatusCode statusCode = statusCodeBuilder.buildObject();
664 statusCode.setValue(DatatypeHelper.safeTrimOrNullString(topLevelCode));
665 status.setStatusCode(statusCode);
667 if (secondLevelCode != null) {
668 StatusCode secondLevelStatusCode = statusCodeBuilder.buildObject();
669 secondLevelStatusCode.setValue(DatatypeHelper.safeTrimOrNullString(secondLevelCode));
670 statusCode.setStatusCode(secondLevelStatusCode);
673 if (failureMessage != null) {
674 StatusMessage msg = statusMessageBuilder.buildObject();
675 msg.setMessage(failureMessage);
676 status.setStatusMessage(msg);
683 * Builds the SAML subject for the user for the service provider.
685 * @param requestContext current request context
686 * @param confirmationMethod subject confirmation method used for the subject
687 * @param issueInstant instant the subject confirmation data should reflect for issuance
689 * @return SAML subject for the user for the service provider
691 * @throws ProfileException thrown if a NameID can not be created either because there was a problem encoding the
692 * name ID attribute or because there are no supported name formats
694 protected Subject buildSubject(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, String confirmationMethod,
695 DateTime issueInstant) throws ProfileException {
696 Subject subject = subjectBuilder.buildObject();
697 subject.getSubjectConfirmations().add(
698 buildSubjectConfirmation(requestContext, confirmationMethod, issueInstant));
700 NameID nameID = buildNameId(requestContext);
701 if (nameID == null) {
705 requestContext.setSubjectNameIdentifier(nameID);
707 if (isEncryptNameID(requestContext)) {
708 log.debug("Attempting to encrypt NameID to relying party '{}'", requestContext.getInboundMessageIssuer());
710 Encrypter encrypter = getEncrypter(requestContext.getInboundMessageIssuer());
711 subject.setEncryptedID(encrypter.encrypt(nameID));
712 } catch (SecurityException e) {
713 log.error("Unable to construct encrypter", e);
715 .setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to encrypt NameID"));
716 throw new ProfileException("Unable to construct encrypter", e);
717 } catch (EncryptionException e) {
718 log.error("Unable to encrypt NameID", e);
720 .setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to encrypt NameID"));
721 throw new ProfileException("Unable to encrypt NameID", e);
724 subject.setNameID(nameID);
731 * Determine whether NameID's should be encrypted.
733 * @param requestContext the current request context
734 * @return true if NameID's should be encrypted, false otherwise
735 * @throws ProfileException if there is a problem determining whether NameID's should be encrypted
737 protected boolean isEncryptNameID(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
739 boolean nameIdEncRequiredByAuthnRequest = isRequestRequiresEncryptNameID(requestContext);
741 SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
742 boolean nameIdEncRequiredByConfig = false;
744 nameIdEncRequiredByConfig = requestContext.getProfileConfiguration().getEncryptNameID() == CryptoOperationRequirementLevel.always
745 || (requestContext.getProfileConfiguration().getEncryptNameID() == CryptoOperationRequirementLevel.conditional && !encoder
746 .providesMessageConfidentiality(requestContext));
747 } catch (MessageEncodingException e) {
748 String msg = MessageFormatter.format(
749 "Unable to determine if outbound encoding '{}' provides message confidentiality protection",
750 encoder.getBindingURI());
752 throw new ProfileException(msg);
755 return nameIdEncRequiredByAuthnRequest || nameIdEncRequiredByConfig;
759 * Determine whether information in the SAML request requires the issued NameID to be encrypted.
761 * @param requestContext the current request context
762 * @return true if the request indicates NameID encryption is required, false otherwise
764 protected boolean isRequestRequiresEncryptNameID(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
765 boolean nameIdEncRequiredByAuthnRequest = false;
766 if (requestContext.getInboundSAMLMessage() instanceof AuthnRequest) {
767 AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
768 NameIDPolicy policy = authnRequest.getNameIDPolicy();
769 if (policy != null && DatatypeHelper.safeEquals(policy.getFormat(), NameID.ENCRYPTED)) {
770 nameIdEncRequiredByAuthnRequest = true;
773 return nameIdEncRequiredByAuthnRequest;
777 * Builds the SubjectConfirmation appropriate for this request.
779 * @param requestContext current request context
780 * @param confirmationMethod confirmation method to use for the request
781 * @param issueInstant issue instant of the response
783 * @return the constructed subject confirmation
785 protected SubjectConfirmation buildSubjectConfirmation(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
786 String confirmationMethod, DateTime issueInstant) {
787 SubjectConfirmationData confirmationData = subjectConfirmationDataBuilder.buildObject();
788 HTTPInTransport inTransport = (HTTPInTransport) requestContext.getInboundMessageTransport();
789 confirmationData.setAddress(inTransport.getPeerAddress());
790 confirmationData.setInResponseTo(requestContext.getInboundSAMLMessageId());
791 confirmationData.setNotOnOrAfter(issueInstant.plus(requestContext.getProfileConfiguration()
792 .getAssertionLifetime()));
794 Endpoint relyingPartyEndpoint = requestContext.getPeerEntityEndpoint();
795 if (relyingPartyEndpoint != null) {
796 if (relyingPartyEndpoint.getResponseLocation() != null) {
797 confirmationData.setRecipient(relyingPartyEndpoint.getResponseLocation());
799 confirmationData.setRecipient(relyingPartyEndpoint.getLocation());
803 SubjectConfirmation subjectConfirmation = subjectConfirmationBuilder.buildObject();
804 subjectConfirmation.setMethod(confirmationMethod);
805 subjectConfirmation.setSubjectConfirmationData(confirmationData);
807 return subjectConfirmation;
811 * Builds a NameID appropriate for this request. NameIDs are built by inspecting the SAML request and metadata,
812 * picking a name format that was requested by the relying party or is mutually supported by both the relying party
813 * and asserting party as described in their metadata entries. Once a set of supported name formats is determined
814 * the principals attributes are inspected for an attribute supported an attribute encoder whose category is one of
815 * the supported name formats.
817 * @param requestContext current request context
819 * @return the NameID appropriate for this request
821 * @throws ProfileException thrown if a NameID can not be created either because there was a problem encoding the
822 * name ID attribute or because there are no supported name formats
824 protected NameID buildNameId(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
825 log.debug("Attemping to build NameID for principal '{}' in response to request from relying party '{}",
826 requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
828 Pair<BaseAttribute, SAML2NameIDEncoder> nameIdAttributeAndEncoder = null;
830 nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(SAML2NameIDEncoder.class, requestContext);
831 } catch (ProfileException e) {
832 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.INVALID_NAMEID_POLICY_URI,
833 "Required NameID format not supported"));
837 BaseAttribute<?> nameIdAttribute = nameIdAttributeAndEncoder.getFirst();
838 SAML2NameIDEncoder nameIdEncoder = nameIdAttributeAndEncoder.getSecond();
840 log.debug("Using attribute '{}' supporting NameID format '{}' to create the NameID for relying party '{}'",
841 new Object[] { nameIdAttribute.getId(), nameIdEncoder.getNameFormat(),
842 requestContext.getInboundMessageIssuer(), });
844 // build the actual NameID
845 NameID nameId = nameIdEncoder.encode(nameIdAttribute);
846 nameId.setNameQualifier(requestContext.getRelyingPartyConfiguration().getProviderId());
848 } catch (AttributeEncodingException e) {
849 log.error("Unable to encode NameID attribute", e);
850 requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to construct NameID"));
851 throw new ProfileException("Unable to encode NameID attribute", e);
856 * Constructs an SAML response message carrying a request error.
858 * @param requestContext current request context
860 * @return the constructed error response
862 protected Response buildErrorResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
863 Response samlResponse = responseBuilder.buildObject();
864 samlResponse.setIssueInstant(new DateTime());
865 populateStatusResponse(requestContext, samlResponse);
867 samlResponse.setStatus(requestContext.getFailureStatus());
873 * Gets an encrypter that may be used encrypt content to a given peer.
875 * @param peerEntityId entity ID of the peer
877 * @return encrypter that may be used encrypt content to a given peer
879 * @throws SecurityException thrown if there is a problem constructing the encrypter. This normally occurs if the
880 * key encryption credential for the peer can not be resolved or a required encryption algorithm is not
881 * supported by the VM's JCE.
883 protected Encrypter getEncrypter(String peerEntityId) throws SecurityException {
884 SecurityConfiguration securityConfiguration = Configuration.getGlobalSecurityConfiguration();
886 EncryptionParameters dataEncParams = SecurityHelper
887 .buildDataEncryptionParams(null, securityConfiguration, null);
889 Credential keyEncryptionCredential = getKeyEncryptionCredential(peerEntityId);
890 if (keyEncryptionCredential == null) {
891 log.error("Could not resolve a key encryption credential for peer entity: {}", peerEntityId);
892 throw new SecurityException("Could not resolve key encryption credential");
894 String wrappedJCAKeyAlgorithm = SecurityHelper.getKeyAlgorithmFromURI(dataEncParams.getAlgorithm());
895 KeyEncryptionParameters keyEncParams = SecurityHelper.buildKeyEncryptionParams(keyEncryptionCredential,
896 wrappedJCAKeyAlgorithm, securityConfiguration, null, null);
898 Encrypter encrypter = new Encrypter(dataEncParams, keyEncParams);
899 encrypter.setKeyPlacement(KeyPlacement.INLINE);
904 * Gets the credential that can be used to encrypt encryption keys for a peer.
906 * @param peerEntityId entity ID of the peer
908 * @return credential that can be used to encrypt encryption keys for a peer
910 * @throws SecurityException thrown if there is a problem resolving the credential from the peer's metadata
912 protected Credential getKeyEncryptionCredential(String peerEntityId) throws SecurityException {
913 MetadataCredentialResolver kekCredentialResolver = new MetadataCredentialResolver(getMetadataProvider());
915 CriteriaSet criteriaSet = new CriteriaSet();
916 criteriaSet.add(new EntityIDCriteria(peerEntityId));
917 criteriaSet.add(new MetadataCriteria(SPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
918 criteriaSet.add(new UsageCriteria(UsageType.ENCRYPTION));
920 return kekCredentialResolver.resolveSingle(criteriaSet);
924 * Writes an audit log entry indicating the successful response to the attribute request.
926 * @param context current request context
928 protected void writeAuditLogEntry(BaseSAMLProfileRequestContext context) {
929 SAML2AuditLogEntry auditLogEntry = new SAML2AuditLogEntry();
930 auditLogEntry.setSAMLResponse((StatusResponseType) context.getOutboundSAMLMessage());
931 auditLogEntry.setMessageProfile(getProfileId());
932 auditLogEntry.setPrincipalAuthenticationMethod(context.getPrincipalAuthenticationMethod());
933 auditLogEntry.setPrincipalName(context.getPrincipalName());
934 auditLogEntry.setAssertingPartyId(context.getLocalEntityId());
935 auditLogEntry.setRelyingPartyId(context.getInboundMessageIssuer());
936 auditLogEntry.setRequestBinding(context.getMessageDecoder().getBindingURI());
937 auditLogEntry.setRequestId(context.getInboundSAMLMessageId());
938 auditLogEntry.setResponseBinding(context.getMessageEncoder().getBindingURI());
939 auditLogEntry.setResponseId(context.getOutboundSAMLMessageId());
940 if (context.getReleasedAttributes() != null) {
941 auditLogEntry.getReleasedAttributes().addAll(context.getReleasedAttributes());
944 getAduitLog().info(auditLogEntry.toString());
947 /** SAML 1 specific audit log entry. */
948 protected class SAML2AuditLogEntry extends AuditLogEntry {
950 /** The response to the SAML request. */
951 private StatusResponseType samlResponse;
953 /** The unencrypted NameID for the SAML response. */
954 private NameID unencryptedNameId;
957 * Gets the response to the SAML request.
959 * @return the response to the SAML request
961 public StatusResponseType getSAMLResponse() {
966 * Sets the response to the SAML request.
968 * @param response the response to the SAML request
970 public void setSAMLResponse(StatusResponseType response) {
971 samlResponse = response;
975 * Gets the unencrypted NameID for the SAML response.
977 * @return unencrypted NameID for the SAML response
979 public NameID getUnencryptedNameId() {
980 return unencryptedNameId;
984 * Sets the unencrypted NameID for the SAML response.
986 * @param id unencrypted NameID for the SAML response
988 public void setUnencryptedNameId(NameID id) {
989 unencryptedNameId = id;
993 public String toString() {
994 StringBuilder entryString = new StringBuilder(super.toString());
996 StringBuilder assertionIds = new StringBuilder();
998 if (samlResponse instanceof Response) {
999 List<Assertion> assertions = ((Response) samlResponse).getAssertions();
1000 if (assertions != null && !assertions.isEmpty()) {
1001 for (Assertion assertion : assertions) {
1002 assertionIds.append(assertion.getID());
1003 assertionIds.append(",");
1008 if (unencryptedNameId != null) {
1009 entryString.append(unencryptedNameId.getValue());
1011 entryString.append("|");
1013 entryString.append(assertionIds.toString());
1014 entryString.append("|");
1016 return entryString.toString();