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.opensaml.xml.util.Pair;
76  import org.slf4j.Logger;
77  import org.slf4j.LoggerFactory;
78  
79  import edu.internet2.middleware.shibboleth.common.attribute.AttributeRequestException;
80  import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute;
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             postProcessAssertion(requestContext, assertion);
265 
266             signAssertion(requestContext, assertion);
267 
268             if (isEncryptAssertion(requestContext)) {
269                 log.debug("Attempting to encrypt assertion to relying party '{}'",
270                         requestContext.getInboundMessageIssuer());
271                 try {
272                     Encrypter encrypter = getEncrypter(requestContext.getInboundMessageIssuer());
273                     samlResponse.getEncryptedAssertions().add(encrypter.encrypt(assertion));
274                 } catch (SecurityException e) {
275                     log.error("Unable to construct encrypter", e);
276                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
277                             "Unable to encrypt assertion"));
278                     throw new ProfileException("Unable to construct encrypter", e);
279                 } catch (EncryptionException e) {
280                     log.error("Unable to encrypt assertion", e);
281                     requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null,
282                             "Unable to encrypt assertion"));
283                     throw new ProfileException("Unable to encrypt assertion", e);
284                 }
285             } else {
286                 samlResponse.getAssertions().add(assertion);
287             }
288         }
289 
290         Status status = buildStatus(StatusCode.SUCCESS_URI, null, null);
291         samlResponse.setStatus(status);
292 
293         postProcessResponse(requestContext, samlResponse);
294 
295         return samlResponse;
296     }
297 
298     /**
299      * Determine whether issued assertions should be encrypted.
300      * 
301      * @param requestContext the current request context
302      * @return true if assertions should be encrypted, false otherwise
303      * @throws ProfileException if there is a problem determining whether assertions should be encrypted
304      */
305     protected boolean isEncryptAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
306             throws ProfileException {
307 
308         SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
309         try {
310             return requestContext.getProfileConfiguration().getEncryptAssertion() == CryptoOperationRequirementLevel.always
311                     || (requestContext.getProfileConfiguration().getEncryptAssertion() == CryptoOperationRequirementLevel.conditional && !encoder
312                             .providesMessageConfidentiality(requestContext));
313         } catch (MessageEncodingException e) {
314             log.error("Unable to determine if outbound encoding '{}' can provide confidentiality",
315                     encoder.getBindingURI());
316             throw new ProfileException("Unable to determine if assertions should be encrypted");
317         }
318     }
319 
320     /**
321      * Extension point for for subclasses to post-process the Response before it is signed and encoded.
322      * 
323      * @param requestContext the current request context
324      * @param samlResponse the SAML Response being built
325      * 
326      * @throws ProfileException if there was an error processing the response
327      */
328     protected void postProcessResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Response samlResponse)
329             throws ProfileException {
330     }
331 
332     /**
333      * Extension point for for subclasses to post-process the Assertion before it is signed and encrypted.
334      * 
335      * @param requestContext the current request context
336      * @param assertion the SAML Assertion being built
337      * 
338      * @throws ProfileException if there is an error processing the assertion
339      */
340     protected void postProcessAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
341             throws ProfileException {
342     }
343 
344     /**
345      * Builds a basic assertion with its id, issue instant, SAML version, issuer, subject, and conditions populated.
346      * 
347      * @param requestContext current request context
348      * @param issueInstant time to use as assertion issue instant
349      * 
350      * @return the built assertion
351      */
352     protected Assertion buildAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, DateTime issueInstant) {
353         Assertion assertion = assertionBuilder.buildObject();
354         assertion.setID(getIdGenerator().generateIdentifier());
355         assertion.setIssueInstant(issueInstant);
356         assertion.setVersion(SAMLVersion.VERSION_20);
357         assertion.setIssuer(buildEntityIssuer(requestContext));
358 
359         Conditions conditions = buildConditions(requestContext, issueInstant);
360         assertion.setConditions(conditions);
361 
362         return assertion;
363     }
364 
365     /**
366      * Creates an {@link Issuer} populated with information about the relying party.
367      * 
368      * @param requestContext current request context
369      * 
370      * @return the built issuer
371      */
372     protected Issuer buildEntityIssuer(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
373         Issuer issuer = issuerBuilder.buildObject();
374         issuer.setFormat(Issuer.ENTITY);
375         issuer.setValue(requestContext.getLocalEntityId());
376 
377         return issuer;
378     }
379 
380     /**
381      * Builds a SAML assertion condition set. The following fields are set; not before, not on or after, audience
382      * restrictions, and proxy restrictions.
383      * 
384      * @param requestContext current request context
385      * @param issueInstant timestamp the assertion was created
386      * 
387      * @return constructed conditions
388      */
389     protected Conditions buildConditions(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, DateTime issueInstant) {
390         AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
391 
392         Conditions conditions = conditionsBuilder.buildObject();
393         conditions.setNotBefore(issueInstant);
394         conditions.setNotOnOrAfter(issueInstant.plus(profileConfig.getAssertionLifetime()));
395 
396         Collection<String> audiences;
397 
398         // add audience restrictions
399         AudienceRestriction audienceRestriction = audienceRestrictionBuilder.buildObject();
400         // TODO we should only do this for certain outgoing bindings, not globally
401         Audience audience = audienceBuilder.buildObject();
402         audience.setAudienceURI(requestContext.getInboundMessageIssuer());
403         audienceRestriction.getAudiences().add(audience);
404         audiences = profileConfig.getAssertionAudiences();
405         if (audiences != null && audiences.size() > 0) {
406             for (String audienceUri : audiences) {
407                 audience = audienceBuilder.buildObject();
408                 audience.setAudienceURI(audienceUri);
409                 audienceRestriction.getAudiences().add(audience);
410             }
411         }
412         conditions.getAudienceRestrictions().add(audienceRestriction);
413 
414         // add proxy restrictions
415         audiences = profileConfig.getProxyAudiences();
416         if (audiences != null && audiences.size() > 0) {
417             ProxyRestriction proxyRestriction = proxyRestrictionBuilder.buildObject();
418             for (String audienceUri : audiences) {
419                 audience = audienceBuilder.buildObject();
420                 audience.setAudienceURI(audienceUri);
421                 proxyRestriction.getAudiences().add(audience);
422             }
423 
424             proxyRestriction.setProxyCount(profileConfig.getProxyCount());
425             conditions.getConditions().add(proxyRestriction);
426         }
427 
428         return conditions;
429     }
430 
431     /**
432      * Populates the response's id, in response to, issue instant, version, and issuer properties.
433      * 
434      * @param requestContext current request context
435      * @param response the response to populate
436      */
437     protected void populateStatusResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
438             StatusResponseType response) {
439         response.setID(getIdGenerator().generateIdentifier());
440 
441         response.setInResponseTo(requestContext.getInboundSAMLMessageId());
442         response.setIssuer(buildEntityIssuer(requestContext));
443 
444         response.setVersion(SAMLVersion.VERSION_20);
445     }
446 
447     /**
448      * Resolves the attributes for the principal.
449      * 
450      * @param requestContext current request context
451      * 
452      * @throws ProfileException thrown if there is a problem resolved attributes
453      */
454     protected void resolveAttributes(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
455         AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
456         SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
457         try {
458             log.debug("Resolving attributes for principal '{}' for SAML request from relying party '{}'",
459                     requestContext.getPrincipalName(), requestContext.getInboundMessageIssuer());
460             Map<String, BaseAttribute> principalAttributes = attributeAuthority.getAttributes(requestContext);
461 
462             requestContext.setAttributes(principalAttributes);
463         } catch (AttributeRequestException e) {
464             log.warn(
465                     "Error resolving attributes for principal '{}'.  No name identifier or attribute statement will be included in response",
466                     requestContext.getPrincipalName());
467         }
468     }
469 
470     /**
471      * Executes a query for attributes and builds a SAML attribute statement from the results.
472      * 
473      * @param requestContext current request context
474      * 
475      * @return attribute statement resulting from the query
476      * 
477      * @throws ProfileException thrown if there is a problem making the query
478      */
479     protected AttributeStatement buildAttributeStatement(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext)
480             throws ProfileException {
481         if (requestContext.getAttributes() == null) {
482             return null;
483         }
484 
485         log.debug("Creating attribute statement in response to SAML request '{}' from relying party '{}'",
486                 requestContext.getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
487 
488         AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
489         SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
490         try {
491             if (requestContext.getInboundSAMLMessage() instanceof AttributeQuery) {
492                 return attributeAuthority.buildAttributeStatement((AttributeQuery) requestContext
493                         .getInboundSAMLMessage(), requestContext.getAttributes().values());
494             } else {
495                 return attributeAuthority.buildAttributeStatement(null, requestContext.getAttributes().values());
496             }
497         } catch (AttributeRequestException e) {
498             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Error resolving attributes"));
499             String msg = "Error encoding attributes for principal " + requestContext.getPrincipalName();
500             log.error(msg, e);
501             throw new ProfileException(msg, e);
502         }
503     }
504 
505     /**
506      * Resolves the principal name of the subject of the request.
507      * 
508      * @param requestContext current request context
509      * 
510      * @throws ProfileException thrown if the principal name can not be resolved
511      */
512     protected void resolvePrincipal(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
513         AbstractSAML2ProfileConfiguration profileConfiguration = requestContext.getProfileConfiguration();
514         if (profileConfiguration == null) {
515             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.REQUEST_DENIED_URI,
516                     "Error resolving principal"));
517             String msg = "Unable to resolve principal, no SAML 2 profile configuration for relying party "
518                     + requestContext.getInboundMessageIssuer();
519             log.warn(msg);
520             throw new ProfileException(msg);
521         }
522         SAML2AttributeAuthority attributeAuthority = profileConfiguration.getAttributeAuthority();
523         log.debug("Resolving principal name for subject of SAML request '{}' from relying party '{}'",
524                 requestContext.getInboundSAMLMessageId(), requestContext.getInboundMessageIssuer());
525 
526         try {
527             String principal = attributeAuthority.getPrincipal(requestContext);
528             requestContext.setPrincipalName(principal);
529         } catch (AttributeRequestException e) {
530             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.UNKNOWN_PRINCIPAL_URI,
531                     "Error resolving principal"));
532             String msg = "Error resolving principal name for SAML request '" + requestContext.getInboundSAMLMessageId()
533                     + "' from relying party '" + requestContext.getInboundMessageIssuer() + "'. Cause: "
534                     + e.getMessage();
535             log.warn(msg);
536             throw new ProfileException(msg, e);
537         }
538     }
539 
540     /**
541      * Signs the given assertion if either the current profile configuration or the relying party configuration contains
542      * signing credentials.
543      * 
544      * @param requestContext current request context
545      * @param assertion assertion to sign
546      * 
547      * @throws ProfileException thrown if the metadata can not be located for the relying party or, if signing is
548      *             required, if a signing credential is not configured
549      */
550     protected void signAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, Assertion assertion)
551             throws ProfileException {
552         log.debug("Determining if SAML assertion to relying party '{}' should be signed",
553                 requestContext.getInboundMessageIssuer());
554 
555         boolean signAssertion = isSignAssertion(requestContext);
556 
557         if (!signAssertion) {
558             return;
559         }
560 
561         AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
562 
563         log.debug("Determining signing credntial for assertion to relying party '{}'",
564                 requestContext.getInboundMessageIssuer());
565         Credential signatureCredential = profileConfig.getSigningCredential();
566         if (signatureCredential == null) {
567             signatureCredential = requestContext.getRelyingPartyConfiguration().getDefaultSigningCredential();
568         }
569 
570         if (signatureCredential == null) {
571             String msg = "No signing credential is specified for relying party configuration "
572                     + requestContext.getRelyingPartyConfiguration().getProviderId();
573             log.warn(msg);
574             throw new ProfileException(msg);
575         }
576 
577         log.debug("Signing assertion to relying party {}", requestContext.getInboundMessageIssuer());
578         Signature signature = signatureBuilder.buildObject(Signature.DEFAULT_ELEMENT_NAME);
579 
580         signature.setSigningCredential(signatureCredential);
581         try {
582             // TODO pull SecurityConfiguration from SAMLMessageContext? needs to be added
583             // TODO how to pull what keyInfoGenName to use?
584             SecurityHelper.prepareSignatureParams(signature, signatureCredential, null, null);
585         } catch (SecurityException e) {
586             String msg = "Error preparing signature for signing";
587             log.error(msg);
588             throw new ProfileException(msg, e);
589         }
590 
591         assertion.setSignature(signature);
592 
593         Marshaller assertionMarshaller = Configuration.getMarshallerFactory().getMarshaller(assertion);
594         try {
595             assertionMarshaller.marshall(assertion);
596             Signer.signObject(signature);
597         } catch (MarshallingException e) {
598             String errMsg = "Unable to marshall assertion for signing";
599             log.error(errMsg, e);
600             throw new ProfileException(errMsg, e);
601         } catch (SignatureException e) {
602             String msg = "Unable to sign assertion";
603             log.error(msg, e);
604             throw new ProfileException(msg, e);
605         }
606     }
607 
608     /**
609      * Determine whether issued assertions should be signed.
610      * 
611      * @param requestContext the current request context
612      * @return true if assertions should be signed, false otherwise
613      * @throws ProfileException if there is a problem determining whether assertions should be signed
614      */
615     protected boolean isSignAssertion(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
616 
617         SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
618         AbstractSAML2ProfileConfiguration profileConfig = requestContext.getProfileConfiguration();
619 
620         try {
621             boolean signAssertion = profileConfig.getSignAssertions() == CryptoOperationRequirementLevel.always
622                     || (profileConfig.getSignAssertions() == CryptoOperationRequirementLevel.conditional && !encoder
623                             .providesMessageIntegrity(requestContext));
624 
625             log.debug("IdP relying party configuration '{}' indicates to sign assertions: {}", requestContext
626                     .getRelyingPartyConfiguration().getRelyingPartyId(), signAssertion);
627 
628             if (!signAssertion && requestContext.getPeerEntityRoleMetadata() instanceof SPSSODescriptor) {
629                 SPSSODescriptor ssoDescriptor = (SPSSODescriptor) requestContext.getPeerEntityRoleMetadata();
630                 if (ssoDescriptor.getWantAssertionsSigned() != null) {
631                     signAssertion = ssoDescriptor.getWantAssertionsSigned().booleanValue();
632                     log.debug("Entity metadata for relying party '{} 'indicates to sign assertions: {}",
633                             requestContext.getInboundMessageIssuer(), signAssertion);
634                 }
635             }
636 
637             return signAssertion;
638         } catch (MessageEncodingException e) {
639             log.error("Unable to determine if outbound encoding '{}' provides message integrity protection",
640                     encoder.getBindingURI());
641             throw new ProfileException("Unable to determine if outbound assertion should be signed");
642         }
643     }
644 
645     /**
646      * Build a status message, with an optional second-level failure message.
647      * 
648      * @param topLevelCode The top-level status code. Should be from saml-core-2.0-os, sec. 3.2.2.2
649      * @param secondLevelCode An optional second-level failure code. Should be from saml-core-2.0-is, sec 3.2.2.2. If
650      *            null, no second-level Status element will be set.
651      * @param failureMessage An optional second-level failure message
652      * 
653      * @return a Status object.
654      */
655     protected Status buildStatus(String topLevelCode, String secondLevelCode, String failureMessage) {
656         Status status = statusBuilder.buildObject();
657 
658         StatusCode statusCode = statusCodeBuilder.buildObject();
659         statusCode.setValue(DatatypeHelper.safeTrimOrNullString(topLevelCode));
660         status.setStatusCode(statusCode);
661 
662         if (secondLevelCode != null) {
663             StatusCode secondLevelStatusCode = statusCodeBuilder.buildObject();
664             secondLevelStatusCode.setValue(DatatypeHelper.safeTrimOrNullString(secondLevelCode));
665             statusCode.setStatusCode(secondLevelStatusCode);
666         }
667 
668         if (failureMessage != null) {
669             StatusMessage msg = statusMessageBuilder.buildObject();
670             msg.setMessage(failureMessage);
671             status.setStatusMessage(msg);
672         }
673 
674         return status;
675     }
676 
677     /**
678      * Builds the SAML subject for the user for the service provider.
679      * 
680      * @param requestContext current request context
681      * @param confirmationMethod subject confirmation method used for the subject
682      * @param issueInstant instant the subject confirmation data should reflect for issuance
683      * 
684      * @return SAML subject for the user for the service provider
685      * 
686      * @throws ProfileException thrown if a NameID can not be created either because there was a problem encoding the
687      *             name ID attribute or because there are no supported name formats
688      */
689     protected Subject buildSubject(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext, String confirmationMethod,
690             DateTime issueInstant) throws ProfileException {
691         Subject subject = subjectBuilder.buildObject();
692         subject.getSubjectConfirmations().add(
693                 buildSubjectConfirmation(requestContext, confirmationMethod, issueInstant));
694 
695         NameID nameID = buildNameId(requestContext);
696         if (nameID == null) {
697             return subject;
698         }
699 
700         requestContext.setSubjectNameIdentifier(nameID);
701 
702         if (isEncryptNameID(requestContext)) {
703             log.debug("Attempting to encrypt NameID to relying party '{}'", requestContext.getInboundMessageIssuer());
704             try {
705                 Encrypter encrypter = getEncrypter(requestContext.getInboundMessageIssuer());
706                 subject.setEncryptedID(encrypter.encrypt(nameID));
707             } catch (SecurityException e) {
708                 log.error("Unable to construct encrypter", e);
709                 requestContext
710                         .setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to encrypt NameID"));
711                 throw new ProfileException("Unable to construct encrypter", e);
712             } catch (EncryptionException e) {
713                 log.error("Unable to encrypt NameID", e);
714                 requestContext
715                         .setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to encrypt NameID"));
716                 throw new ProfileException("Unable to encrypt NameID", e);
717             }
718         } else {
719             subject.setNameID(nameID);
720         }
721 
722         return subject;
723     }
724 
725     /**
726      * Determine whether NameID's should be encrypted.
727      * 
728      * @param requestContext the current request context
729      * @return true if NameID's should be encrypted, false otherwise
730      * @throws ProfileException if there is a problem determining whether NameID's should be encrypted
731      */
732     protected boolean isEncryptNameID(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
733 
734         boolean nameIdEncRequiredByAuthnRequest = isRequestRequiresEncryptNameID(requestContext);
735 
736         SAMLMessageEncoder encoder = getOutboundMessageEncoder(requestContext);
737         boolean nameIdEncRequiredByConfig = false;
738         try {
739             nameIdEncRequiredByConfig = requestContext.getProfileConfiguration().getEncryptNameID() == CryptoOperationRequirementLevel.always
740                     || (requestContext.getProfileConfiguration().getEncryptNameID() == CryptoOperationRequirementLevel.conditional && !encoder
741                             .providesMessageConfidentiality(requestContext));
742         } catch (MessageEncodingException e) {
743             String msg = "Unable to determine if outbound encoding '" + encoder.getBindingURI()
744                     + "' provides message confidentiality protection";
745             log.error(msg);
746             throw new ProfileException(msg);
747         }
748 
749         return nameIdEncRequiredByAuthnRequest || nameIdEncRequiredByConfig;
750     }
751 
752     /**
753      * Determine whether information in the SAML request requires the issued NameID to be encrypted.
754      * 
755      * @param requestContext the current request context
756      * @return true if the request indicates NameID encryption is required, false otherwise
757      */
758     protected boolean isRequestRequiresEncryptNameID(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
759         boolean nameIdEncRequiredByAuthnRequest = false;
760         if (requestContext.getInboundSAMLMessage() instanceof AuthnRequest) {
761             AuthnRequest authnRequest = (AuthnRequest) requestContext.getInboundSAMLMessage();
762             NameIDPolicy policy = authnRequest.getNameIDPolicy();
763             if (policy != null && DatatypeHelper.safeEquals(policy.getFormat(), NameID.ENCRYPTED)) {
764                 nameIdEncRequiredByAuthnRequest = true;
765             }
766         }
767         return nameIdEncRequiredByAuthnRequest;
768     }
769 
770     /**
771      * Builds the SubjectConfirmation appropriate for this request.
772      * 
773      * @param requestContext current request context
774      * @param confirmationMethod confirmation method to use for the request
775      * @param issueInstant issue instant of the response
776      * 
777      * @return the constructed subject confirmation
778      */
779     protected SubjectConfirmation buildSubjectConfirmation(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext,
780             String confirmationMethod, DateTime issueInstant) {
781         SubjectConfirmationData confirmationData = subjectConfirmationDataBuilder.buildObject();
782         HTTPInTransport inTransport = (HTTPInTransport) requestContext.getInboundMessageTransport();
783         confirmationData.setAddress(inTransport.getPeerAddress());
784         confirmationData.setInResponseTo(requestContext.getInboundSAMLMessageId());
785         confirmationData.setNotOnOrAfter(issueInstant.plus(requestContext.getProfileConfiguration()
786                 .getAssertionLifetime()));
787 
788         Endpoint relyingPartyEndpoint = requestContext.getPeerEntityEndpoint();
789         if (relyingPartyEndpoint != null) {
790             if (relyingPartyEndpoint.getResponseLocation() != null) {
791                 confirmationData.setRecipient(relyingPartyEndpoint.getResponseLocation());
792             } else {
793                 confirmationData.setRecipient(relyingPartyEndpoint.getLocation());
794             }
795         }
796 
797         SubjectConfirmation subjectConfirmation = subjectConfirmationBuilder.buildObject();
798         subjectConfirmation.setMethod(confirmationMethod);
799         subjectConfirmation.setSubjectConfirmationData(confirmationData);
800 
801         return subjectConfirmation;
802     }
803 
804     /**
805      * Builds a NameID appropriate for this request. NameIDs are built by inspecting the SAML request and metadata,
806      * picking a name format that was requested by the relying party or is mutually supported by both the relying party
807      * and asserting party as described in their metadata entries. Once a set of supported name formats is determined
808      * the principals attributes are inspected for an attribute supported an attribute encoder whose category is one of
809      * the supported name formats.
810      * 
811      * @param requestContext current request context
812      * 
813      * @return the NameID appropriate for this request
814      * 
815      * @throws ProfileException thrown if a NameID can not be created either because there was a problem encoding the
816      *             name ID attribute or because there are no supported name formats
817      */
818     protected NameID buildNameId(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) throws ProfileException {
819         Pair<BaseAttribute, SAML2NameIDEncoder> nameIdAttributeAndEncoder = null;
820         try {
821             nameIdAttributeAndEncoder = selectNameIDAttributeAndEncoder(SAML2NameIDEncoder.class, requestContext);
822         } catch (ProfileException e) {
823             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, StatusCode.INVALID_NAMEID_POLICY_URI,
824                     "Required NameID format not supported"));
825             throw e;
826         }
827 
828         if (nameIdAttributeAndEncoder == null) {
829             return null;
830         }
831 
832         BaseAttribute<?> nameIdAttribute = nameIdAttributeAndEncoder.getFirst();
833         requestContext.setNameIdentifierAttribute(nameIdAttribute);
834         SAML2NameIDEncoder nameIdEncoder = nameIdAttributeAndEncoder.getSecond();
835 
836         log.debug(
837                 "Using attribute '{}' supporting NameID format '{}' to create the NameID for relying party '{}'",
838                 new Object[] { nameIdAttribute.getId(), nameIdEncoder.getNameFormat(),
839                         requestContext.getInboundMessageIssuer(), });
840         try {
841             // build the actual NameID
842             NameID nameId = nameIdEncoder.encode(nameIdAttribute);
843             nameId.setNameQualifier(requestContext.getRelyingPartyConfiguration().getProviderId());
844             return nameId;
845         } catch (AttributeEncodingException e) {
846             log.error("Unable to encode NameID attribute", e);
847             requestContext.setFailureStatus(buildStatus(StatusCode.RESPONDER_URI, null, "Unable to construct NameID"));
848             throw new ProfileException("Unable to encode NameID attribute", e);
849         }
850     }
851 
852     /**
853      * Constructs an SAML response message carrying a request error.
854      * 
855      * @param requestContext current request context
856      * 
857      * @return the constructed error response
858      */
859     protected Response buildErrorResponse(BaseSAML2ProfileRequestContext<?, ?, ?> requestContext) {
860         Response samlResponse = responseBuilder.buildObject();
861         samlResponse.setIssueInstant(new DateTime());
862         populateStatusResponse(requestContext, samlResponse);
863 
864         samlResponse.setStatus(requestContext.getFailureStatus());
865 
866         return samlResponse;
867     }
868 
869     /**
870      * Gets an encrypter that may be used encrypt content to a given peer.
871      * 
872      * @param peerEntityId entity ID of the peer
873      * 
874      * @return encrypter that may be used encrypt content to a given peer
875      * 
876      * @throws SecurityException thrown if there is a problem constructing the encrypter. This normally occurs if the
877      *             key encryption credential for the peer can not be resolved or a required encryption algorithm is not
878      *             supported by the VM's JCE.
879      */
880     protected Encrypter getEncrypter(String peerEntityId) throws SecurityException {
881         SecurityConfiguration securityConfiguration = Configuration.getGlobalSecurityConfiguration();
882 
883         EncryptionParameters dataEncParams = SecurityHelper
884                 .buildDataEncryptionParams(null, securityConfiguration, null);
885 
886         Credential keyEncryptionCredential = getKeyEncryptionCredential(peerEntityId);
887         if (keyEncryptionCredential == null) {
888             log.error("Could not resolve a key encryption credential for peer entity: {}", peerEntityId);
889             throw new SecurityException("Could not resolve key encryption credential");
890         }
891         String wrappedJCAKeyAlgorithm = SecurityHelper.getKeyAlgorithmFromURI(dataEncParams.getAlgorithm());
892         KeyEncryptionParameters keyEncParams = SecurityHelper.buildKeyEncryptionParams(keyEncryptionCredential,
893                 wrappedJCAKeyAlgorithm, securityConfiguration, null, null);
894 
895         Encrypter encrypter = new Encrypter(dataEncParams, keyEncParams);
896         encrypter.setKeyPlacement(KeyPlacement.INLINE);
897         return encrypter;
898     }
899 
900     /**
901      * Gets the credential that can be used to encrypt encryption keys for a peer.
902      * 
903      * @param peerEntityId entity ID of the peer
904      * 
905      * @return credential that can be used to encrypt encryption keys for a peer
906      * 
907      * @throws SecurityException thrown if there is a problem resolving the credential from the peer's metadata
908      */
909     protected Credential getKeyEncryptionCredential(String peerEntityId) throws SecurityException {
910         MetadataCredentialResolver kekCredentialResolver = getMetadataCredentialResolver();
911 
912         CriteriaSet criteriaSet = new CriteriaSet();
913         criteriaSet.add(new EntityIDCriteria(peerEntityId));
914         criteriaSet.add(new MetadataCriteria(SPSSODescriptor.DEFAULT_ELEMENT_NAME, SAMLConstants.SAML20P_NS));
915         criteriaSet.add(new UsageCriteria(UsageType.ENCRYPTION));
916 
917         return kekCredentialResolver.resolveSingle(criteriaSet);
918     }
919 
920     /**
921      * Writes an audit log entry indicating the successful response to the attribute request.
922      * 
923      * @param context current request context
924      */
925     protected void writeAuditLogEntry(BaseSAMLProfileRequestContext context) {
926         SAML2AuditLogEntry auditLogEntry = new SAML2AuditLogEntry();
927         auditLogEntry.setSAMLResponse((StatusResponseType) context.getOutboundSAMLMessage());
928         auditLogEntry.setMessageProfile(getProfileId());
929         auditLogEntry.setPrincipalAuthenticationMethod(context.getPrincipalAuthenticationMethod());
930         auditLogEntry.setPrincipalName(context.getPrincipalName());
931         auditLogEntry.setAssertingPartyId(context.getLocalEntityId());
932         auditLogEntry.setRelyingPartyId(context.getInboundMessageIssuer());
933         auditLogEntry.setRequestBinding(context.getMessageDecoder().getBindingURI());
934         auditLogEntry.setRequestId(context.getInboundSAMLMessageId());
935         auditLogEntry.setResponseBinding(context.getMessageEncoder().getBindingURI());
936         auditLogEntry.setResponseId(context.getOutboundSAMLMessageId());
937         if (context.getReleasedAttributes() != null) {
938             auditLogEntry.getReleasedAttributes().addAll(context.getReleasedAttributes());
939         }
940 
941         if (context.getNameIdentifierAttribute() != null) {
942             Object idValue = context.getNameIdentifierAttribute().getValues().iterator().next();
943             if(idValue != null){
944                 auditLogEntry.setNameIdValue(idValue.toString());
945             }
946         }
947 
948         getAduitLog().info(auditLogEntry.toString());
949     }
950 
951     /** SAML 1 specific audit log entry. */
952     protected class SAML2AuditLogEntry extends AuditLogEntry {
953 
954         /** The response to the SAML request. */
955         private StatusResponseType samlResponse;
956 
957         /**
958          * Gets the response to the SAML request.
959          * 
960          * @return the response to the SAML request
961          */
962         public StatusResponseType getSAMLResponse() {
963             return samlResponse;
964         }
965 
966         /**
967          * Sets the response to the SAML request.
968          * 
969          * @param response the response to the SAML request
970          */
971         public void setSAMLResponse(StatusResponseType response) {
972             samlResponse = response;
973         }
974 
975         /** {@inheritDoc} */
976         public String toString() {
977             StringBuilder entryString = new StringBuilder(super.toString());
978 
979             StringBuilder assertionIds = new StringBuilder();
980 
981             if (samlResponse instanceof Response) {
982                 List<Assertion> assertions = ((Response) samlResponse).getAssertions();
983                 if (assertions != null && !assertions.isEmpty()) {
984                     for (Assertion assertion : assertions) {
985                         assertionIds.append(assertion.getID());
986                         assertionIds.append(",");
987                     }
988                 }
989             }
990 
991             if (getNameIdValue() != null) {
992                 entryString.append(getNameIdValue());
993             }
994             entryString.append("|");
995 
996             entryString.append(assertionIds.toString());
997             entryString.append("|");
998 
999             return entryString.toString();
1000         }
1001     }
1002 }