View Javadoc

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