Identity DSL
Identity DSL is a fluent, type-safe Java API for configuring your entire authentication platform. Define authentication methods, state management, and Spring Security settings in a single PlatformSecurityConfig class.
Quick Start
The simplest configuration — form login with session-based state:
@Configuration
@EnableWebSecurity
public class PlatformSecurityConfig {
@Bean
public PlatformConfig platformDslConfig(
IdentityDslRegistry<HttpSecurity> registry) throws Exception {
return registry
.form(form -> form.loginPage("/login").defaultSuccessUrl("/home"))
.session(Customizer.withDefaults())
.build();
}
}
Why Contexa DSL?
Spring Security requires a separate @Bean method for every SecurityFilterChain. Three login pages means three methods with repeated boilerplate. Contexa's Identity DSL eliminates this entirely — one fluent chain, one method, any number of filter chains.
Spring Security: 3 Filter Chains
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean @Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/admin/**")
.csrf(AbstractHttpConfigurer::disable)
.formLogin(form -> form.loginPage("/admin/login").defaultSuccessUrl("/admin"))
.sessionManagement(session -> session.sessionFixation().changeSessionId())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.build();
}
@Bean @Order(2)
public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.csrf(AbstractHttpConfigurer::disable)
.formLogin(form -> form.loginPage("/api/login").defaultSuccessUrl("/api"))
.sessionManagement(session -> session.sessionFixation().changeSessionId())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.build();
}
@Bean @Order(3)
public SecurityFilterChain userChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(form -> form.loginPage("/login").defaultSuccessUrl("/home"))
.sessionManagement(session -> session.sessionFixation().changeSessionId())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.build();
}
}
// 3 @Bean methods, ~40 lines, repeated boilerplate per chain
Contexa Identity DSL: Same Result
@Bean
public PlatformConfig platformDslConfig(
IdentityDslRegistry<HttpSecurity> registry) throws Exception {
return registry
.global(http -> {
http.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionFixation().changeSessionId())
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated());
})
.form(f -> f.order(10).loginPage("/admin/login").defaultSuccessUrl("/admin")
.rawHttp(http -> http.securityMatcher("/admin/**")))
.session(Customizer.withDefaults())
.form(f -> f.order(20).loginPage("/api/login").defaultSuccessUrl("/api")
.rawHttp(http -> http.securityMatcher("/api/**")))
.session(Customizer.withDefaults())
.form(f -> f.order(30).loginPage("/login").defaultSuccessUrl("/home"))
.session(Customizer.withDefaults())
.build();
}
// 1 method, ~15 lines, reduced boilerplate, 3 SecurityFilterChain beans registered at runtime
.form().session().form().session().build() — one fluent chain creates multiple filter chains automaticallySecurityFilterChainRegistrar registers SecurityFilterChain beans at runtime via BeanDefinitionRegistry.session() or .oauth2()). In a plain Spring Security setup, form login and stateless resource-server flows are usually configured as separate chains.DSL Architecture
The DSL follows a two-phase chained builder pattern. Each .auth().state() pair creates an independent SecurityFilterChain, ordered by priority.
Two-Phase Builder Pattern
Multiple Chains from One Registry
Each .auth().state() pair produces a separate SecurityFilterChain. The registry collects all pairs and SecurityFilterChainRegistrar registers them as Spring beans at runtime.
Each chain is isolated with its own filters, authentication strategy, and state management. Requests are routed by order() and securityMatcher — the first matching chain handles the request.
How It Works: Dynamic Bean Registration
You write one @Bean method returning PlatformConfig. Contexa's bootstrap pipeline reads it and registers multiple SecurityFilterChain beans automatically — no extra @Bean annotations needed.
What You Write vs. What Spring Sees
@Bean methodreturns
PlatformConfigSecurityFilterChain beansregistered at runtime
formSecurityFilterChain1restSecurityFilterChain2ottSecurityFilterChain3Bootstrap Pipeline
order=10, matcher=/admin/**
order=20, matcher=/api/**
order=30, matcher=/**
independent filters
independent filters
independent filters
login page, filters
bearer filters
magic link filters
order=10
order=20
order=30
SecurityFilterChainRegistrar accesses Spring's BeanDefinitionRegistry directly. You write one @Bean method returning PlatformConfig, yet Spring sees multiple SecurityFilterChain beans — exactly as if you had written separate @Bean methods for each.
Global Configuration
global() applies shared HTTP security settings to all filter chains. Individual authentication method settings override global settings when they conflict (the SecurityConfigurerOrchestrator applies global first, then flow-specific).
registry
.global(http -> {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authReq -> authReq
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().access(customAuthorizationManager))
.securityContext(sc ->
sc.securityContextRepository(customRepository));
})
// ... authentication methods follow
Override rule: Global configurer runs first, then each flow's own configurer runs. If a flow sets disableCsrf() or rawHttp(), it overrides the global CSRF setting for that specific chain only.
Filter Chain Order & Request Routing
Each authentication method has a default order() value. Lower order = higher priority = intercepts requests first. When multiple filter chains are registered, Spring Security evaluates them in order and uses the first chain whose securityMatcher matches the request.
| Auth Method | Default Order | Description |
|---|---|---|
| Form Login | 100 | Traditional form-based login |
| MFA | 200 | Multi-factor authentication |
| REST API | 200 | JSON credential submission |
| One-Time Token | 300 | Magic link / OTT |
| Passkey | 400 | WebAuthn / FIDO2 |
Use order() and rawHttp(http -> http.securityMatcher(...)) to control which chain handles which requests:
registry
.global(http -> { /* shared settings */ })
// Admin pages: Form login, order 20 (highest priority)
.form(f -> f.order(20).loginPage("/admin/login")
.rawHttp(http -> http.securityMatcher("/admin/**")))
.session(Customizer.withDefaults())
// API endpoints: REST login, order 50
.rest(r -> r.order(50).loginProcessingUrl("/api/auth")
.rawHttp(http -> http.securityMatcher("/api/**")))
.oauth2(Customizer.withDefaults())
// General users: MFA, order 100
.mfa(m -> m.order(100)
.primaryAuthentication(p -> p.formLogin(form -> form.loginPage("/login")))
.passkey(Customizer.withDefaults()))
.session(Customizer.withDefaults())
.build();
Without securityMatcher, a chain matches all requests. Always set securityMatcher via rawHttp() when using multiple chains to avoid overlapping.
Authentication + State Combinations
Every authentication method can be paired with either session or OAuth2 state. You can mix different state strategies across different chains.
| Auth Method | + Session | + OAuth2 |
|---|---|---|
| Form Login | .form(...).session(...) | .form(...).oauth2(...) |
| REST API | .rest(...).session(...) | .rest(...).oauth2(...) |
| One-Time Token | .ott(...).session(...) | .ott(...).oauth2(...) |
| Passkey | .passkey(...).session(...) | .passkey(...).oauth2(...) |
| MFA | .mfa(...).session(...) | .mfa(...).oauth2(...) |
Per-flow state: The .state() call applies to the most recently registered authentication method. This allows different state strategies per chain:
registry
.form(f -> f.loginPage("/login"))
.session(Customizer.withDefaults()) // form -> Session
.rest(r -> r.loginProcessingUrl("/api/auth"))
.oauth2(Customizer.withDefaults()) // rest -> OAuth2
.mfa(m -> m.primaryAuthentication(...).passkey(...))
.session(Customizer.withDefaults()) // mfa -> Session
.build();
The same authentication method can be registered multiple times (auto-named form_flow, form_flow_2, etc.).
Spring Security: Separate Form and Stateless Chains
Spring Security supports both form login and stateless resource-server flows, but they are typically expressed as separate SecurityFilterChain definitions. Contexa's DSL keeps those pairings in one registry while still producing independent chains.
// Spring Security: form login and stateless API flows are typically modeled as separate chains
http.formLogin(form -> form.loginPage("/login"));
// This chain remains session-based unless you build a separate stateless chain.
// A stateless API flow is typically configured as a separate chain:
@Bean
public SecurityFilterChain resourceServerChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}
// The stateless API flow is still configured as an independent chain.
Contexa DSL: Any Auth x Any State
Contexa decouples authentication from state management. The same .form() can use .session() in one chain and .oauth2() in another — in a single method.
registry
// Admin: form login with OAuth2 token state
.form(f -> f.order(20).loginPage("/admin/login")
.rawHttp(http -> http.securityMatcher("/admin/**")))
.oauth2(Customizer.withDefaults())
// API: REST login with OAuth2 token state (stateless)
.rest(r -> r.order(50).loginProcessingUrl("/api/auth")
.rawHttp(http -> http.securityMatcher("/api/**")))
.oauth2(Customizer.withDefaults())
// Users: MFA with session state
.mfa(m -> m.order(100)
.primaryAuthentication(auth -> auth.formLogin(form -> form.loginPage("/login")))
.passkey(Customizer.withDefaults()))
.session(Customizer.withDefaults())
.build();
// 5 auth methods x 2 state types = 10 combinations, all in one method
rawHttp() — Full Spring Security Access
rawHttp() provides a SafeHttpCustomizer<HttpSecurity> hook that exposes the entire Spring Security HttpSecurity API. It supports checked exceptions and can be called multiple times (each call accumulates).
.form(f -> f.order(20)
.rawHttp(http -> http
.securityMatcher("/admin/**")
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class))
.rawHttp(http -> http
.headers(headers -> headers.frameOptions(fo -> fo.deny())))
)
Available on every authentication method and in global(). For form login, there is also rawFormLogin() to customize HttpSecurity.formLogin() internals directly.
SecurityFilterChain Customization
The globalHttpCustomizer lambda receives the HttpSecurity builder, giving you full access to Spring Security's configuration API. Below are common customization points.
CSRF Configuration
The default configuration disables CSRF. For applications serving HTML forms, consider enabling CSRF with a cookie-based token repository:
// Disable CSRF (default - suitable for stateless APIs)
http.csrf(AbstractHttpConfigurer::disable);
// Enable CSRF with cookie repository (for HTML form applications)
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
Session Management with AISessionSecurityContextRepository
AISessionSecurityContextRepository extends Spring's HttpSessionSecurityContextRepository with AI-enhanced session tracking. It feeds behavioral data (session patterns, access frequency, anomaly scores) into the authorization pipeline.
// Wire AI session context into the security filter chain
http.securityContext(sc ->
sc.securityContextRepository(aiSessionSecurityContextRepository)
);
// For stateless REST APIs, use stateless session management
// in the form/rest chain configuration:
registry.form(form -> form
.order(20)
.loginPage("/admin/login")
.defaultSuccessUrl("/admin")
.rawHttp(http -> http.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)))
);
CORS Settings
SafeHttpCustomizer<HttpSecurity> globalHttpCustomizer = http -> {
http
.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowCredentials(true);
return config;
}))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authReq -> authReq
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().access(customDynamicAuthorizationManager)
)
.securityContext(sc ->
sc.securityContextRepository(aiSessionSecurityContextRepository));
};
Custom Filters
Add custom filters using the rawHttp method on individual chain configurations, or include them in the global customizer for cross-chain application:
// Add a custom filter before the authorization filter
SafeHttpCustomizer<HttpSecurity> globalHttpCustomizer = http -> {
http
.addFilterBefore(new TenantContextFilter(),
AuthorizationFilter.class)
.authorizeHttpRequests(authReq -> authReq
.anyRequest().access(customDynamicAuthorizationManager)
);
};
Common Options
All authentication methods share these options (from AbstractOptions):
| Option | Description |
|---|---|
order(int) | Filter chain priority (lower = higher priority) |
loginProcessingUrl(String) | URL that processes login requests |
defaultSuccessUrl(String) | Redirect after successful login |
failureUrl(String) | Redirect after failed login |
successHandler(PlatformAuthenticationSuccessHandler) | Custom success handler |
failureHandler(PlatformAuthenticationFailureHandler) | Custom failure handler |
securityContextRepository(SecurityContextRepository) | Override security context storage |
disableCsrf() | Disable CSRF for this chain |
cors(Customizer) | Configure CORS |
headers(Customizer) | Configure HTTP headers |
sessionManagement(Customizer) | Configure session management |
logout(Customizer) | Configure logout |
rawHttp(SafeHttpCustomizer) | Full Spring Security API access (accumulates) |
authorizeStaticPermitAll(String...) | Permit static resources without auth |
asep(Customizer) | ASEP annotation attributes for this flow |
Complete Example
A production-ready configuration with admin form login (OAuth2), user MFA (Session), and API REST endpoint (OAuth2):
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class PlatformSecurityConfig {
private final CustomDynamicAuthorizationManager authorizationManager;
private final AISessionSecurityContextRepository securityContextRepository;
@Bean
public PlatformConfig platformDslConfig(
IdentityDslRegistry<HttpSecurity> registry) throws Exception {
return registry
// Global: shared across all chains
.global(http -> {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authReq -> authReq
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().access(authorizationManager))
.securityContext(sc ->
sc.securityContextRepository(securityContextRepository));
})
// Admin: form login with OAuth2, highest priority
.form(f -> f.order(20).loginPage("/admin/login")
.defaultSuccessUrl("/admin/dashboard")
.rawHttp(http -> http.securityMatcher("/admin/**")))
.oauth2(Customizer.withDefaults())
// API: REST login with OAuth2
.rest(r -> r.order(50).loginProcessingUrl("/api/auth")
.rawHttp(http -> http
.securityMatcher("/api/**")
.sessionManagement(s ->
s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))))
.oauth2(Customizer.withDefaults())
// Users: MFA with session
.mfa(m -> m.order(100)
.primaryAuthentication(auth ->
auth.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")))
.passkey(Customizer.withDefaults()))
.session(Customizer.withDefaults())
.build();
}
}
Migration Guide
Migrating from a traditional Spring Security configuration to Contexa's PlatformSecurityConfig involves replacing your SecurityFilterChain bean with the DSL registry pattern and wiring in the dynamic authorization manager.
Before: Traditional Spring Security
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/admin")
)
.build();
}
}
After: Contexa PlatformSecurityConfig
@EnableWebSecurity
@RequiredArgsConstructor
public class PlatformSecurityConfig {
private final CustomDynamicAuthorizationManager customDynamicAuthorizationManager;
private final AISessionSecurityContextRepository aiSessionSecurityContextRepository;
@Bean
public PlatformConfig platformDslConfig(
IdentityDslRegistry<HttpSecurity> registry) throws Exception {
SafeHttpCustomizer<HttpSecurity> globalHttpCustomizer = http -> {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico")
.permitAll()
.anyRequest()
.access(customDynamicAuthorizationManager)
)
.securityContext(sc -> sc
.securityContextRepository(aiSessionSecurityContextRepository));
};
return registry
.global(globalHttpCustomizer)
.form(form -> form.order(20)
.loginPage("/admin/login")
.defaultSuccessUrl("/admin"))
.oauth2(Customizer.withDefaults())
.build();
}
}
Migration Steps
spring-boot-starter-contexa to your project. This transitively brings in contexa-iam, contexa-core, and contexa-identity modules.hasRole() / hasAuthority() rules.security.zerotrust.mode=SHADOW to run dynamic authorization in audit-only mode. Decisions are logged but not enforced, so you can verify policies match expected behavior.SecurityFilterChain bean with the PlatformConfig pattern. Move static permitAll() rules to requestMatchers and replace all hasRole() / hasAuthority() with .anyRequest().access(customDynamicAuthorizationManager).security.zerotrust.mode=ENFORCE to activate live policy enforcement. Monitor authorization audit logs closely during the transition period.Migration Checklist
| Step | Action | Verification |
|---|---|---|
| 1 | Add spring-boot-starter-contexa dependency | Application starts without errors |
| 2 | Run Resource Scanner to discover endpoints | Scanned resources visible in Policy Center resource views |
| 3 | Create XACML policies for each endpoint | Policies visible in Policy Center and related policy detail flows |
| 4 | Enable shadow mode (security.zerotrust.mode=SHADOW) | Audit logs show authorization decisions |
| 5 | Replace SecurityFilterChain with PlatformConfig | All filter chains build successfully |
| 6 | Remove static hasRole() / hasAuthority() rules | SecurityFilterChain configuration no longer contains legacy hasRole() / hasAuthority() request rules |
| 7 | Verify shadow mode audit logs match expected behavior | Zero unexpected deny/allow decisions |
| 8 | Switch to enforce mode (security.zerotrust.mode=ENFORCE) | Application behaves identically to shadow mode |
DSL API Reference
IdentityAuthDsl
Authentication-phase interface. Each method returns IdentityStateDsl for state selection.
| Method | Returns | Description |
|---|---|---|
global(SafeHttpCustomizer<HttpSecurity>) | IdentityAuthDsl | Shared HTTP security settings for all chains |
form(Customizer<FormConfigurerConfigurer>) | IdentityStateDsl | Register form login (default order: 100) |
rest(Customizer<RestConfigurerConfigurer>) | IdentityStateDsl | Register REST API login (default order: 200) |
ott(Customizer<OttConfigurerConfigurer>) | IdentityStateDsl | Register One-Time Token (default order: 300) |
passkey(Customizer<PasskeyConfigurerConfigurer>) | IdentityStateDsl | Register Passkey/WebAuthn (default order: 400) |
mfa(Customizer<MfaDslConfigurer>) | IdentityStateDsl | Register multi-factor auth (default order: 200) |
build() | PlatformConfig | Finalize and produce immutable config |
IdentityStateDsl
State-management phase. Returned after registering an authentication method.
| Method | Returns | Description |
|---|---|---|
session(Customizer<SessionStateConfigurer>) | IdentityAuthDsl | Session-based state (auto: sessionFixation.changeSessionId) |
oauth2(Customizer<OAuth2StateConfigurer>) | IdentityAuthDsl | OAuth2/JWT state (auto: Resource Server + Auth Server) |
For
application.yml properties (login/logout URIs, OAuth2 client settings, token endpoints, MFA settings), see State Management and Identity Configuration.