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