View Javadoc

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