Core authentication API module for Apereo CAS providing fundamental authentication interfaces, implementations, and components for the CAS server infrastructure
—
Principal resolution is the process of identifying and constructing user principals with their associated attributes after successful authentication. The CAS authentication API provides a flexible framework for resolving principals from various sources and managing their attributes through configurable merging strategies.
package org.apereo.cas.authentication.principal;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
public interface Principal extends Serializable {
String getId();
Map<String, List<Object>> getAttributes();
}The default implementation that provides immutable attribute storage:
package org.apereo.cas.authentication.principal;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class SimplePrincipal implements Principal {
private final String id;
private final Map<String, List<Object>> attributes;
@JsonCreator
public SimplePrincipal(@JsonProperty("id") String id,
@JsonProperty("attributes") Map<String, List<Object>> attributes) {
this.id = id;
this.attributes = attributes != null ?
new TreeMap<>(String.CASE_INSENSITIVE_ORDER) {{
putAll(attributes);
}} : new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
}
public SimplePrincipal(String id) {
this(id, null);
}
public String getId() { return id; }
public Map<String, List<Object>> getAttributes() { return attributes; }
@Override
public boolean equals(Object obj) {
if (obj == null || !this.getClass().equals(obj.getClass())) {
return false;
}
return getId().equals(((SimplePrincipal) obj).getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}Null object pattern implementation for cases where no principal is available:
package org.apereo.cas.authentication.principal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public final class NullPrincipal implements Principal {
private static final NullPrincipal INSTANCE = new NullPrincipal();
private NullPrincipal() {}
public static NullPrincipal getInstance() {
return INSTANCE;
}
public String getId() {
return null;
}
public Map<String, List<Object>> getAttributes() {
return Collections.emptyMap();
}
@Override
public String toString() {
return "[Principal id=null, attributes={}]";
}
}Principal factories are responsible for creating principal instances:
package org.apereo.cas.authentication.principal;
import java.util.List;
import java.util.Map;
public interface PrincipalFactory {
Principal createPrincipal(String id);
Principal createPrincipal(String id, Map<String, List<Object>> attributes);
Principal createPrincipal(String id, Map<String, List<Object>> attributes,
Map<String, List<Object>> originalAttributes);
}package org.apereo.cas.authentication.principal;
import java.util.List;
import java.util.Map;
public class DefaultPrincipalFactory implements PrincipalFactory {
public Principal createPrincipal(String id) {
return new SimplePrincipal(id);
}
public Principal createPrincipal(String id, Map<String, List<Object>> attributes) {
return new SimplePrincipal(id, attributes);
}
public Principal createPrincipal(String id,
Map<String, List<Object>> attributes,
Map<String, List<Object>> originalAttributes) {
return new SimplePrincipal(id, attributes);
}
}Factory that uses Groovy scripts for dynamic principal creation:
package org.apereo.cas.authentication.principal;
import org.apereo.cas.util.scripting.ExecutableCompiledGroovyScript;
import java.util.List;
import java.util.Map;
public class GroovyPrincipalFactory implements PrincipalFactory {
private final ExecutableCompiledGroovyScript watchableScript;
public GroovyPrincipalFactory(ExecutableCompiledGroovyScript watchableScript) {
this.watchableScript = watchableScript;
}
public Principal createPrincipal(String id, Map<String, List<Object>> attributes) {
Object result = watchableScript.execute(id, attributes, Principal.class);
if (result instanceof Principal) {
return (Principal) result;
}
return new SimplePrincipal(id, attributes);
}
public Principal createPrincipal(String id) {
return createPrincipal(id, null);
}
public Principal createPrincipal(String id,
Map<String, List<Object>> attributes,
Map<String, List<Object>> originalAttributes) {
Object result = watchableScript.execute(id, attributes, originalAttributes, Principal.class);
if (result instanceof Principal) {
return (Principal) result;
}
return new SimplePrincipal(id, attributes);
}
}Factory that creates principals by calling REST endpoints:
package org.apereo.cas.authentication.principal;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Map;
public class RestfulPrincipalFactory implements PrincipalFactory {
private final RestTemplate restTemplate;
private final String endpoint;
public RestfulPrincipalFactory(RestTemplate restTemplate, String endpoint) {
this.restTemplate = restTemplate;
this.endpoint = endpoint;
}
public Principal createPrincipal(String id, Map<String, List<Object>> attributes) {
try {
Map<String, Object> request = Map.of("id", id, "attributes", attributes);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request);
Map<String, Object> response = restTemplate.exchange(
endpoint, HttpMethod.POST, entity, Map.class).getBody();
if (response != null && response.containsKey("id")) {
String principalId = (String) response.get("id");
Map<String, List<Object>> principalAttributes =
(Map<String, List<Object>>) response.get("attributes");
return new SimplePrincipal(principalId, principalAttributes);
}
} catch (Exception e) {
// Log error and fall back to default behavior
}
return new SimplePrincipal(id, attributes);
}
public Principal createPrincipal(String id) {
return createPrincipal(id, null);
}
public Principal createPrincipal(String id,
Map<String, List<Object>> attributes,
Map<String, List<Object>> originalAttributes) {
return createPrincipal(id, attributes);
}
}package org.apereo.cas.authentication.principal;
public final class PrincipalFactoryUtils {
private PrincipalFactoryUtils() {}
public static PrincipalFactory newPrincipalFactory() {
return new DefaultPrincipalFactory();
}
}When multiple authentication handlers produce different principals, an election strategy determines which principal to use:
package org.apereo.cas.authentication.principal;
import org.apereo.cas.authentication.Authentication;
import java.util.Collection;
public interface PrincipalElectionStrategy {
Principal electPrincipal(Collection<Authentication> authentications);
}package org.apereo.cas.authentication.principal;
import org.apereo.cas.authentication.Authentication;
import java.util.Collection;
public class DefaultPrincipalElectionStrategy implements PrincipalElectionStrategy {
public Principal electPrincipal(Collection<Authentication> authentications) {
return authentications.stream()
.findFirst()
.map(Authentication::getPrincipal)
.orElse(NullPrincipal.getInstance());
}
}package org.apereo.cas.authentication.principal;
import org.apereo.cas.authentication.Authentication;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class ChainingPrincipalElectionStrategy implements PrincipalElectionStrategy {
private final List<PrincipalElectionStrategy> chain = new ArrayList<>();
public void addStrategy(PrincipalElectionStrategy strategy) {
chain.add(strategy);
}
public Principal electPrincipal(Collection<Authentication> authentications) {
return chain.stream()
.map(strategy -> strategy.electPrincipal(authentications))
.filter(principal -> principal != null && !NullPrincipal.getInstance().equals(principal))
.findFirst()
.orElse(NullPrincipal.getInstance());
}
}Principal resolvers are responsible for enriching principals with attributes from external sources:
package org.apereo.cas.authentication.principal;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.Credential;
import java.util.Optional;
public interface PrincipalResolver {
Optional<Principal> resolve(Credential credential,
Optional<Principal> currentPrincipal,
Optional<AuthenticationHandler> handler);
boolean supports(Credential credential);
IPersonAttributeDao getAttributeRepository();
}Simple resolver that returns the principal as-is:
package org.apereo.cas.authentication.principal.resolvers;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import java.util.Optional;
public class EchoingPrincipalResolver implements PrincipalResolver {
public Optional<Principal> resolve(Credential credential,
Optional<Principal> currentPrincipal,
Optional<AuthenticationHandler> handler) {
return currentPrincipal;
}
public boolean supports(Credential credential) {
return true;
}
public IPersonAttributeDao getAttributeRepository() {
return null;
}
}Resolver that creates proxy principals for service authentication:
package org.apereo.cas.authentication.principal.resolvers;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import java.util.Optional;
public class ProxyingPrincipalResolver implements PrincipalResolver {
private final PrincipalFactory principalFactory;
public ProxyingPrincipalResolver(PrincipalFactory principalFactory) {
this.principalFactory = principalFactory;
}
public Optional<Principal> resolve(Credential credential,
Optional<Principal> currentPrincipal,
Optional<AuthenticationHandler> handler) {
String principalId = credential.getId();
if (principalId != null) {
Principal principal = principalFactory.createPrincipal(principalId);
return Optional.of(principal);
}
return currentPrincipal;
}
public boolean supports(Credential credential) {
return credential != null && credential.getId() != null;
}
public IPersonAttributeDao getAttributeRepository() {
return null;
}
}Resolver that chains multiple resolvers together:
package org.apereo.cas.authentication.principal.resolvers;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class ChainingPrincipalResolver implements PrincipalResolver {
private final List<PrincipalResolver> chain = new ArrayList<>();
public void addResolver(PrincipalResolver resolver) {
chain.add(resolver);
}
public Optional<Principal> resolve(Credential credential,
Optional<Principal> currentPrincipal,
Optional<AuthenticationHandler> handler) {
Optional<Principal> result = currentPrincipal;
for (PrincipalResolver resolver : chain) {
if (resolver.supports(credential)) {
result = resolver.resolve(credential, result, handler);
if (result.isPresent()) {
break;
}
}
}
return result;
}
public boolean supports(Credential credential) {
return chain.stream().anyMatch(resolver -> resolver.supports(credential));
}
public IPersonAttributeDao getAttributeRepository() {
return chain.stream()
.map(PrincipalResolver::getAttributeRepository)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
}Attribute mergers determine how to combine attributes from multiple sources:
package org.apereo.cas.authentication.principal.merger;
import java.util.List;
import java.util.Map;
public interface AttributeMerger {
Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
Map<String, List<Object>> toMerge);
}package org.apereo.cas.authentication.principal.merger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public abstract class BaseAdditiveAttributeMerger implements AttributeMerger {
public final Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
Map<String, List<Object>> toMerge) {
Map<String, List<Object>> results = new LinkedHashMap<>(toModify);
for (Map.Entry<String, List<Object>> entry : toMerge.entrySet()) {
String key = entry.getKey();
List<Object> values = entry.getValue();
if (results.containsKey(key)) {
List<Object> existingValues = new ArrayList<>(results.get(key));
List<Object> mergedValues = mergeValues(existingValues, values);
results.put(key, mergedValues);
} else {
results.put(key, new ArrayList<>(values));
}
}
return results;
}
protected abstract List<Object> mergeValues(List<Object> existing, List<Object> additional);
}Adds all values from both sources:
package org.apereo.cas.authentication.principal.merger;
import java.util.ArrayList;
import java.util.List;
public class MultivaluedAttributeMerger extends BaseAdditiveAttributeMerger {
protected List<Object> mergeValues(List<Object> existing, List<Object> additional) {
List<Object> merged = new ArrayList<>(existing);
merged.addAll(additional);
return merged;
}
}Only adds attributes that don't already exist:
package org.apereo.cas.authentication.principal.merger;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;
public class NoncollidingAttributeAdder implements AttributeMerger {
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
Map<String, List<Object>> toMerge) {
Map<String, List<Object>> results = new LinkedHashMap<>(toModify);
for (Map.Entry<String, List<Object>> entry : toMerge.entrySet()) {
String key = entry.getKey();
if (!results.containsKey(key)) {
results.put(key, entry.getValue());
}
}
return results;
}
}Replaces existing attributes with new values:
package org.apereo.cas.authentication.principal.merger;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ReplacingAttributeAdder implements AttributeMerger {
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
Map<String, List<Object>> toMerge) {
Map<String, List<Object>> results = new LinkedHashMap<>(toModify);
results.putAll(toMerge);
return results;
}
}Only returns the new/changed attributes:
package org.apereo.cas.authentication.principal.merger;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ReturnChangesAttributeMerger implements AttributeMerger {
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
Map<String, List<Object>> toMerge) {
return new LinkedHashMap<>(toMerge);
}
}Always returns the original attributes unchanged:
package org.apereo.cas.authentication.principal.merger;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ReturnOriginalAttributeMerger implements AttributeMerger {
public Map<String, List<Object>> mergeAttributes(Map<String, List<Object>> toModify,
Map<String, List<Object>> toMerge) {
return new LinkedHashMap<>(toModify);
}
}package org.apereo.cas.authentication.principal;
import org.apereo.cas.util.transforms.PrefixSuffixPrincipalNameTransformer;
import org.apereo.cas.util.transforms.RegexPrincipalNameTransformer;
import org.apereo.cas.util.transforms.UpperCasePrincipalNameTransformer;
import org.apereo.cas.util.transforms.LowerCasePrincipalNameTransformer;
public final class PrincipalNameTransformerUtils {
public static PrincipalNameTransformer newPrefixSuffixTransformer(String prefix, String suffix) {
return new PrefixSuffixPrincipalNameTransformer(prefix, suffix);
}
public static PrincipalNameTransformer newRegexTransformer(String pattern, String replacement) {
return new RegexPrincipalNameTransformer(pattern, replacement);
}
public static PrincipalNameTransformer newUpperCaseTransformer() {
return new UpperCasePrincipalNameTransformer();
}
public static PrincipalNameTransformer newLowerCaseTransformer() {
return new LowerCasePrincipalNameTransformer();
}
}package org.apereo.cas.authentication.principal;
import org.apereo.cas.authentication.AuthenticationHandler;
import java.util.Collection;
public interface PrincipalResolutionExecutionPlan {
void registerPrincipalResolver(PrincipalResolver resolver);
void registerPrincipalResolver(PrincipalResolver resolver, AuthenticationHandler handler);
Collection<PrincipalResolver> getRegisteredPrincipalResolvers();
}
public class DefaultPrincipalResolutionExecutionPlan implements PrincipalResolutionExecutionPlan {
private final Map<AuthenticationHandler, PrincipalResolver> resolverMap = new LinkedHashMap<>();
private PrincipalResolver globalResolver;
public void registerPrincipalResolver(PrincipalResolver resolver) {
this.globalResolver = resolver;
}
public void registerPrincipalResolver(PrincipalResolver resolver, AuthenticationHandler handler) {
resolverMap.put(handler, resolver);
}
public Collection<PrincipalResolver> getRegisteredPrincipalResolvers() {
Collection<PrincipalResolver> resolvers = new ArrayList<>(resolverMap.values());
if (globalResolver != null) {
resolvers.add(globalResolver);
}
return resolvers;
}
}@Configuration
public class PrincipalResolutionConfiguration {
@Bean
public PrincipalFactory principalFactory() {
return new DefaultPrincipalFactory();
}
@Bean
public PrincipalElectionStrategy principalElectionStrategy() {
return new DefaultPrincipalElectionStrategy();
}
@Bean
public AttributeMerger attributeMerger() {
return new MultivaluedAttributeMerger();
}
@Bean
public PrincipalResolver echoingPrincipalResolver() {
return new EchoingPrincipalResolver();
}
}// Create principal with attributes
Map<String, List<Object>> attributes = Map.of(
"email", List.of("user@example.com"),
"roles", List.of("admin", "user"),
"department", List.of("IT")
);
PrincipalFactory factory = new DefaultPrincipalFactory();
Principal principal = factory.createPrincipal("username", attributes);
// Merge attributes from multiple sources
AttributeMerger merger = new MultivaluedAttributeMerger();
Map<String, List<Object>> additionalAttrs = Map.of(
"groups", List.of("developers", "admins"),
"roles", List.of("manager") // This will be merged with existing roles
);
Map<String, List<Object>> mergedAttrs = merger.mergeAttributes(
principal.getAttributes(), additionalAttrs);
// Create election strategy chain
ChainingPrincipalElectionStrategy electionStrategy = new ChainingPrincipalElectionStrategy();
electionStrategy.addStrategy(new DefaultPrincipalElectionStrategy());Principal resolution provides the foundation for user identity management in CAS, allowing flexible attribute resolution, merging strategies, and principal election to support complex enterprise requirements.
Install with Tessl CLI
npx tessl i tessl/maven-org-apereo-cas--cas-server-core-authentication-api