Using SecurityFilterChain and MethodSecurity (@Protected, @PreAuthorize, etc.) I am able to secure my endpoints no problem. The issue is, I'd like to get individualized messages for when a user is unauthenticated (not logged in) and a separate message for when the user is unauthorized (logged in, but does not have the required role or permission to access the endpoint).
Here's my AuthenticationEntryPoint class:
@Component
public class AuthEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPoint.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
final Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_FORBIDDEN);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getRequestURI());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}
Here's my SecurityFilterChain Bean in SecurityConfig:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/","*","/register","/login").permitAll()
.requestMatchers("/test2").hasAnyAuthority("ROLE_USER")
.anyRequest().authenticated()
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.httpBasic().and()
.authenticationProvider(authenticationProvider())
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
The main issue is that the error is the same for both Unauthenticated and Unauthorized scenarios. I've tried printing out some info about the error with this:
System.out.println("Error message: " +authException.getMessage());
System.out.println("Error class: " +authException.getClass());
System.out.println("Error cause: " +authException.getCause());
authException.printStackTrace();
getMessage
returns the same message for both scenarios. ("Full authentication is required to access this resource")
getClass
returns the same exception class for both scenarios. (org.springframework.security.authentication.InsufficientAuthenticationException)
getCause
returns null
for both scenarios.
authException.printStackTrace()
returns slightly different stacktraces.
I've also tried creating a Handler class:
@Component
public class AuthEntryPointExceptionHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException exception) throws IOException {
// 401 HTTP Response
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "You need to be logged in to do that.");
}
@ExceptionHandler (value = {AccessDeniedException.class})
public void commence(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException {
// 403 HTTP Response
response.sendError(HttpServletResponse.SC_FORBIDDEN, "You don't have permission to do that: "+exception.getMessage());
}
@ExceptionHandler (value = {Exception.class})
public void commence(HttpServletRequest request, HttpServletResponse response, Exception exception) throws IOException {
// 500 HTTP Response
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error: " + exception.getMessage());
}
}
but I still got the same error, handled by the original AuthEntryPoint class. Also for this class I tried all of these annotations: @Component, @Controller, @ControllerAdvice and I tried implementing the original AuthEntryPoint class instead of Spring's included AuthenticationEntryPoint.
I know I can check authentication status inside the first handler and manipulate the response from that, but I believe Spring should return different Exceptions for Unauthenticated and Unauthorized scenarios by default. Correct me if I'm wrong.
P.S. Authentication process works as intended. I get the JWT token in return and use it to Authorize/Authenticate myself for other requests.
I've tried looking at other people's questions on similar topics, tried watching youtube tutorials, etc., the issue still persists, and I believe the underlying cause is that spring returns the same exception for both scenarios.
UPDATE
When I update my SecurityFilterChain to .requestMatchers("/test2").authenticated()
, I can extract the Authentication inside the /test2
endpoint's method via SecurityContextHolder.getContext().getAuthentication()
, but when I secure the endpoint with a role .requestMatchers("/test2").hasAnyAuthority("ROLE_USER")
and put the same SecurityContextHolder line inside AuthEntryPoint's commence method, I get an exception the return value of "org.springframework.security.core.context.SecurityContext.getAuthentication()" is null
. Same endpoint, same valid JWT token. How come the authentication is null, if it gets set in AuthTokenFilter class, which initializes earlier than AuthEntryPoint does? I verified the order. What am I missing?
UPDATE2:
Principal principal = request.getUserPrincipal();
System.out.println(principal.getName());
This does the same thing, the same way.
UPDATE3:
I've found the source of the problem. Kinda.
When I change SecurityFilterChain to permit any request (http.authorizeHttpRequests().anyRequest().permitAll()
) and only use Method Security (@Secured
& @PreAuthorize
), I get the desired separation between Forbidden and Unauthorized (unauthenticated) responses. If only I change SecurityFilterChain to ...anyRequest().authenticated()
, all unaccessible scenarios just return the initial Unauthorized response. (Can't replicate the Forbidden response when SecurityFilterChain has ...authenticated()
).
Basically, maybe this somewhat narrows down what might be happening. And while knowing this lets me make a usable app with correct separation, I'd still like to make ...authenticated()
behave correctly.
In other words, SecurityFilterChain does not go through the same exceptions and their handlers as Method Security does, as far as I understand.