package org.apereo.cas.authentication; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.ZonedDateTime; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Optional; import java.util.Set; /** * This is {@link DefaultAuthenticationResultBuilder}. * * @author Misagh Moayyed * @since 4.2.0 */ public class DefaultAuthenticationResultBuilder implements AuthenticationResultBuilder { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAuthenticationResultBuilder.class); private static final long serialVersionUID = 6180465589526463843L; private Credential providedCredential; private Set<Authentication> authentications = Collections.synchronizedSet(new LinkedHashSet<>()); private PrincipalElectionStrategy principalElectionStrategy; /** * Instantiates a new default authentication result builder. * * @param principalElectionStrategy the principal election strategy */ public DefaultAuthenticationResultBuilder(final PrincipalElectionStrategy principalElectionStrategy) { this.principalElectionStrategy = principalElectionStrategy; } @Override public Optional<Authentication> getInitialAuthentication() { if (this.authentications.isEmpty()) { LOGGER.warn("Authentication chain is empty as no authentications have been collected"); } return this.authentications.stream().findFirst(); } @Override public AuthenticationResultBuilder collect(final Authentication authentication) { this.authentications.add(authentication); return this; } @Override public AuthenticationResultBuilder collect(final Credential credential) { this.providedCredential = credential; return this; } @Override public AuthenticationResult build() { return build(null); } @Override public AuthenticationResult build(final Service service) { final Authentication authentication = buildAuthentication(); if (authentication == null) { LOGGER.info("Authentication result cannot be produced because no authentication is recorded into in the chain. Returning null"); return null; } LOGGER.debug("Building an authentication result for authentication [{}] and service [{}]", authentication, service); final DefaultAuthenticationResult res = new DefaultAuthenticationResult(authentication, service); res.setCredentialProvided(this.providedCredential != null); return res; } private boolean isEmpty() { return this.authentications.isEmpty(); } private Authentication buildAuthentication() { if (isEmpty()) { LOGGER.warn("No authentication event has been recorded; CAS cannot finalize the authentication result"); return null; } final Map<String, Object> authenticationAttributes = new HashMap<>(); final Map<String, Object> principalAttributes = new HashMap<>(); final AuthenticationBuilder authenticationBuilder = DefaultAuthenticationBuilder.newInstance(); buildAuthenticationHistory(this.authentications, authenticationAttributes, principalAttributes, authenticationBuilder); final Principal primaryPrincipal = getPrimaryPrincipal(this.authentications, principalAttributes); authenticationBuilder.setPrincipal(primaryPrincipal); LOGGER.debug("Determined primary authentication principal to be [{}]", primaryPrincipal); authenticationBuilder.setAttributes(authenticationAttributes); LOGGER.debug("Collected authentication attributes for this result are [{}]", authenticationAttributes); authenticationBuilder.setAuthenticationDate(ZonedDateTime.now()); final Authentication auth = authenticationBuilder.build(); LOGGER.debug("Authentication result commenced at [{}]", auth.getAuthenticationDate()); return auth; } private static void buildAuthenticationHistory(final Set<Authentication> authentications, final Map<String, Object> authenticationAttributes, final Map<String, Object> principalAttributes, final AuthenticationBuilder authenticationBuilder) { LOGGER.debug("Collecting authentication history based on [{}] authentication events", authentications.size()); authentications.stream().forEach(authn -> { final Principal authenticatedPrincipal = authn.getPrincipal(); LOGGER.debug("Evaluating authentication principal [{}] for inclusion in result", authenticatedPrincipal); principalAttributes.putAll(authenticatedPrincipal.getAttributes()); LOGGER.debug("Collected principal attributes [{}] for inclusion in this result for principal [{}]", principalAttributes, authenticatedPrincipal.getId()); authn.getAttributes().keySet().stream().forEach(attrName -> { if (authenticationAttributes.containsKey(attrName)) { LOGGER.debug("Collecting multi-valued authentication attribute [{}]", attrName); final Object oldValue = authenticationAttributes.remove(attrName); LOGGER.debug("Converting authentication attribute [{}] to a collection of values", attrName); final Collection<Object> listOfValues = CollectionUtils.toCollection(oldValue); final Object newValue = authn.getAttributes().get(attrName); listOfValues.addAll(CollectionUtils.toCollection(newValue)); authenticationAttributes.put(attrName, listOfValues); LOGGER.debug("Collected multi-valued authentication attribute [{}] -> [{}]", attrName, listOfValues); } else { final Object value = authn.getAttributes().get(attrName); if (value != null) { authenticationAttributes.put(attrName, value); LOGGER.debug("Collected single authentication attribute [{}] -> [{}]", attrName, value); } else { LOGGER.warn("Authentication attribute [{}] has no value and is not collected", attrName); } } }); LOGGER.debug("Finalized authentication attributes [{}] for inclusion in this authentication result", authenticationAttributes); authenticationBuilder.addSuccesses(authn.getSuccesses()) .addFailures(authn.getFailures()) .addCredentials(authn.getCredentials()); }); } /** * Principal id is and must be enforced to be the same for all authentications. * Based on that restriction, it's safe to simply grab the first principal id in the chain * when composing the authentication chain for the caller. */ private Principal getPrimaryPrincipal(final Set<Authentication> authentications, final Map<String, Object> principalAttributes) { return this.principalElectionStrategy.nominate(Collections.unmodifiableSet(authentications), principalAttributes); } }