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