Apereo CAS Core Utilities - A comprehensive utility library providing functional programming constructs, encryption utilities, configuration helpers, and core infrastructure components for the Central Authentication Service framework
—
Message sanitization and text processing utilities for secure handling of user input, system messages, and principal name transformations in CAS applications.
Core interface for message sanitization providing secure handling of potentially malicious or sensitive content.
public interface MessageSanitizer {
// Primary sanitization method
String sanitize(String message);
// Batch sanitization
Collection<String> sanitize(Collection<String> messages);
Map<String, String> sanitize(Map<String, String> messageMap);
// Configuration methods
boolean isEnabled();
Set<String> getSupportedPatterns();
}Default implementation providing comprehensive message sanitization with configurable rules and patterns.
public class DefaultMessageSanitizer implements MessageSanitizer {
// Configuration
private final boolean enabled;
private final Set<Pattern> sanitizationPatterns;
private final Map<String, String> replacementMap;
// Constructors
public DefaultMessageSanitizer();
public DefaultMessageSanitizer(boolean enabled);
public DefaultMessageSanitizer(Set<Pattern> patterns, Map<String, String> replacements);
// MessageSanitizer implementation
@Override
public String sanitize(String message);
@Override
public Collection<String> sanitize(Collection<String> messages);
@Override
public Map<String, String> sanitize(Map<String, String> messageMap);
@Override
public boolean isEnabled();
@Override
public Set<String> getSupportedPatterns();
// Configuration methods
public void addSanitizationPattern(Pattern pattern, String replacement);
public void addSanitizationRule(String regex, String replacement);
public void removeSanitizationPattern(Pattern pattern);
}Basic message sanitization:
@Service
public class UserInputService {
private final MessageSanitizer messageSanitizer;
public UserInputService(MessageSanitizer messageSanitizer) {
this.messageSanitizer = messageSanitizer;
}
public String processUserComment(String rawComment) {
if (StringUtils.isBlank(rawComment)) {
return rawComment;
}
// Sanitize potentially malicious content
String sanitized = messageSanitizer.sanitize(rawComment);
// Additional validation
if (sanitized.length() > 1000) {
throw new IllegalArgumentException("Comment too long after sanitization");
}
return sanitized;
}
public Map<String, String> processFormData(Map<String, String> formData) {
// Sanitize all form field values
Map<String, String> sanitized = messageSanitizer.sanitize(formData);
// Log sanitization if content was changed
formData.forEach((key, original) -> {
String cleaned = sanitized.get(key);
if (!Objects.equals(original, cleaned)) {
log.info("Sanitized field '{}': '{}' -> '{}'", key, original, cleaned);
}
});
return sanitized;
}
public List<String> processErrorMessages(List<String> errorMessages) {
// Sanitize error messages before display
Collection<String> sanitized = messageSanitizer.sanitize(errorMessages);
return new ArrayList<>(sanitized);
}
}Custom sanitizer configuration:
@Configuration
public class SanitizationConfiguration {
@Bean
public MessageSanitizer customMessageSanitizer() {
DefaultMessageSanitizer sanitizer = new DefaultMessageSanitizer(true);
// Remove script tags
sanitizer.addSanitizationRule("<script[^>]*>.*?</script>", "");
// Remove potentially dangerous HTML attributes
sanitizer.addSanitizationRule("\\s*javascript\\s*:", "");
sanitizer.addSanitizationRule("\\s*vbscript\\s*:", "");
sanitizer.addSanitizationRule("\\s*onload\\s*=", "");
sanitizer.addSanitizationRule("\\s*onerror\\s*=", "");
// Sanitize SQL injection attempts
sanitizer.addSanitizationRule("(?i)(union|select|insert|update|delete|drop)\\s", "");
// Remove excessive whitespace
sanitizer.addSanitizationRule("\\s+", " ");
// Replace sensitive patterns
sanitizer.addSanitizationRule("password\\s*[=:]\\s*\\S+", "password=[REDACTED]");
sanitizer.addSanitizationRule("token\\s*[=:]\\s*\\S+", "token=[REDACTED]");
return sanitizer;
}
@Bean
@ConditionalOnProperty(name = "cas.message.sanitization.strict", havingValue = "true")
public MessageSanitizer strictMessageSanitizer() {
DefaultMessageSanitizer sanitizer = new DefaultMessageSanitizer(true);
// Strict HTML removal
sanitizer.addSanitizationRule("<[^>]+>", "");
// Allow only alphanumeric and basic punctuation
sanitizer.addSanitizationRule("[^a-zA-Z0-9\\s.,!?-]", "");
return sanitizer;
}
}Interface for contributing custom sanitization rules and patterns to the message sanitization process.
public interface MessageSanitationContributor {
// Contribution methods
Collection<Pattern> getPatterns();
Map<String, String> getReplacements();
// Priority and ordering
int getOrder();
// Conditional application
boolean supports(String messageType);
boolean isEnabled();
}Specialized contributor for ticket-related message sanitization.
public class TicketCatalogMessageSanitationContributor implements MessageSanitationContributor {
// Constructor
public TicketCatalogMessageSanitationContributor();
@Override
public Collection<Pattern> getPatterns();
@Override
public Map<String, String> getReplacements();
@Override
public int getOrder();
@Override
public boolean supports(String messageType);
@Override
public boolean isEnabled();
}Custom sanitization contributor:
@Component
@Order(100)
public class AuthenticationMessageSanitationContributor implements MessageSanitationContributor {
private final Collection<Pattern> patterns;
private final Map<String, String> replacements;
public AuthenticationMessageSanitationContributor() {
this.patterns = Arrays.asList(
Pattern.compile("(?i)credential\\s*[=:]\\s*\\S+"),
Pattern.compile("(?i)password\\s*[=:]\\s*\\S+"),
Pattern.compile("(?i)secret\\s*[=:]\\s*\\S+"),
Pattern.compile("TGT-\\d+-\\S+"), // Ticket Granting Tickets
Pattern.compile("ST-\\d+-\\S+") // Service Tickets
);
this.replacements = Map.of(
"(?i)credential\\s*[=:]\\s*\\S+", "credential=[PROTECTED]",
"(?i)password\\s*[=:]\\s*\\S+", "password=[PROTECTED]",
"(?i)secret\\s*[=:]\\s*\\S+", "secret=[PROTECTED]",
"TGT-\\d+-\\S+", "TGT-***",
"ST-\\d+-\\S+", "ST-***"
);
}
@Override
public Collection<Pattern> getPatterns() {
return patterns;
}
@Override
public Map<String, String> getReplacements() {
return replacements;
}
@Override
public int getOrder() {
return 100;
}
@Override
public boolean supports(String messageType) {
return messageType != null && (
messageType.contains("authentication") ||
messageType.contains("login") ||
messageType.contains("ticket")
);
}
@Override
public boolean isEnabled() {
return true;
}
}
@Component
@Order(200)
public class SessionMessageSanitationContributor implements MessageSanitationContributor {
@Override
public Collection<Pattern> getPatterns() {
return Arrays.asList(
Pattern.compile("JSESSIONID=[^;\\s]+"),
Pattern.compile("sessionId=[^;\\s]+"),
Pattern.compile("sid=[^;\\s]+")
);
}
@Override
public Map<String, String> getReplacements() {
return Map.of(
"JSESSIONID=[^;\\s]+", "JSESSIONID=[HIDDEN]",
"sessionId=[^;\\s]+", "sessionId=[HIDDEN]",
"sid=[^;\\s]+", "sid=[HIDDEN]"
);
}
@Override
public int getOrder() {
return 200;
}
@Override
public boolean supports(String messageType) {
return true; // Apply to all message types
}
@Override
public boolean isEnabled() {
return true;
}
}Composite sanitizer with contributors:
@Service
public class CompositeMessageSanitizationService {
private final List<MessageSanitationContributor> contributors;
private final MessageSanitizer baseSanitizer;
public CompositeMessageSanitizationService(
List<MessageSanitationContributor> contributors,
MessageSanitizer baseSanitizer) {
// Sort contributors by order
this.contributors = contributors.stream()
.filter(MessageSanitationContributor::isEnabled)
.sorted(Comparator.comparingInt(MessageSanitationContributor::getOrder))
.collect(Collectors.toList());
this.baseSanitizer = baseSanitizer;
}
public String sanitizeMessage(String message, String messageType) {
if (StringUtils.isBlank(message)) {
return message;
}
String sanitized = message;
// Apply base sanitization first
if (baseSanitizer.isEnabled()) {
sanitized = baseSanitizer.sanitize(sanitized);
}
// Apply contributor-specific sanitization
for (MessageSanitationContributor contributor : contributors) {
if (contributor.supports(messageType)) {
sanitized = applyContributorSanitization(sanitized, contributor);
}
}
return sanitized;
}
private String applyContributorSanitization(String message, MessageSanitationContributor contributor) {
String sanitized = message;
// Apply replacement patterns
Map<String, String> replacements = contributor.getReplacements();
for (Map.Entry<String, String> entry : replacements.entrySet()) {
sanitized = sanitized.replaceAll(entry.getKey(), entry.getValue());
}
// Apply additional patterns
Collection<Pattern> patterns = contributor.getPatterns();
for (Pattern pattern : patterns) {
sanitized = pattern.matcher(sanitized).replaceAll("[SANITIZED]");
}
return sanitized;
}
}Interface for transforming principal names during authentication processing.
public interface PrincipalNameTransformer {
// Primary transformation method
String transform(String formUserId);
// Configuration methods
String getName();
boolean isEnabled();
}No-operation transformer that returns the input unchanged.
public class NoOpPrincipalNameTransformer implements PrincipalNameTransformer {
@Override
public String transform(String formUserId);
@Override
public String getName();
@Override
public boolean isEnabled();
}Transformer for case conversion (uppercase/lowercase).
public class ConvertCasePrincipalNameTransformer implements PrincipalNameTransformer {
// Case conversion modes
public enum CaseConversion {
UPPERCASE, LOWERCASE, NONE
}
// Constructor
public ConvertCasePrincipalNameTransformer(CaseConversion conversion);
@Override
public String transform(String formUserId);
}Transformer for adding prefixes and/or suffixes to principal names.
public class PrefixSuffixPrincipalNameTransformer implements PrincipalNameTransformer {
// Constructor
public PrefixSuffixPrincipalNameTransformer(String prefix, String suffix);
@Override
public String transform(String formUserId);
}Transformer using regular expressions for complex name transformations.
public class RegexPrincipalNameTransformer implements PrincipalNameTransformer {
// Constructor
public RegexPrincipalNameTransformer(String pattern, String replacement);
public RegexPrincipalNameTransformer(Pattern compiledPattern, String replacement);
@Override
public String transform(String formUserId);
}Transformer that blocks/rejects certain principal names.
public class BlockingPrincipalNameTransformer implements PrincipalNameTransformer {
// Constructor
public BlockingPrincipalNameTransformer(Set<String> blockedNames);
public BlockingPrincipalNameTransformer(Pattern blockingPattern);
@Override
public String transform(String formUserId);
}Transformer that chains multiple transformers in sequence.
public class ChainingPrincipalNameTransformer implements PrincipalNameTransformer {
// Constructor
public ChainingPrincipalNameTransformer(List<PrincipalNameTransformer> transformers);
@Override
public String transform(String formUserId);
// Chain management
public void addTransformer(PrincipalNameTransformer transformer);
public List<PrincipalNameTransformer> getTransformers();
}Transformer using Groovy scripts for dynamic transformations.
public class GroovyPrincipalNameTransformer implements PrincipalNameTransformer {
// Constructor
public GroovyPrincipalNameTransformer(String groovyScript);
public GroovyPrincipalNameTransformer(Resource groovyScriptResource);
@Override
public String transform(String formUserId);
}Basic transformers:
@Configuration
public class PrincipalTransformerConfiguration {
@Bean
@ConditionalOnProperty(name = "cas.principal.transform.case", havingValue = "lowercase")
public PrincipalNameTransformer lowercaseTransformer() {
return new ConvertCasePrincipalNameTransformer(CaseConversion.LOWERCASE);
}
@Bean
@ConditionalOnProperty(name = "cas.principal.transform.domain.enabled", havingValue = "true")
public PrincipalNameTransformer domainTransformer(
@Value("${cas.principal.transform.domain.suffix:@example.com}") String suffix) {
return new PrefixSuffixPrincipalNameTransformer("", suffix);
}
@Bean
public PrincipalNameTransformer emailToUsernameTransformer() {
// Transform email addresses to usernames
return new RegexPrincipalNameTransformer("(.+)@.+", "$1");
}
@Bean
public PrincipalNameTransformer blockingTransformer() {
// Block administrative accounts
Set<String> blockedNames = Set.of("admin", "root", "administrator", "guest");
return new BlockingPrincipalNameTransformer(blockedNames);
}
}Complex chained transformations:
@Configuration
public class ComplexPrincipalTransformation {
@Bean
public PrincipalNameTransformer chainedPrincipalTransformer() {
List<PrincipalNameTransformer> transformers = Arrays.asList(
// 1. Block dangerous usernames first
new BlockingPrincipalNameTransformer(Set.of("admin", "root")),
// 2. Extract username from email if present
new RegexPrincipalNameTransformer("(.+)@.+", "$1"),
// 3. Convert to lowercase
new ConvertCasePrincipalNameTransformer(CaseConversion.LOWERCASE),
// 4. Remove special characters
new RegexPrincipalNameTransformer("[^a-z0-9._-]", ""),
// 5. Add domain if not present
new RegexPrincipalNameTransformer("^([^@]+)$", "$1@company.com")
);
return new ChainingPrincipalNameTransformer(transformers);
}
}Groovy-based dynamic transformation:
@Bean
@ConditionalOnProperty(name = "cas.principal.transform.groovy.enabled", havingValue = "true")
public PrincipalNameTransformer groovyPrincipalTransformer() {
String groovyScript = """
// Dynamic principal transformation script
// Input: formUserId (String)
// Output: transformed principal name (String)
if (formUserId == null || formUserId.isEmpty()) {
return formUserId
}
def transformed = formUserId.toLowerCase()
// Handle email addresses
if (transformed.contains('@')) {
def parts = transformed.split('@')
def username = parts[0]
def domain = parts[1]
// Map domains to internal format
def domainMappings = [
'gmail.com': 'external',
'company.com': 'internal',
'contractor.com': 'contractor'
]
def mappedDomain = domainMappings[domain] ?: 'unknown'
return username + '.' + mappedDomain
}
// Handle employee ID format (EMP123456)
if (transformed.startsWith('emp')) {
return transformed.substring(3)
}
// Default transformation
return transformed.replaceAll('[^a-z0-9._-]', '')
""";
return new GroovyPrincipalNameTransformer(groovyScript);
}Usage in authentication handler:
@Component
public class CustomAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
private final PrincipalNameTransformer principalTransformer;
private final UserRepository userRepository;
public CustomAuthenticationHandler(
PrincipalNameTransformer principalTransformer,
UserRepository userRepository) {
this.principalTransformer = principalTransformer;
this.userRepository = userRepository;
}
@Override
protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal(
UsernamePasswordCredential credential,
String originalPassword) throws GeneralSecurityException {
try {
// Transform the principal name
String originalUsername = credential.getUsername();
String transformedUsername = principalTransformer.transform(originalUsername);
log.debug("Transformed principal: '{}' -> '{}'", originalUsername, transformedUsername);
// Authenticate with transformed username
User user = userRepository.findByUsername(transformedUsername);
if (user == null) {
throw new AccountNotFoundException("User not found: " + transformedUsername);
}
if (!passwordEncoder.matches(originalPassword, user.getPasswordHash())) {
throw new FailedLoginException("Invalid credentials");
}
// Create principal with transformed name
Principal principal = principalFactory.createPrincipal(
transformedUsername,
user.getAttributes()
);
return createHandlerResult(credential, principal);
} catch (Exception e) {
log.error("Authentication failed for user: {}", credential.getUsername(), e);
throw new FailedLoginException("Authentication failed", e);
}
}
}This text processing library provides comprehensive capabilities for secure message handling and flexible principal name transformations, essential for production CAS deployments with security and compliance requirements.
Install with Tessl CLI
npx tessl i tessl/maven-org-apereo-cas--cas-server-core-util-api