Support for conditional encryption and signing based on what the transport and bindin...
[java-idp.git] / src / edu / internet2 / middleware / shibboleth / idp / profile / saml2 / AbstractSAML2ProfileHandler.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.saml2;
18
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.Map;
23
24 import org.joda.time.DateTime;
25 import org.opensaml.Configuration;
26 import org.opensaml.common.SAMLObjectBuilder;
27 import org.opensaml.common.SAMLVersion;
28 import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
29 import org.opensaml.common.xml.SAMLConstants;
30 import org.opensaml.saml2.core.Assertion;
31 import org.opensaml.saml2.core.AttributeQuery;
32 import org.opensaml.saml2.core.AttributeStatement;
33 import org.opensaml.saml2.core.Audience;
34 import org.opensaml.saml2.core.AudienceRestriction;
35 import org.opensaml.saml2.core.AuthnRequest;
36 import org.opensaml.saml2.core.Conditions;
37 import org.opensaml.saml2.core.Issuer;
38 import org.opensaml.saml2.core.NameID;
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.AttributeAuthorityDescriptor;
52 import org.opensaml.saml2.metadata.AuthnAuthorityDescriptor;
53 import org.opensaml.saml2.metadata.Endpoint;
54 import org.opensaml.saml2.metadata.NameIDFormat;
55 import org.opensaml.saml2.metadata.PDPDescriptor;
56 import org.opensaml.saml2.metadata.RoleDescriptor;
57 import org.opensaml.saml2.metadata.SPSSODescriptor;
58 import org.opensaml.saml2.metadata.SSODescriptor;
59 import org.opensaml.security.MetadataCredentialResolver;
60 import org.opensaml.security.MetadataCriteria;
61 import org.opensaml.ws.message.encoder.MessageEncodingException;
62 import org.opensaml.ws.transport.http.HTTPInTransport;
63 import org.opensaml.xml.XMLObjectBuilder;
64 import org.opensaml.xml.encryption.EncryptionException;
65 import org.opensaml.xml.encryption.EncryptionParameters;
66 import org.opensaml.xml.encryption.KeyEncryptionParameters;
67 import org.opensaml.xml.io.Marshaller;
68 import org.opensaml.xml.io.MarshallingException;
69 import org.opensaml.xml.security.CriteriaSet;
70 import org.opensaml.xml.security.SecurityConfiguration;
71 import org.opensaml.xml.security.SecurityException;
72 import org.opensaml.xml.security.SecurityHelper;
73 import org.opensaml.xml.security.credential.Credential;
74 import org.opensaml.xml.security.credential.UsageType;
75 import org.opensaml.xml.security.criteria.EntityIDCriteria;
76 import org.opensaml.xml.security.criteria.UsageCriteria;
77 import org.opensaml.xml.signature.Signature;
78 import org.opensaml.xml.signature.Signer;
79 import org.opensaml.xml.util.DatatypeHelper;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 import edu.internet2.middleware.shibboleth.common.attribute.AttributeRequestException;
84 import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
85 import edu.internet2.middleware.shibboleth.common.attribute.encoding.AttributeEncoder;
86 import edu.internet2.middleware.shibboleth.common.attribute.encoding.AttributeEncodingException;
87 import edu.internet2.middleware.shibboleth.common.attribute.encoding.SAML2NameIDEncoder;
88 import edu.internet2.middleware.shibboleth.common.attribute.provider.SAML2AttributeAuthority;
89 import edu.internet2.middleware.shibboleth.common.profile.ProfileException;
90 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.CryptoOperationRequirementLevel;
91 import edu.internet2.middleware.shibboleth.common.relyingparty.provider.saml2.AbstractSAML2ProfileConfiguration;
92 import edu.internet2.middleware.shibboleth.idp.profile.AbstractSAMLProfileHandler;
93
94 /** Common implementation details for profile handlers. */
95 public abstract class AbstractSAML2ProfileHandler extends AbstractSAMLProfileHandler {
96
97     /** SAML Version for this profile handler. */
98     public static final SAMLVersion SAML_VERSION = SAMLVersion.VERSION_20;
99
100     /** Class logger. */
101     private Logger log = LoggerFactory.getLogger(AbstractSAML2ProfileHandler.class);
102
103     /** For building response. */
104     private SAMLObjectBuilder<Response> responseBuilder;
105
106     /** For building status. */
107     private SAMLObjectBuilder<Status> statusBuilder;
108
109     /** For building statuscode. */
110     private SAMLObjectBuilder<StatusCode> statusCodeBuilder;
111
112     /** For building StatusMessages. */
113     private SAMLObjectBuilder<StatusMessage> statusMessageBuilder;
114
115     /** For building assertion. */
116     private SAMLObjectBuilder<Assertion> assertionBuilder;
117
118     /** For building issuer. */
119     private SAMLObjectBuilder<Issuer> issuerBuilder;
120
121     /** For building subject. */
122     private SAMLObjectBuilder<Subject> subjectBuilder;
123
124     /** For building subject confirmation. */
125     private SAMLObjectBuilder<SubjectConfirmation> subjectConfirmationBuilder;
126
127     /** For building subject confirmation data. */
128     private SAMLObjectBuilder<SubjectConfirmationData> subjectConfirmationDataBuilder;
129
130     /** For building conditions. */
131     private SAMLObjectBuilder<Conditions> conditionsBuilder;
132
133     /** For building audience restriction. */
134     private SAMLObjectBuilder<AudienceRestriction> audienceRestrictionBuilder;
135
136     /** For building proxy retrictions. */
137     private SAMLObjectBuilder<ProxyRestriction> proxyRestrictionBuilder;
138
139     /** For building audience. */
140     private SAMLObjectBuilder<Audience> audienceBuilder;
141
142     /** For building signature. */
143     private XMLObjectBuilder<Signature> signatureBuilder;
144
145     /** Constructor. */
146     @SuppressWarnings("unchecked")
147     protected AbstractSAML2ProfileHandler() {
148         super();
149
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);
172     }
173
174     /**
175      * Checks that the SAML major version for a request is 2.
176      * 
177      * @param requestContext current request context containing the SAML message
178      * 
179      * @throws ProfileException thrown if the major version of the SAML request is not 2
180      */
181     protected void checkSamlVersion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
182         SAMLVersion version = requestContext.getInboundSAMLMessage().getVersion();
183         if (version.getMajorVersion() < 2) {
184             requestContext.setFailureStatus(buildStatus(StatusCode.VERSION_MISMATCH_URI,
185                     StatusCode.REQUEST_VERSION_TOO_LOW_URI, null));
186             throw new ProfileException("SAML request version too low");
187         } else if (version.getMajorVersion() > 2 || version.getMinorVersion() > 0) {
188             requestContext.setFailureStatus(buildStatus(StatusCode.VERSION_MISMATCH_URI,
189                     StatusCode.REQUEST_VERSION_TOO_HIGH_URI, null));
190             throw new ProfileException("SAML request version too high");
191         }
192     }
193
194     /**
195      * Builds a response to the attribute query within the request context.
196      * 
197      * @param requestContext current request context
198      * @param subjectConfirmationMethod confirmation method used for the subject
199      * @param statements the statements to include in the response
200      * 
201      * @return the built response
202      * 
203      * @throws ProfileException thrown if there is a problem creating the SAML response
204      */
205     protected Response buildResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
206             String subjectConfirmationMethod, List<Statement> statements) throws ProfileException {
207
208         DateTime issueInstant = new DateTime();
209
210         Subject subject = buildSubject(requestContext, subjectConfirmationMethod, issueInstant);
211
212         // create the assertion and add the attribute statement
213         Assertion assertion = buildAssertion(requestContext, issueInstant);
214         assertion.setSubject(subject);
215         if (statements != null && !statements.isEmpty()) {
216             assertion.getStatements().addAll(statements);
217         }
218
219         // create the SAML response and add the assertion
220         Response samlResponse = responseBuilder.buildObject();
221         samlResponse.setIssueInstant(issueInstant);
222         populateStatusResponse(requestContext, samlResponse);
223
224         // sign the assertion if it should be signed
225         signAssertion(requestContext, assertion);
226
227         SAMLMessageEncoder encoder = getMessageEncoders().get(requestContext.getPeerEntityEndpoint().getBinding());
228         try {
229             if (requestContext.getProfileConfiguration().getEncryptAssertion() == CryptoOperationRequirementLevel.always
230                     || (requestContext.getProfileConfiguration().getEncryptAssertion() == CryptoOperationRequirementLevel.conditional && !encoder
231                             .providesMessageConfidentiality(requestContext))) {
232                 log.debug("Attempting to encrypt assertion to relying party {}", requestContext
233                         .getInboundMessageIssuer());
234                 try {
235                     Encrypter encrypter = getEncrypter(requestContext.getInboundMessageIssuer());
236                     samlResponse.getEncryptedAssertions().add(encrypter.encrypt(assertion));
237                 } catch (SecurityException e) {
238                     log.error("Unable to construct encrypter", e);
239                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
240                             "Unable to encrypt assertion"));
241                     throw new ProfileException("Unable to construct encrypter", e);
242                 } catch (EncryptionException e) {
243                     log.error("Unable to encrypt assertion", e);
244                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
245                             "Unable to encrypt assertion"));
246                     throw new ProfileException("Unable to encrypt assertion", e);
247                 }
248             } else {
249                 samlResponse.getAssertions().add(assertion);
250             }
251         } catch (MessageEncodingException e) {
252             log.error("Unable to determine if outbound encoding {} can provide confidentiality", encoder
253                     .getBindingURI());
254             throw new ProfileException("Unable to determine if assertions should be encrypted");
255         }
256
257         Status status = buildStatus(StatusCode.SUCCESS_URI, null, null);
258         samlResponse.setStatus(status);
259
260         return samlResponse;
261     }
262
263     /**
264      * Builds a basic assertion with its id, issue instant, SAML version, issuer, subject, and conditions populated.
265      * 
266      * @param requestContext current request context
267      * @param issueInstant time to use as assertion issue instant
268      * 
269      * @return the built assertion
270      */
271     protected Assertion buildAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, DateTime issueInstant) {
272         Assertion assertion = assertionBuilder.buildObject();
273         assertion.setID(getIdGenerator().generateIdentifier());
274         assertion.setIssueInstant(issueInstant);
275         assertion.setVersion(SAMLVersion.VERSION_20);
276         assertion.setIssuer(buildEntityIssuer(requestContext));
277
278         Conditions conditions = buildConditions(requestContext, issueInstant);
279         assertion.setConditions(conditions);
280
281         return assertion;
282     }
283
284     /**
285      * Creates an {@link Issuer} populated with information about the relying party.
286      * 
287      * @param requestContext current request context
288      * 
289      * @return the built issuer
290      */
291     protected Issuer buildEntityIssuer(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
292         Issuer issuer = issuerBuilder.buildObject();
293         issuer.setFormat(Issuer.ENTITY);
294         issuer.setValue(requestContext.getLocalEntityId());
295
296         return issuer;
297     }
298
299     /**
300      * Builds a SAML assertion condition set. The following fields are set; not before, not on or after, audience
301      * restrictions, and proxy restrictions.
302      * 
303      * @param requestContext current request context
304      * @param issueInstant timestamp the assertion was created
305      * 
306      * @return constructed conditions
307      */
308     protected Conditions buildConditions(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, DateTime issueInstant) {
309         AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
310
311         Conditions conditions = conditionsBuilder.buildObject();
312         conditions.setNotBefore(issueInstant);
313         conditions.setNotOnOrAfter(issueInstant.plus(profileConfig.getAssertionLifetime()));
314
315         Collection<String> audiences;
316
317         // add audience restrictions
318         AudienceRestriction audienceRestriction = audienceRestrictionBuilder.buildObject();
319         // TODO we should only do this for certain outgoing bindings, not globally
320         Audience audience = audienceBuilder.buildObject();
321         audience.setAudienceURI(requestContext.getInboundMessageIssuer());
322         audienceRestriction.getAudiences().add(audience);
323         audiences = profileConfig.getAssertionAudiences();
324         if (audiences != null && audiences.size() > 0) {
325             for (String audienceUri : audiences) {
326                 audience = audienceBuilder.buildObject();
327                 audience.setAudienceURI(audienceUri);
328                 audienceRestriction.getAudiences().add(audience);
329             }
330         }
331         conditions.getAudienceRestrictions().add(audienceRestriction);
332
333         // add proxy restrictions
334         audiences = profileConfig.getProxyAudiences();
335         if (audiences != null && audiences.size() > 0) {
336             ProxyRestriction proxyRestriction = proxyRestrictionBuilder.buildObject();
337             for (String audienceUri : audiences) {
338                 audience = audienceBuilder.buildObject();
339                 audience.setAudienceURI(audienceUri);
340                 proxyRestriction.getAudiences().add(audience);
341             }
342
343             proxyRestriction.setProxyCount(profileConfig.getProxyCount());
344             conditions.getConditions().add(proxyRestriction);
345         }
346
347         return conditions;
348     }
349
350     /**
351      * Populates the response's id, in response to, issue instant, version, and issuer properties.
352      * 
353      * @param requestContext current request context
354      * @param response the response to populate
355      */
356     protected void populateStatusResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
357             StatusResponseType response) {
358         response.setID(getIdGenerator().generateIdentifier());
359         if (requestContext.getInboundSAMLMessage() != null) {
360             response.setInResponseTo(requestContext.getInboundSAMLMessageId());
361         }
362         response.setVersion(SAMLVersion.VERSION_20);
363         response.setIssuer(buildEntityIssuer(requestContext));
364     }
365
366     /**
367      * Resolves the attributes for the principal.
368      * 
369      * @param requestContext current request context
370      * 
371      * @throws ProfileException thrown if there is a problem resolved attributes
372      */
373     protected void resolveAttributes(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
374         AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
375         SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
376
377         try {
378             log.debug("Resolving attributes for principal {} of SAML request from relying party {}", requestContext
379                     .getPrincipalName(), requestContext.getInboundMessageIssuer());
380             Map<String, BaseAttribute> principalAttributes = attributeAuthority.getAttributes(requestContext);
381
382             requestContext.setAttributes(principalAttributes);
383         } catch (AttributeRequestException e) {
384             log.error("Error resolving attributes for SAML request " + requestContext.getInboundSAMLMessageId()
385                     + " from relying party " + requestContext.getInboundMessageIssuer(), e);
386             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Error resolving attributes"));
387             throw new ProfileException("Error resolving attributes for SAML request "
388                     + requestContext.getInboundSAMLMessageId() + " from relying party "
389                     + requestContext.getInboundMessageIssuer(), e);
390         }
391     }
392
393     /**
394      * Executes a query for attributes and builds a SAML attribute statement from the results.
395      * 
396      * @param requestContext current request context
397      * 
398      * @return attribute statement resulting from the query
399      * 
400      * @throws ProfileException thrown if there is a problem making the query
401      */
402     protected AttributeStatement buildAttributeStatement(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
403             throws ProfileException {
404         log.debug("Creating attribute statement in response to SAML request {} from relying party {}", requestContext
405                 .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
406
407         AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
408         SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
409         try {
410             if (requestContext.getInboundSAMLMessage() instanceof AttributeQuery) {
411                 return attributeAuthority.buildAttributeStatement((AttributeQuery) requestContext
412                         .getInboundSAMLMessage(), requestContext.getPrincipalAttributes().values());
413             } else {
414                 return attributeAuthority.buildAttributeStatement(null, requestContext.getPrincipalAttributes()
415                         .values());
416             }
417         } catch (AttributeRequestException e) {
418             log.error("Error encoding attributes for principal " + requestContext.getPrincipalName(), e);
419             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Error resolving attributes"));
420             throw new ProfileException("Error encoding attributes for principal " + requestContext.getPrincipalName(),
421                     e);
422         }
423     }
424
425     /**
426      * Resolves the principal name of the subject of the request.
427      * 
428      * @param requestContext current request context
429      * 
430      * @throws ProfileException thrown if the principal name can not be resolved
431      */
432     protected void resolvePrincipal(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
433         AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
434         if (profileConfiguration == null) {
435             log.error("Unable to resolve principal, no SAML 2 profile configuration for relying party "
436                     + requestContext.getInboundMessageIssuer());
437             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI,
438                     "Error resolving principal"));
439             throw new ProfileException(
440                     "Unable to resolve principal, no SAML 2 profile configuration for relying party "
441                             + requestContext.getInboundMessageIssuer());
442         }
443         SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
444         log.debug("Resolving principal name for subject of SAML request {} from relying party {}", requestContext
445                 .getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
446
447         try {
448             String principal = attributeAuthority.getPrincipal(requestContext);
449             requestContext.setPrincipalName(principal);
450         } catch (AttributeRequestException e) {
451             log.error("Error resolving attributes for SAML request " + requestContext.getInboundSAMLMessageId()
452                     + " from relying party " + requestContext.getInboundMessageIssuer(), e);
453             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.UNKNOWN_PRINCIPAL_URI,
454                     "Error resolving principal"));
455             throw new ProfileException("Error resolving attributes for SAML request "
456                     + requestContext.getInboundSAMLMessageId() + " from relying party "
457                     + requestContext.getInboundMessageIssuer(), e);
458         }
459     }
460
461     /**
462      * Signs the given assertion if either the current profile configuration or the relying party configuration contains
463      * signing credentials.
464      * 
465      * @param requestContext current request context
466      * @param assertion assertion to sign
467      * 
468      * @throws ProfileException thrown if the metadata can not be located for the relying party or, if signing is
469      *             required, if a signing credential is not configured
470      */
471     protected void signAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
472             throws ProfileException {
473         log.debug("Determining if SAML assertion to relying party {} should be signed", requestContext
474                 .getInboundMessageIssuer());
475
476         boolean signAssertion = false;
477
478         SAMLMessageEncoder encoder = getMessageEncoders().get(requestContext.getPeerEntityEndpoint().getBinding());
479         AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
480         try {
481             if (profileConfig.getSignAssertions() == CryptoOperationRequirementLevel.always
482                     || (profileConfig.getSignAssertions() == CryptoOperationRequirementLevel.conditional && !encoder
483                             .providesMessageIntegrity(requestContext))) {
484                 signAssertion = true;
485                 log.debug("IdP relying party configuration {} indicates to sign assertions: {}", requestContext
486                         .getRelyingPartyConfiguration().getRelyingPartyId(), signAssertion);
487             }
488         } catch (MessageEncodingException e) {
489             log.error("Unable to determine if outbound encoding {} can provide integrity protection", encoder
490                     .getBindingURI());
491             throw new ProfileException("Unable to determine if outbound message should be signed");
492         }
493
494         if (!signAssertion && requestContext.getPeerEntityRoleMetadata() instanceof SPSSODescriptor) {
495             SPSSODescriptor ssoDescriptor = (SPSSODescriptor) requestContext.getPeerEntityRoleMetadata();
496             if (ssoDescriptor.getWantAssertionsSigned() != null) {
497                 signAssertion = ssoDescriptor.getWantAssertionsSigned().booleanValue();
498                 log.debug("Entity metadata for relying party {} indicates to sign assertions: {}", requestContext
499                         .getInboundMessageIssuer(), signAssertion);
500             }
501         }
502
503         if (!signAssertion) {
504             return;
505         }
506
507         log.debug("Determining signing credntial for assertion to relying party {}", requestContext
508                 .getInboundMessageIssuer());
509         Credential signatureCredential = profileConfig.getSigningCredential();
510         if (signatureCredential == null) {
511             signatureCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
512         }
513
514         if (signatureCredential == null) {
515             throw new ProfileException("No signing credential is specified for relying party configuration "
516                     + requestContext.getRelyingPartyConfiguration().getProviderId()
517                     + " or it's SAML2 attribute query profile configuration");
518         }
519
520         log.debug("Signing assertion to relying party {}", requestContext.getInboundMessageIssuer());
521         Signature signature = signatureBuilder.buildObject(Signature.DEFAULT_ELEMENT_NAME);
522
523         signature.setSigningCredential(signatureCredential);
524         try {
525             // TODO pull SecurityConfiguration from SAMLMessageContext? needs to be added
526             // TODO how to pull what keyInfoGenName to use?
527             SecurityHelper.prepareSignatureParams(signature, signatureCredential, null, null);
528         } catch (SecurityException e) {
529             throw new ProfileException("Error preparing signature for signing", e);
530         }
531
532         assertion.setSignature(signature);
533
534         Marshaller assertionMarshaller = Configuration.getMarshallerFactory().getMarshaller(assertion);
535         try {
536             assertionMarshaller.marshall(assertion);
537             Signer.signObject(signature);
538         } catch (MarshallingException e) {
539             log.error("Unable to marshall assertion for signing", e);
540             throw new ProfileException("Unable to marshall assertion for signing", e);
541         }
542     }
543
544     /**
545      * Build a status message, with an optional second-level failure message.
546      * 
547      * @param topLevelCode The top-level status code. Should be from saml-core-2.0-os, sec. 3.2.2.2
548      * @param secondLevelCode An optional second-level failure code. Should be from saml-core-2.0-is, sec 3.2.2.2. If
549      *            null, no second-level Status element will be set.
550      * @param failureMessage An optional second-level failure message
551      * 
552      * @return a Status object.
553      */
554     protected Status buildStatus(String topLevelCode, String secondLevelCode, String failureMessage) {
555         Status status = statusBuilder.buildObject();
556
557         StatusCode statusCode = statusCodeBuilder.buildObject();
558         statusCode.setValue(DatatypeHelper.safeTrimOrNullString(topLevelCode));
559         status.setStatusCode(statusCode);
560
561         if (secondLevelCode != null) {
562             StatusCode secondLevelStatusCode = statusCodeBuilder.buildObject();
563             secondLevelStatusCode.setValue(DatatypeHelper.safeTrimOrNullString(secondLevelCode));
564             statusCode.setStatusCode(secondLevelStatusCode);
565         }
566
567         if (failureMessage != null) {
568             StatusMessage msg = statusMessageBuilder.buildObject();
569             msg.setMessage(failureMessage);
570             status.setStatusMessage(msg);
571         }
572
573         return status;
574     }
575
576     /**
577      * Builds the SAML subject for the user for the service provider.
578      * 
579      * @param requestContext current request context
580      * @param confirmationMethod subject confirmation method used for the subject
581      * @param issueInstant instant the subject confirmation data should reflect for issuance
582      * 
583      * @return SAML subject for the user for the service provider
584      * 
585      * @throws ProfileException thrown if a NameID can not be created either because there was a problem encoding the
586      *             name ID attribute or because there are no supported name formats
587      */
588     protected Subject buildSubject(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, String confirmationMethod,
589             DateTime issueInstant) throws ProfileException {
590         NameID nameID = buildNameId(requestContext);
591         requestContext.setSubjectNameIdentifier(nameID);
592
593         SubjectConfirmationData confirmationData = subjectConfirmationDataBuilder.buildObject();
594         HTTPInTransport inTransport = (HTTPInTransport) requestContext.getInboundMessageTransport();
595         confirmationData.setAddress(inTransport.getPeerAddress());
596         confirmationData.setInResponseTo(requestContext.getInboundSAMLMessageId());
597         confirmationData.setNotOnOrAfter(issueInstant.plus(requestContext.getProfileConfiguration()
598                 .getAssertionLifetime()));
599
600         Endpoint relyingPartyEndpoint = requestContext.getPeerEntityEndpoint();
601         if (relyingPartyEndpoint != null) {
602             if (relyingPartyEndpoint.getResponseLocation() != null) {
603                 confirmationData.setRecipient(relyingPartyEndpoint.getResponseLocation());
604             } else {
605                 confirmationData.setRecipient(relyingPartyEndpoint.getLocation());
606             }
607         }
608
609         SubjectConfirmation subjectConfirmation = subjectConfirmationBuilder.buildObject();
610         subjectConfirmation.setMethod(confirmationMethod);
611         subjectConfirmation.setSubjectConfirmationData(confirmationData);
612
613         Subject subject = subjectBuilder.buildObject();
614         subject.getSubjectConfirmations().add(subjectConfirmation);
615         SAMLMessageEncoder encoder = getMessageEncoders().get(requestContext.getPeerEntityEndpoint().getBinding());
616         try {
617             if (requestContext.getProfileConfiguration().getEncryptNameID() == CryptoOperationRequirementLevel.always
618                     || (requestContext.getProfileConfiguration().getEncryptNameID() == CryptoOperationRequirementLevel.conditional && !encoder
619                             .providesMessageConfidentiality(requestContext))) {
620                 log.debug("Attempting to encrypt NameID to relying party {}", requestContext.getInboundMessageIssuer());
621                 try {
622                     Encrypter encrypter = getEncrypter(requestContext.getInboundMessageIssuer());
623                     subject.setEncryptedID(encrypter.encrypt(nameID));
624                 } catch (SecurityException e) {
625                     log.error("Unable to construct encrypter", e);
626                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
627                             "Unable to construct NameID"));
628                     throw new ProfileException("Unable to construct encrypter", e);
629                 } catch (EncryptionException e) {
630                     log.error("Unable to encrypt NameID", e);
631                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
632                             "Unable to construct NameID"));
633                     throw new ProfileException("Unable to encrypt NameID", e);
634                 }
635             } else {
636                 subject.setNameID(nameID);
637             }
638         } catch (MessageEncodingException e) {
639             log.error("Unable to determine if outbound encoding {} can provide confidentiality", encoder
640                     .getBindingURI());
641             throw new ProfileException("Unable to determine if assertions should be encrypted");
642         }
643
644         return subject;
645     }
646
647     /**
648      * Builds a NameID appropriate for this request. NameIDs are built by inspecting the SAML request and metadata,
649      * picking a name format that was requested by the relying party or is mutually supported by both the relying party
650      * and asserting party as described in their metadata entries. Once a set of supported name formats is determined
651      * the principals attributes are inspected for an attribute supported an attribute encoder whose category is one of
652      * the supported name formats.
653      * 
654      * @param requestContext current request context
655      * 
656      * @return the NameID appropriate for this request
657      * 
658      * @throws ProfileException thrown if a NameID can not be created either because there was a problem encoding the
659      *             name ID attribute or because there are no supported name formats
660      */
661     protected NameID buildNameId(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
662         log.debug("Building assertion NameID for principal/relying party:{}/{}", requestContext.getPrincipalName(),
663                 requestContext.getInboundMessageIssuer());
664
665         Map<String, BaseAttribute> principalAttributes = requestContext.getPrincipalAttributes();
666         if (principalAttributes == null || principalAttributes.isEmpty()) {
667             log.error("No attributes for principal {}, unable to construct of NameID", requestContext
668                     .getPrincipalName());
669             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.INVALID_NAMEID_POLICY_URI,
670                     "Unable to construct NameID"));
671             throw new ProfileException("No principal attributes support NameID construction");
672         }
673
674         List<String> supportedNameFormats = getNameFormats(requestContext);
675         if (supportedNameFormats == null || supportedNameFormats.isEmpty()) {
676             log.error("No common NameID formats supported by SP {} and IdP", requestContext.getInboundMessageIssuer());
677             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.INVALID_NAMEID_POLICY_URI,
678                     "Unable to construct NameID"));
679             throw new ProfileException("No principal attributes support NameID construction");
680         }
681
682         log.debug("Supported NameID formats: {}", supportedNameFormats);
683         try {
684             SAML2NameIDEncoder nameIdEncoder;
685             for (BaseAttribute<?> attribute : principalAttributes.values()) {
686                 for (AttributeEncoder encoder : attribute.getEncoders()) {
687                     if (encoder instanceof SAML2NameIDEncoder) {
688                         nameIdEncoder = (SAML2NameIDEncoder) encoder;
689                         if (supportedNameFormats.contains(nameIdEncoder.getNameFormat())) {
690                             log.debug("Using attribute {} suppoting NameID format {} to create the NameID.", attribute
691                                     .getId(), nameIdEncoder.getNameFormat());
692                             return nameIdEncoder.encode(attribute);
693                         }
694                     }
695                 }
696             }
697
698             log.error("No principal attribute supported encoding into a supported name ID format.");
699             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to construct NameID"));
700             throw new ProfileException("No principal attribute supported encoding into a supported name ID format.");
701         } catch (AttributeEncodingException e) {
702             log.error("Unable to encode NameID attribute", e);
703             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to construct NameID"));
704             throw new ProfileException("Unable to encode NameID attribute", e);
705         }
706     }
707
708     /**
709      * Gets the NameID format to use when creating NameIDs for the relying party.
710      * 
711      * @param requestContext current request context
712      * 
713      * @return list of nameID formats that may be used with the relying party
714      * 
715      * @throws ProfileException thrown if there is a problem determing the NameID format to use
716      */
717     protected List<String> getNameFormats(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
718             throws ProfileException {
719         ArrayList<String> nameFormats = new ArrayList<String>();
720
721         // Determine name formats supported by both SP and IdP
722         RoleDescriptor relyingPartyRole = requestContext.getPeerEntityRoleMetadata();
723         if (relyingPartyRole != null) {
724             List<String> relyingPartySupportedFormats = getEntitySupportedFormats(relyingPartyRole);
725             if (relyingPartySupportedFormats != null && !relyingPartySupportedFormats.isEmpty()) {
726                 nameFormats.addAll(relyingPartySupportedFormats);
727
728                 RoleDescriptor assertingPartyRole = requestContext.getLocalEntityRoleMetadata();
729                 if (assertingPartyRole != null) {
730                     List<String> assertingPartySupportedFormats = getEntitySupportedFormats(assertingPartyRole);
731                     if (assertingPartySupportedFormats != null && !assertingPartySupportedFormats.isEmpty()) {
732                         nameFormats.retainAll(assertingPartySupportedFormats);
733                     }
734                 }
735             }
736         }
737
738         if (nameFormats.isEmpty()) {
739             nameFormats.add("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified");
740         }
741
742         // If authn request and name ID policy format specified, make sure it's in the list of supported formats
743         String nameFormat = null;
744         if (requestContext.getInboundSAMLMessage() instanceof AuthnRequest) {
745             AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
746             if (authnRequest.getNameIDPolicy() != null) {
747                 nameFormat = DatatypeHelper.safeTrimOrNullString(authnRequest.getNameIDPolicy().getFormat());
748                 if (nameFormat != null) {
749                     if (nameFormats.contains(nameFormat)) {
750                         nameFormats.clear();
751                         nameFormats.add(nameFormat);
752                     } else {
753                         requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI,
754                                 StatusCode.INVALID_NAMEID_POLICY_URI, "Format not supported: " + nameFormat));
755                         throw new ProfileException("NameID format required by relying party is not supported");
756                     }
757                 }
758
759             }
760         }
761
762         return nameFormats;
763     }
764
765     /**
766      * Gets the list of NameID formats supported for a given role.
767      * 
768      * @param role the role to get the list of supported NameID formats
769      * 
770      * @return list of supported NameID formats
771      */
772     protected List<String> getEntitySupportedFormats(RoleDescriptor role) {
773         List<NameIDFormat> nameIDFormats = null;
774
775         if (role instanceof SSODescriptor) {
776             nameIDFormats = ((SSODescriptor) role).getNameIDFormats();
777         } else if (role instanceof AuthnAuthorityDescriptor) {
778             nameIDFormats = ((AuthnAuthorityDescriptor) role).getNameIDFormats();
779         } else if (role instanceof PDPDescriptor) {
780             nameIDFormats = ((PDPDescriptor) role).getNameIDFormats();
781         } else if (role instanceof AttributeAuthorityDescriptor) {
782             nameIDFormats = ((AttributeAuthorityDescriptor) role).getNameIDFormats();
783         }
784
785         ArrayList<String> supportedFormats = new ArrayList<String>();
786         if (nameIDFormats != null) {
787             for (NameIDFormat format : nameIDFormats) {
788                 supportedFormats.add(format.getFormat());
789             }
790         }
791
792         return supportedFormats;
793     }
794
795     /**
796      * Constructs an SAML response message carrying a request error.
797      * 
798      * @param requestContext current request context
799      * 
800      * @return the constructed error response
801      */
802     protected Response buildErrorResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
803         Response samlResponse = responseBuilder.buildObject();
804         samlResponse.setIssueInstant(new DateTime());
805         populateStatusResponse(requestContext, samlResponse);
806
807         samlResponse.setStatus(requestContext.getFailureStatus());
808
809         return samlResponse;
810     }
811
812     /**
813      * Gets an encrypter that may be used encrypt content to a given peer.
814      * 
815      * @param peerEntityId entity ID of the peer
816      * 
817      * @return encrypter that may be used encrypt content to a given peer
818      * 
819      * @throws SecurityException thrown if there is a problem constructing the encrypter. This normally occurs if the
820      *             key encryption credential for the peer can not be resolved or a required encryption algorithm is not
821      *             supported by the VM's JCE.
822      */
823     protected Encrypter getEncrypter(String peerEntityId) throws SecurityException {
824         SecurityConfiguration securityConfiguration = Configuration.getGlobalSecurityConfiguration();
825
826         EncryptionParameters dataEncParams = SecurityHelper
827                 .buildDataEncryptionParams(null, securityConfiguration, null);
828
829         Credential keyEncryptionCredentials = getKeyEncryptionCredential(peerEntityId);
830         String wrappedJCAKeyAlgorithm = SecurityHelper.getKeyAlgorithmFromURI(dataEncParams.getAlgorithm());
831         KeyEncryptionParameters keyEncParams = SecurityHelper.buildKeyEncryptionParams(keyEncryptionCredentials,
832                 wrappedJCAKeyAlgorithm, securityConfiguration, null, null);
833
834         Encrypter encrypter = new Encrypter(dataEncParams, keyEncParams);
835         encrypter.setKeyPlacement(KeyPlacement.INLINE);
836         return encrypter;
837     }
838
839     /**
840      * Gets the credential that can be used to encrypt encryption keys for a peer.
841      * 
842      * @param peerEntityId entity ID of the peer
843      * 
844      * @return credential that can be used to encrypt encryption keys for a peer
845      * 
846      * @throws SecurityException thrown if there is a problem resolving the credential from the peer's metadata
847      */
848     protected Credential getKeyEncryptionCredential(String peerEntityId) throws SecurityException {
849         MetadataCredentialResolver kekCredentialResolver = new MetadataCredentialResolver(getMetadataProvider());
850
851         CriteriaSet criteriaSet = new CriteriaSet();
852         criteriaSet.add(new EntityIDCriteria(peerEntityId));
853         criteriaSet.add(new MetadataCriteria(SPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
854         criteriaSet.add(new UsageCriteria(UsageType.ENCRYPTION));
855
856         return kekCredentialResolver.resolveSingle(criteriaSet);
857     }
858 }