package net.unicon.cas.mfa.authentication.principal;
import net.unicon.cas.mfa.authentication.DefaultCompositeAuthentication;
import net.unicon.cas.mfa.util.MultiFactorUtils;
import org.jasig.cas.authentication.Authentication;
import org.jasig.cas.authentication.Credential;
import org.jasig.cas.authentication.CredentialMetaData;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.principal.DefaultPrincipalFactory;
import org.jasig.cas.authentication.principal.Principal;
import org.jasig.cas.authentication.principal.PrincipalFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* A {@link Credential} implementation that is to ease multifactor authentication.
* It primarily carries the following entities:
* <ul>
* <li><strong>Chain of credentials:</strong> represent various forms of credentials during an MFA flow</li>
* <li><strong>Chain of authentications:</strong> represent the authentication contexts established during the flow
* when credentials are verified.
* </li>
* </ul>
* <p>Because the {@link Credential} itself is simply a marker interface, it is up to each credential
* implementation in the chain to decide how it wants to identify itself. This identifier will be used
* to locate the particular credential entry in the collection.
*
* <p>The collection of established authentication contexts are ordered, in such a way that
* each entry in the collection is linked to corresponding leg in the authentication flow.
*
* <p>It is the responsibility of the authentication flow of course, to gather and carry on
* the instance of {@link MultiFactorCredentials} as it knows how to authenticate the user agent
* and is itself the recipient of each credential identifier.
* @author Misagh Moayyed
* @see #getChainedCredentials()
* @see #getAuthentication()
*/
public class MultiFactorCredentials implements Credential, Serializable {
private final PrincipalFactory principalFactory = new DefaultPrincipalFactory();
private final Map<String, Credential> chainedCredentials = new LinkedHashMap<>();
private final List<Authentication> chainedAuthentication = new LinkedList<>();
private static final Logger LOGGER = LoggerFactory.getLogger(MultiFactorCredentials.class);
public final Map<String, Credential> getChainedCredentials() {
return this.chainedCredentials;
}
/**
* Indicate whether or not the authentication chain is empty.
* @return true, if the context is empty.
*/
public final boolean isEmpty() {
return this.chainedAuthentication.isEmpty();
}
/**
* Count the number of authentication contexts in the chain.
* @return total number of authentications in the chain.
*/
public final int countChainedAuthentications() {
return this.chainedAuthentication.size();
}
/**
* Add the authentication to the chain, having verified
* that the resolved principal of the new authentication
* matches what has been remembered and collected as the principal.
* @param authentication authentication context to add to the chain
*/
public final void addAuthenticationToChain(final Authentication authentication) {
if (!doesPrincipalMatchAuthenticationChain(authentication)) {
LOGGER.warn("The provided principal [{}] does not match the authentication chain. CAS has no record of "
+ "this principal ever having authenticated in the active authentication context.",
authentication.getPrincipal());
throw new UnknownPrincipalMatchException(authentication);
}
this.chainedAuthentication.add(authentication);
}
/**
* Enumerates the list of available principals in the authentication chain
* and ensures that the newly given and provided principal is compliant
* and equals the rest of the principals in the chain. The match
* is explicitly controlled by {@link Principal#equals(Object)}
* implementation.
*
* @param authentication the authentication object whose principal is compared against the chain
* @return true if no mismatch is found; false otherwise.
*/
private boolean doesPrincipalMatchAuthenticationChain(final Authentication authentication) {
for (final Authentication authn : this.chainedAuthentication) {
final Principal currentPrincipal = authn.getPrincipal();
final Principal newPrincipal = authentication.getPrincipal();
if (!currentPrincipal.equals(newPrincipal)) {
return false;
}
}
return true;
}
/**
* Creates an instance of the {@link net.unicon.cas.mfa.authentication.CompositeAuthentication} object that collects
* and harmonizes all principal and authentication attributes into one context.
*
* <p>Principal attributes are merged from all principals that are already resolved in the authentication chain.
* Attributes with the same name that belong to the same principal are merged into one, with the latter value
* overwriting the first. The established principal will be one that is based of {@link Principal}.</p>
*
* <p>Authentication attributes are merged from all authentications that make up the chain.
* The merging strategy is such that duplicate attribute names are grouped together into an instance of
* a {@link Collection} implementation and preserved.
* @return an instance of {@link net.unicon.cas.mfa.authentication.CompositeAuthentication}
*/
public final Authentication getAuthentication() {
if (!isEmpty()) {
/**
* Principal id is and must be enforced to be the same for all authentication contexts.
* 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.
*/
final String principalId = this.chainedAuthentication.get(0).getPrincipal().getId();
final Map<String, Object> principalAttributes = new HashMap<>();
final Map<String, Object> authenticationAttributes = new HashMap<>();
final List<CredentialMetaData> credentials = new ArrayList<>();
final Map<String, HandlerResult> successes = new LinkedHashMap<>();
final Map<String, Class<? extends Exception>> failures = new LinkedHashMap<>();
for (final Authentication authn : this.chainedAuthentication) {
final Principal authenticatedPrincipal = authn.getPrincipal();
principalAttributes.putAll(authenticatedPrincipal.getAttributes());
credentials.addAll(authn.getCredentials());
successes.putAll(authn.getSuccesses());
failures.putAll(authn.getFailures());
for (final String attrName : authn.getAttributes().keySet()) {
if (!authenticationAttributes.containsKey(attrName)) {
authenticationAttributes.put(attrName, authn.getAttributes().get(attrName));
} else {
final Object oldValue = authenticationAttributes.remove(attrName);
final Collection<Object> listOfValues = MultiFactorUtils.convertValueToCollection(oldValue);
listOfValues.add(authn.getAttributes().get(attrName));
authenticationAttributes.put(attrName, listOfValues);
}
}
}
final Principal compositePrincipal = principalFactory.createPrincipal(principalId, principalAttributes);
final DefaultCompositeAuthentication finalAuth =
new DefaultCompositeAuthentication(compositePrincipal,
authenticationAttributes, credentials, successes, failures);
return finalAuth;
}
return null;
}
/**
* Provides the ability to access the resolved
* and primary principal based on the authentication context.
* @return the primary principal.
*/
public final Principal getPrincipal() {
final Authentication auth = this.getAuthentication();
if (auth != null) {
return auth.getPrincipal();
}
return null;
}
public final Credential getCredentials() {
return getChainedCredentials().values().iterator().next();
}
@Override
public String getId() {
if (getPrincipal() != null) {
return getPrincipal().getId();
}
return Credential.UNKNOWN_ID;
}
}