Spring Security support for Apereo's Central Authentication Service (CAS) enabling Single Sign-On authentication
—
Jackson module for serializing CAS authentication tokens and related objects in distributed session scenarios. This module enables proper JSON serialization and deserialization of CAS authentication objects for session management in clustered applications.
Jackson module that provides JSON serialization support for CAS authentication types, enabling distributed session management and caching scenarios.
/**
* Jackson module for JSON serialization of CAS authentication objects.
* Registers mixins for proper serialization of CAS types in distributed environments.
*/
public class CasJackson2Module extends SimpleModule {
/**
* Creates CAS Jackson2 module with default configuration.
* Automatically registers mixins for CAS authentication types.
*/
public CasJackson2Module();
/**
* Sets up the module by registering mixins for CAS types.
* Called automatically by Jackson during module registration.
* @param context Jackson setup context for module configuration
*/
public void setupModule(SetupContext context);
}Usage Example:
@Configuration
public class JacksonConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new CasJackson2Module());
return mapper;
}
}The module includes several Jackson mixin classes that handle the serialization complexity of CAS objects:
Mixin for deserializing CasAuthenticationToken objects using Jackson.
/**
* Mixin class for CasAuthenticationToken deserialization.
* Uses the private constructor designed for Jackson deserialization.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
getterVisibility = JsonAutoDetect.Visibility.NONE,
creatorVisibility = JsonAutoDetect.Visibility.ANY)
@JsonIgnoreProperties(ignoreUnknown = true)
class CasAuthenticationTokenMixin {
/**
* Mixin constructor for deserializing CasAuthenticationToken.
* @param keyHash hashCode of the provider key for token validation
* @param principal the authenticated principal (usually username)
* @param credentials the service/proxy ticket ID from CAS
* @param authorities granted authorities for the user
* @param userDetails detailed user information
* @param assertion CAS assertion containing user attributes
*/
@JsonCreator
CasAuthenticationTokenMixin(
@JsonProperty("keyHash") Integer keyHash,
@JsonProperty("principal") Object principal,
@JsonProperty("credentials") Object credentials,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("userDetails") UserDetails userDetails,
@JsonProperty("assertion") Assertion assertion);
}Mixin for deserializing CAS AssertionImpl objects from the Apereo CAS client.
/**
* Mixin for deserializing AssertionImpl from org.apereo.cas.client.validation package.
* Handles CAS assertion objects containing authentication details and user attributes.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
class AssertionImplMixin {
/**
* Mixin constructor for deserializing AssertionImpl.
* @param principal the Principal associated with the assertion
* @param validFromDate when the assertion becomes valid
* @param validUntilDate when the assertion expires
* @param authenticationDate when the user was authenticated
* @param attributes key/value pairs for assertion attributes
*/
@JsonCreator
AssertionImplMixin(
@JsonProperty("principal") AttributePrincipal principal,
@JsonProperty("validFromDate") Date validFromDate,
@JsonProperty("validUntilDate") Date validUntilDate,
@JsonProperty("authenticationDate") Date authenticationDate,
@JsonProperty("attributes") Map<String, Object> attributes);
}Mixin for deserializing CAS AttributePrincipalImpl objects from the Apereo CAS client.
/**
* Mixin for deserializing AttributePrincipalImpl from org.apereo.cas.client.authentication package.
* Handles CAS principal objects containing user identity and attributes.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
class AttributePrincipalImplMixin {
/**
* Mixin constructor for deserializing AttributePrincipalImpl.
* @param name unique identifier for the principal (username)
* @param attributes key/value pairs for user attributes
* @param proxyGrantingTicket ticket for obtaining proxy tickets
* @param proxyRetriever callback implementation for CAS server communication
*/
@JsonCreator
AttributePrincipalImplMixin(
@JsonProperty("name") String name,
@JsonProperty("attributes") Map<String, Object> attributes,
@JsonProperty("proxyGrantingTicket") String proxyGrantingTicket,
@JsonProperty("proxyRetriever") ProxyRetriever proxyRetriever);
}The module provides serialization support for the following CAS types:
// Example: Serializing CasAuthenticationToken
CasAuthenticationToken token = new CasAuthenticationToken(
"cas-key",
"username",
"ST-123456-abcdef",
authorities,
userDetails,
assertion
);
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new CasJackson2Module());
// Serialize to JSON
String json = mapper.writeValueAsString(token);
// Deserialize from JSON
CasAuthenticationToken deserializedToken = mapper.readValue(json, CasAuthenticationToken.class);@SpringBootApplication
public class Application {
@Bean
public Module casJackson2Module() {
return new CasJackson2Module();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new CasJackson2Module());
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
return new GenericJackson2JsonRedisSerializer(mapper);
}
}@Configuration
public class HazelcastSessionConfig {
@Bean
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
// Configure serialization for CAS objects
SerializationConfig serializationConfig = config.getSerializationConfig();
serializationConfig.addSerializerConfig(
new SerializerConfig()
.setTypeClass(CasAuthenticationToken.class)
.setImplementation(new JsonSerializer())
);
return Hazelcast.newHazelcastInstance(config);
}
public static class JsonSerializer implements StreamSerializer<CasAuthenticationToken> {
private final ObjectMapper mapper;
public JsonSerializer() {
this.mapper = new ObjectMapper();
this.mapper.registerModule(new CasJackson2Module());
}
@Override
public void write(ObjectDataOutput out, CasAuthenticationToken token) throws IOException {
String json = mapper.writeValueAsString(token);
out.writeUTF(json);
}
@Override
public CasAuthenticationToken read(ObjectDataInput in) throws IOException {
String json = in.readUTF();
return mapper.readValue(json, CasAuthenticationToken.class);
}
@Override
public int getTypeId() {
return 1;
}
}
}@Configuration
public class SessionConfig {
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
@Bean
public SessionRepository<MapSession> sessionRepository() {
MapSessionRepository repository = new MapSessionRepository(new ConcurrentHashMap<>());
repository.setDefaultMaxInactiveInterval(Duration.ofMinutes(30));
return repository;
}
@Bean
public ObjectMapper sessionObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new CasJackson2Module());
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
return mapper;
}
}@Configuration
public class SecurityContextConfig {
@Bean
public SecurityContextRepository securityContextRepository() {
HttpSessionSecurityContextRepository repository = new HttpSessionSecurityContextRepository();
repository.setAllowSessionCreation(true);
return repository;
}
@Bean
public ObjectMapper securityContextObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new CasJackson2Module());
mapper.registerModule(new CoreJackson2Module());
return mapper;
}
}@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(casRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory())
.cacheDefaults(config)
.build();
}
@Bean
public RedisSerializer<Object> casRedisSerializer() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new CasJackson2Module());
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
return new GenericJackson2JsonRedisSerializer(mapper);
}
}@Configuration
public class CustomJacksonConfig {
@Bean
public ObjectMapper customObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Register CAS module
mapper.registerModule(new CasJackson2Module());
// Add custom mixins for additional types
mapper.addMixIn(CustomUserDetails.class, CustomUserDetailsMixin.class);
// Configure additional settings
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return mapper;
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static abstract class CustomUserDetailsMixin {
@JsonIgnore
abstract String getPassword();
}
}{
"@class": "org.springframework.security.cas.authentication.CasAuthenticationToken",
"keyHash": 123456789,
"principal": "username",
"credentials": "ST-123456-abcdefghijklmnop",
"authorities": [
{
"@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
"authority": "ROLE_USER"
}
],
"userDetails": {
"@class": "org.springframework.security.core.userdetails.User",
"username": "username",
"authorities": [...],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
},
"assertion": {
"@class": "org.apereo.cas.client.validation.AssertionImpl",
"principal": {
"@class": "org.apereo.cas.client.authentication.AttributePrincipalImpl",
"name": "username",
"attributes": {
"email": "user@example.com",
"role": ["USER", "ADMIN"]
}
},
"validFromDate": "2023-10-01T10:00:00.000+00:00",
"validUntilDate": "2023-10-01T10:05:00.000+00:00",
"authenticationAttributes": {}
},
"authenticated": true
}// Issue: Circular reference during serialization
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class User implements UserDetails {
// ...
}
// Issue: Missing default constructor
public class CustomUserDetails implements UserDetails {
@JsonCreator
public CustomUserDetails(@JsonProperty("username") String username) {
this.username = username;
}
}
// Issue: Incompatible Jackson versions
@Configuration
public class JacksonVersionConfig {
@Bean
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
return new Jackson2ObjectMapperBuilder()
.modules(new CasJackson2Module())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}@JsonTypeInfo for secure polymorphic deserializationInstall with Tessl CLI
npx tessl i tessl/maven-org-springframework-security--spring-security-cas