I'm using Dynamic datasource routing as indicated in this blog post:
http://spring.io/blog/2007/01/23/dynamic-datasource-routing/
This works fine, but when I combine it with spring-data-rest and browsing of my generated repositories I (rightfully) get an exception that my lookup-key is not defined (I do not set a default).
How and where can I hook into the Spring data rest request handling to set the lookup-key based on 'x' (user authorizations, path prefix, or other), before any connection is made to the database?
Code-wise my datasource configuration just mostly matches the blogpost at the top, with some basic entity classes, generated repositories and Spring Boot to wrap everything together. If need I could post some code, but there's nothing much to see there.
My first idea is to leverage Spring Security's authentication object to set current datasource based on authorities attached to the authentication.
Of course, you can put the lookup key in a custom UserDetails object or even a custom Authentication object, too. For sake of brevity I`ll concentrate on a solution based on authorities.
This solution requires a valid authentication object (anonymous user can have a valid authentication, too). Depending on your Spring Security configuration changing authority/datasource can be accomplished on a per request or session basis.
My second idea is to work with a javax.servlet.Filter to set lookup key in a thread local variable before Spring Data Rest kicks in. This solution is framework independent and can be used on a per request or session basis.
Datasource routing with Spring Security
Use SecurityContextHolder to access current authentication's authorities. Based on the authorities decide which datasource to use.
Just as your code I'm not setting a defaultTargetDataSource on my AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
Set<String> authorities = getAuthoritiesOfCurrentUser();
if(authorities.contains("ROLE_TENANT1")) {
return "TENANT1";
}
return "TENANT2";
}
private Set<String> getAuthoritiesOfCurrentUser() {
if(SecurityContextHolder.getContext().getAuthentication() == null) {
return Collections.emptySet();
}
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
return AuthorityUtils.authorityListToSet(authorities);
}
}
In your code you must replace the in memory UserDetailsService (inMemoryAuthentication) with a UserDetailsService that serves your need.
It shows you that there are two different users with different roles TENANT1 and TENANT2 used for the datasource routing.
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user1").password("user1").roles("USER", "TENANT1")
.and()
.withUser("user2").password("user2").roles("USER", "TENANT2");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers("/**").hasRole("USER")
.and()
.httpBasic()
.and().csrf().disable();
}
}
Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-spring-security/spring-data
Datasource routing with javax.servlet.Filter
Create a new filter class and add it to your web.xml or register it with the AbstractAnnotationConfigDispatcherServletInitializer, respectively.
public class TenantFilter implements Filter {
private final Pattern pattern = Pattern.compile(";\\s*tenant\\s*=\\s*(\\w+)");
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenant = matchTenantSystemIDToken(httpRequest.getRequestURI());
Tenant.setCurrentTenant(tenant);
try {
chain.doFilter(request, response);
} finally {
Tenant.clearCurrentTenant();
}
}
private String matchTenantSystemIDToken(final String uri) {
final Matcher matcher = pattern.matcher(uri);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
Tenant class is a simple wrapper around a static ThreadLocal.
public class Tenant {
private static final ThreadLocal<String> TENANT = new ThreadLocal<>();
public static void setCurrentTenant(String tenant) { TENANT.set(tenant); }
public static String getCurrentTenant() { return TENANT.get(); }
public static void clearCurrentTenant() { TENANT.remove(); }
}
Just as your code I`m not setting a defaultTargetDataSource on my AbstractRoutingDataSource.
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
#Override
protected Object determineCurrentLookupKey() {
if(Tenant.getCurrentTenant() == null) {
return "TENANT1";
}
return Tenant.getCurrentTenant().toUpperCase();
}
}
Now you can switch datasource with http://localhost:8080/sandbox/myEntities;tenant=tenant1. Beware that tenant has to be set on every request. Alternatively, you can store the tenant in the HttpSession for subsequent requests.
Here is a complete example: https://github.com/ksokol/spring-sandbox/tree/sdr-routing-datasource-url/spring-data
Related
Imagine the following (hypothetical) data structure
endpoint | username | password
users admin 123
info george awd
data magnus e4
this means that every endpoint requires different credentials and no one username/password combo can log in to every endpoint. I am looking for a way to make this scalable in our Spring MVC project when adding more endpoints. We could use roles and hardcore this into the config class but the endpoints and login combinations vary for every customer installation
Given the following SecurityConfiguration with LookupAuthenticationService being the class that looks up the username/password data in the database
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String[] ENDPOINT_LIST = {
"/rest/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(ENDPOINT_LIST)
.authenticated()
.and()
.httpBasic();
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected UserDetailsService userDetailsService() {
return new LookupAuthenticationService(passwordEncoder());
}
}
The ideal situation would be if LookupAuthenticationService has access to the request so we know which endpoint to fetch but I guess this is only possible when working with individual Filters
The possibilities I've found so far are:
Add a WebSecurityConfigurerAdapter and multiple UserDetailsServer specific per endpoint -> lots of code
Add a HandlerInterceptor per endpoint -> lots of code
AuthenticationManagerResolver returning a different AuthenticationManager based on pathInfo?
Any input how to best resolve this issue would be appreciated
You can have a table where you map endpoints to rules, like so:
pattern
authority
/users/**
ROLE_ADMIN
/info/**
ROLE_USER
/another/**
ROLE_ANOTHER
And instead of assigning a user to an endpoint, you assign a role to the users. With this in place, you can create an AuthorizationManager which is going to protect your endpoints based on the request path.
#Component
public class AccessRuleAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final AccessRuleRepository rules;
private RequestMatcherDelegatingAuthorizationManager delegate;
public AccessRuleAuthorizationManager(AccessRuleRepository rules) {
this.rules = rules;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return this.delegate.check(authentication, object.getRequest());
}
#EventListener
void applyRules(ApplicationReadyEvent event) {
Builder builder = builder();
for (AccessRule rule : this.rules.findAll()) {
builder.add(
new AntPathRequestMatcher(rule.getPattern()),
AuthorityAuthorizationManager.hasAuthority(rule.getAuthority())
);
}
this.delegate = builder.build();
}
}
And, in your SecurityConfiguration you simply do this:
#Autowired
private AccessRuleAuthorizationManager access;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) ->
authz.anyRequest().access(this.access)
)
.httpBasic(Customizer.withDefaults());
}
I recommend you to take a look at this repository and watch the presentation from the repository's description. The last steps of the presentation was adding the custom AuthorizationManager, and there's a great explanation about it.
What I have currently
I'm currently implementing an OIDC Resource Provider for my company. They use their intern OIDC servers, which I managed to work with by following this example: https://github.com/jgrandja/oauth2login-demo/tree/linkedin
I'm now able to retrieve user information from the Authorization Server, like that:
#RestController
#RequestMapping("/some/route")
public class SomeController {
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(#RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
String userInfoEndpointUri = authorizedClient.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUri();
Map userAttributes = this.webClient
.get()
.uri(userInfoEndpointUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(Map.class)
.block();
String firstName = (String) userAttributes.get("first_name");
String lastName = (String) userAttributes.get("last_name");
...
}
}
What I'd like
I am now searching for a solution to map the userAttributes to an Object prior to
the controller method call, so that I get e.g.:
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(MyCostumUserBean user) {
String firstName = user.getFirstName();
String lastName = user.getLastName();
...
}
I read something about the ChannelInterceptor and HandlerInterceptor and also the PrincipalExtractor and AuthoritiesExtractor.
The problem is, that I am just learning the Spring framework and these possibilities are overwhelming me.
It would be a plus if that method would allow some validation and would immediately respond with Error codes if the validation fails.
After that is achieved, I would like to add additional information to MyCostumUserBean from another server, which I send the identity of the current session's user to and receive e.g. Role/Permissions of that user.
I tried to put it in a picture:
Question
What is the proper / by the Spring Framework intended way to deal with that? How do I achieve that?
Extra: Is it secure to rely on OAuth2AuthorizedClient.getPrincipalName()? Or can that be faked by an user, by faking the Cookie/Token?
I think you are asking the way to configure the success handler, or a filter which can check the user attributes.
If this is what you are asking, There are many ways to do it.
For examples:
Use User scope check:(need to assign the scope to the user in advance.)
#ResponseBody
#GetMapping("/some/route")
public String getSomeThing(#RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
Set<String> scopes = authorizedClient.getAccessToken()
.getScopes();
if (scopes.contains("users:read")) {
} else if (scopes.contains("users:read")) {
return " page 1";
} else {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Forbidden.");
}
}
You can put some logic in the successHandler:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.permitAll().and()
.formLogin()
.successHandler(successHandler());
}
#Bean
public CustomSuccessHandler successHandler() {
return new CustomSuccessHandler();
}
If you want to apply a filter for your Security Chians:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
.and()
.addFilterBefore(getBeforeAuthenticationFilter(), CustomBeforeAuthenticationFilter.class)
.formLogin()
.loginPage()
.permitAll()
...
}
public UsernamePasswordAuthenticationFilter getBeforeAuthenticationFilter() throws Exception {
CustomBeforeAuthenticationFilter filter = new CustomBeforeAuthenticationFilter();
....
return filter;
}
}
You can also achieve the same purpose by using a Customizing Filter Chains, by give the different order and the relative login in it.
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup1 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup2 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
I have an application that authenticates the login against the Microsoft Active Directory, but now I need that if the user is not in the AD of the organization, try to authenticate against an OpenLDAP directory, is it possible with spring-boot in a single application?
How can I indicate in the configuration class that there are two providers to authenticate? Or, do I have to use a handler or similar to perform double authentication?
My code is similar to the following with its own filters and some changes, but the scheme is similar.
Code source: https://medium.com/#dmarko484/spring-boot-active-directory-authentication-5ea04969f220
#Configuration
#EnableWebSecurity
public class WebSecurityConfigAD extends WebSecurityConfigurerAdapter {
#Value("${ad.domain}")
private String AD_DOMAIN;
#Value("${ad.url}")
private String AD_URL;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
#Override
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception {
authManagerBuilder.authenticationProvider(activeDirectoryLdapAuthenticationProvider()).userDetailsService(userDetailsService());
}
#Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider()));
}
#Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(AD_DOMAIN, AD_URL);
provider.setConvertSubErrorCodesToExceptions(true);
provider.setUseAuthenticationRequestCredentials(true);
return provider;
}
}
I am also doing the same in my application:
Authenticate against the LDAP
Authenticate against the database.
First of all LDAP server is already set up.
So in application.properties file we have all the information required to connect to the LDAP server and a Boolean field to check whether we want to authenticate against LDAP server or not.
project.ldap.server.protocol=
project.ldap.server.host.name=
project.ldap.server.ip=
project.ldap.server.port=
project.ldap.service.url=
project.ldap.authentication=(false/true)
A service which will perform the authentication against the LDAP server
#Service
public class LDAPAuthenticationConnectorService {
#Value("${project.ldap.server.protocol}")
private String LDAP_SERVER_PROTOCOL;
#Value("${project.ldap.server.ip}")
private String LDAP_SERVER_IP;
#Value("${project.ldap.server.port}")
private int LDAP_SERVER_PORT;
#Value("${project.ldap.service.url}")
private String LDAP_SERVICE_URL;
/**
*
* #param loginDto
* #throws ADAuthenticationException
*/
public String authenticate(LoginDto loginDto) throws ADAuthenticationException{//logic }
Now in FacadeImplementation you can do as following:
public class LoginFacadeImpl implements LoginFacadeInt {
#Value("${project.ldap.authentication}")
private Boolean isProjectLDAPAuthenticationEnabled;
#Override
public UserDto authenticateUser(String userName, String password) {
try {
authenticateViaLDAP(userName, password);
} catch (Exception e1) {
//do authetication against database
UserEntity userEntityForAuthentication =
UserManagementDaoInt.authenticateUser(userName,
AuthenticationUtils.convertPasswordToSha256(password));
}
Hope this helps:)
Let me know:)
I found the solution.
Spring Security supports a wide range of authentication mechanisms. AuthenticationManagerBuilder object allows using multiple built-in authentication provider like In-Memory authentication, LDAP authentication, JDBC based authentication. In addition to its own set of authentication models, Spring Security allows to write your custom authentication mechanism to authenticate, for example, against a secure RESTful or SOAP remote API authentication service.
Link: https://www.baeldung.com/spring-security-multiple-auth-providers
The following example shows how to put two authentication providers, one in a row from another. One: in memory, and other: customAuthentcationProvider.
auth.authenticationProvider(customAuthProvider);
auth.inMemoryAuthentication()
Example:
#EnableWebSecurity
public class MultipleAuthProvidersSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomAuthenticationProvider customAuthProvider;
#Override
public void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.authenticationProvider(customAuthProvider);
auth.inMemoryAuthentication()
.withUser("memuser")
.password(encoder().encode("pass"))
.roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/api/**")
.authenticated();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
I am trying to implement SpringSecurity mechanism on this little project, that will limit interactions with the URL of the request by roles.
I have two roles USER and ADMIN, USER can see the items, but not add or delete them, while ADMIN can do both.
Now the problem, the requests from USER role and even unauthenticated users to create/delete/read an item are allowed somehow. It seems to me that my application is not configured correctly somewhere.
SecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}12345").roles("USER").and()
.withUser("admin").password("{noop}12345").roles("ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("api/**").hasRole("ADMIN")
.antMatchers("api/items", "api/items/").hasRole("USER")
.anyRequest().authenticated()
.and().csrf().disable().headers().frameOptions().disable();
}
}
Controller:
#RestController
public class ItemController {
#Autowired
private ItemService itemService;
#GetMapping("/api/items")
public List<Item> getItems() {
return itemService.getAllItems();
}
#PostMapping(value = "/api/addItem",consumes = {"application/json"},produces = {"application/json"})
#ResponseBody
public Item addItem(#RequestBody Item item) {
itemService.addItem(item);
return item;
}
#DeleteMapping(value = "api/deleteItem/{id}")
#ResponseBody
public String deleteItem(#PathVariable int id) {
itemService.deleteItem(id);
return "Deleted";
}
}
I am sending requests to the following URL's:
http://localhost:8080/api/items // GET
http://localhost:8080/api/addItem // POST
http://localhost:8080/api/deleteItem/4 // DELETE
Have you tried adding slashes to your antmatcher patterns, such as:
antMatchers("/api/**").hasRole("ADMIN")
The Spring documentation mentions:
Note: a pattern and a path must both be absolute or must both be relative in order for the two to match. Therefore it is recommended that users of this implementation to sanitize patterns in order to prefix them with "/" as it makes sense in the context in which they're used.
Furthermore, Spring security uses the first match of all the matching rules expressed. I would recommend reordering the matchers from most-specific to less-specific as otherwise a call to api/items will be matched by the api/** matcher instead of being matched by the api/items matcher.
.antMatchers("/api/items", "api/items/").hasRole("USER")
.antMatchers("/api/**").hasRole("ADMIN")
On top of #GetMapping or #PostMapping you can add following annotation to manage Role based access
#PreAuthorize("hasAnyRole('ROLE_ADMIN')")
#PostMapping(value = "/api/addItem",consumes = {"application/json"},produces = {"application/json"})
#ResponseBody
public Item addItem(#RequestBody Item item) {
itemService.addItem(item);
return item;
}
Service works after gateway in trusted space (gateWay verifies OAuth token and gives to the service only unique user ID other case it redirects to authenticate service).
I want use spring security in the service to be able validate permissions for userId.
So I've added CustomUserDetailsService
#Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
#Autowired(required = false)
private ContextSsoActiveProfileIdProvider contextSsoActiveProfileIdProvider;
#Autowired
private GrantedAuthorityService grantedAuthorityService;
#Override
public User loadUserByUsername(final String username) throws UsernameNotFoundException {
// verify it with authentication service, but there is not token, userId only, so trust to gateway service.
return new User(
String.valueOf(contextSsoActiveProfileIdProvider.getSsoActiveProfileId()),
"authenticatedWithGateWay",
grantedAuthorityService.getGrantedAuthoritiesForCurrentUser()
);
}
}
Where contextSsoActiveProfileIdProvider.getSsoActiveProfileId() returns uniqueUserId and grantedAuthorityService.getGrantedAuthoritiesForCurrentUser() returns authorities.
The service starts in trusted zone so I have configured security in next way:
#EnableWebSecurity
#Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll();
}
#Override
protected UserDetailsService userDetailsService() {
return userDetailsService;
}
}
I need provide free access for all users (without triggering login offer) for all URIs (http.authorizeRequests().antMatchers("/**").permitAll();) but it seems suppressed triggering handlers for next annotations #PreAuthorize, #PreFilter, #PostAuthorize and #PostFilter.
I suppose I mistook here with http.authorizeRequests().antMatchers("/**").permitAll(); or with other configuration part.
More issue symptoms:
CustomUserDetailsService.loadUserByUsername(..) is never called;
On REST API part #AuthenticationPrincipal User activeUser is null
On REST API part Principal principal is null also
Trusted space issue has similar solution to anonymous user identification (I've done this conclusion when I was working on it.)
Short answer
Trusted space does not need authorization, but no UserDetailsService will be called, because of using only AnonymousAuthenticationProvider and AnonymousAuthenticationFilter by default. It is good enough implement custom filter based on AnonymousAuthenticationFilter overriding createAuthentication and replace default (AnonymousAuthenticationFilter) with custom one (CustomAnonymousAuthenticationFilter):
#Configuration
public static class NoAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private IdentifiableAnonymousAuthenticationFilter identifiableAnonymousAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.anonymous().authenticationFilter(identifiableAnonymousAuthenticationFilter);
http.antMatcher("/**").authorizeRequests()
.anyRequest().permitAll();
}
}
Full answer
I found out that CustomUserDetailsService will never be called if user is not authorized. Continuing research pay attention on the AnonymousAuthenticationFilter which is responsible for creating anonymous user info. So in the very and purpose is to replace the AnonymousAuthenticationFilter with my IdentifiableAnonymousAuthenticationFilter where some methods should be overridden:
#Component
public class IdentifiableAnonymousAuthenticationFilter extends AnonymousAuthenticationFilter {
public static final String KEY_IDENTIFIABLE_ANONYMOUS_AUTHENTICATION_FILTER
= "Key.IdentifiableAnonymousAuthenticationFilter";
#Autowired
private CustomUserDetailsService userDetailsService;
#Autowired
private GrantedAuthorityService grantedAuthorityService;
private AuthenticationDetailsSource authenticationDetailsSource
= new WebAuthenticationDetailsSource();
public IdentifiableAnonymousAuthenticationFilter() {
this(KEY_IDENTIFIABLE_ANONYMOUS_AUTHENTICATION_FILTER);
}
public IdentifiableAnonymousAuthenticationFilter(String key) {
super(key);
}
#Override
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(
KEY_IDENTIFIABLE_ANONYMOUS_AUTHENTICATION_FILTER,
userDetailsService.loadCurrentUser(request),
grantedAuthorityService.getGrantedAuthoritiesForCurrentUser());
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
}
to inject it into configuration
#Configuration
public class IdentifyAnonymousConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private IdentifiableAnonymousAuthenticationFilter identifiableAnonymousAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.anonymous().authenticationFilter(identifiableAnonymousAuthenticationFilter);
// ... some other configurations
}
}
Now it seems much better, because identifiableAnonymousAuthenticationFilter is injected in AnonymousConfigurer. Pay your attention to your configurations based on WebSecurityConfigurerAdapter. If you have few ones and one of them will not set customAnonymousAuthenticationFilter but configured earlier than custom.. you'll get default instance of AnonymousAuthenticationFilter (configured in WebSecurityConfigurerAdapter by default):
protected final HttpSecurity getHttp() throws Exception {
//...
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
// ...
I would not care about it if application fixed, but AnonymousAuthenticationFilter called earlier than IdentifiableAnonymousAuthenticationFilter. And doFilter puts into SecurityContextHolder incorrect Authentication.
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if(SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
if(this.logger.isDebugEnabled()) {
this.logger.debug("Populated SecurityContextHolder with anonymous token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
} else if(this.logger.isDebugEnabled()) {
this.logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(req, res);
}
So when next time doFilter is called for IdentifiableAnonymousAuthenticationFilter it does not replace the Authentication because of condition if(SecurityContextHolder.getContext().getAuthentication() == null) (see method before).
As result would be really good to provide configuration where fix for WebSecurityConfigurerAdapter configuration using magic annotation #Order to manage configuration loading order.
Warning
Or someone could think - add doFilter overriding in IdentifiableAnonymousAuthenticationFilter without condition (it is hack):
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));
if (logger.isDebugEnabled()) {
logger.debug("Populated SecurityContextHolder with anonymous token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}
chain.doFilter(req, res);
}
It is not acceptable if you need spring security with handling authorized/authenticated user but in some cases it is enough.
P.S.
Some parts of the solution could be improved but I hope that idea is clear in general.