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