I'm trying to setup a basic login endpoint for a RESTful API. I just want to be able to send via POST the user credentials, validate them and setup a cookie, nothing fancy with the session yet. No matter what I do, I get 403 even if my Authentication Filter seems to be capturing the Loging Request successfully and passing it to my AuthenticationManager, which correctly sets the authentication to authenticated.
Here's my SecurityConfig:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// TODO: Enable CORS and CSRF protection
return http
.csrf().disable()
.formLogin().disable()
.addFilter(new JsonAuthenticationFilter(authenticationManager))
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
)
.build();
}
My AuthenticationManager:
@Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService){
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(authProvider);
}
And my JsonAuthenticationFilter:
@Component
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JsonAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("Attempting to authenticate");
if (!"POST".equals(request.getMethod())) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
ObjectMapper objectMapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest;
try {
BufferedReader reader = request.getReader();
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
String jsonBody = stringBuilder.toString();
LoginRequest loginRequest = objectMapper.readValue(jsonBody, LoginRequest.class);
authRequest = new UsernamePasswordAuthenticationToken(loginRequest.username(), loginRequest.password());
} catch (IOException e) {
throw new AuthenticationServiceException("Failed to parse authentication request body", e);
}
System.out.println(authRequest);
setDetails(request, authRequest);
Authentication authenticate = this.getAuthenticationManager().authenticate(authRequest);
System.out.println(authenticate);
return authenticate;
}
}
If I try a POST to localhost/login with Postman, I get the following output:
Attempting to authenticate
UsernamePasswordAuthenticationToken [Principal=admin, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]]
Loading user by username: admin
Found: edata.test.my_overengineered_app.authentication.credentials.Credentials@629ebc06
Found Bindings: [LIST_USER, CREATE_USER, MANAGE_USER_ACCESS]
UsernamePasswordAuthenticationToken [Principal=edata.test.my_overengineered_app.authentication.Identity@692c1bad, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=A52D919E3DFCE2B45843488B2B1C3AB7], Granted Authorities=[LIST_USER, MANAGE_USER_ACCESS, CREATE_USER]]
Based on the output, my Json filter is triggering correctly, my UserDetailsService as well, and the AuthenticationManager successfully identifies the user. But somehow I still get a 403 response, and trying other protected routes with the cookie I received results in a 403 as well, as if the authentication didn't take place. I have read multiple posts, the Spring Security reference for both Authentication and Authorization and have no clue what I'm doing wrong. Any help is appreciated :(
From Spring Security 6 on the SecurityContextRepository
must be set explicitly, as described in the Spring Security Migration Guide. You should try the following:
@Component
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@PostConstruct
private void setup() {
super.setSecurityContextRepository(
new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()));
}
public JsonAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
....
}
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// TODO: Enable CORS and CSRF protection
return http
.csrf().disable()
.formLogin().disable()
.addFilter(new JsonAuthenticationFilter(authenticationManager))
.authorizeHttpRequests((authorize) -> authorize
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
)
.securityContext(securityContext -> securityContext
.securityContextRepository(
new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
)))
.build();
}
Also I'd recommend you to read this question here: How to properly set SecurityContextRepository for a custom UsernamePasswordAuthenticationFilter in Spring Security 6?