View Javadoc

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