AbstractAuthenticationProcessingFilter chain in successfulAuthentication - spring

Hello I have filter to get Autorization from JWT
public class JwtAuthorizationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthorizationFilter() {
super("/**");
}
#Override
public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
super.setAuthenticationSuccessHandler(successHandler);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String token = request.getHeader("Authorization");
//code
return getAuthenticationManager().authenticate(getAuthentication(token));
}
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
// code
}
Problem is that when I reach successfulAuthentication and doing chain.doFilter I getting exception
"servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: Cannot call sendError() after the response has been committed] with root cause"
And I can't get my endpoint. I also notice that authResult generate many (Objects?) with same data even if I login only once
#Edit I noticed now that, after successful authorize, spring trying to reach my controller few time. First return value but other just throw eceptions, and I don't know why I have this loop

What is done inside super.successfulAuthentication? If you do any modification to HttpServletResponse object, e.g., by changing the Http Status Codes or ResponseEntity you won't be able to proceed to next filter in the chain by invoking chain.doFilter(request, response) anymore since the response is committed and returned to the client.

Related

Onboarding filter in Spring Boot and Spring Security

I'm trying to create a filter in order to redirect logged users to the onboarding page in case they haven't completed the process before.
This is my filter so far:
#Component
#Order(110)
public class OnboardingFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// cast the request and response to HTTP
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpSession session = httpRequest.getSession(false);
SecurityContextImpl securityContext = (SecurityContextImpl) session.getAttribute("SPRING_SECURITY_CONTEXT");
// if there's a logged user
if (securityContext != null) {
UserPrincipal principal = (UserPrincipal) securityContext.getAuthentication().getPrincipal();
if (!principal.hasOnboarded()) {
httpResponse.sendRedirect("/onboarding");
}
}
// continue with the filter chain
chain.doFilter(httpRequest, httpResponse);
}
I've tried different values for #Order but in every case the http response generates a downloadable content instead of showing the actual requested URL or sending the redirect. Any ideas?
I'm getting this exception:
Caused by: java.lang.IllegalStateException: Committed
at org.eclipse.jetty.server.HttpChannel.resetBuffer(HttpChannel.java:917)
HttpChannel.java:917
at org.eclipse.jetty.server.HttpOutput.resetBuffer(HttpOutput.java:1423)
HttpOutput.java:1423
at org.eclipse.jetty.server.Response.resetBuffer(Response.java:1182)
Response.java:1182
at org.eclipse.jetty.server.Response.sendRedirect(Response.java:534)
Response.java:534
at org.eclipse.jetty.server.Response.sendRedirect(Response.java:543)
Response.java:543
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:130)
HttpServletResponseWrapper.java:130
at org.springframework.security.web.firewall.FirewalledResponse.sendRedirect(FirewalledResponse.java:43)
FirewalledResponse.java:43
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:130)
HttpServletResponseWrapper.java:130
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:135)
OnCommittedResponseWrapper.java:135
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:130)
HttpServletResponseWrapper.java:130
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendRedirect(OnCommittedResponseWrapper.java:135)
OnCommittedResponseWrapper.java:135
at javax.servlet.http.HttpServletResponseWrapper.sendRedirect(HttpServletResponseWrapper.java:130)
HttpServletResponseWrapper.java:130
at org.springframework.web.servlet.view.RedirectView.sendRedirect(RedirectView.java:627)
RedirectView.java:627
at org.springframework.web.servlet.view.RedirectView.renderMergedOutputModel(RedirectView.java:314)
RedirectView.java:314
at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:316)
AbstractView.java:316
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1373)
DispatcherServlet.java:1373
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1118)
DispatcherServlet.java:1118
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)
DispatcherServlet.java:1057
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
DispatcherServlet.java:943
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
FrameworkServlet.java:1006
... 94 common frames omitted
What about adding a return in:
httpResponse.sendRedirect("/onboarding");
return;
Otherwise you are continuing with the filter chain:
chain.doFilter(httpRequest, httpResponse);
Not 100% sure if this is already solving your issue, but might be also a problem.
I finally solved this by implementing an Spring context-aware GenericFilterBean as follows:
public class OnboardingFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if (SecurityContextHolder.getContext().getAuthentication() != null) {
// some business logic
httpResponse.sendRedirect("/onboarding");
}
else {
chain.doFilter(httpRequest, httpResponse);
}
}
}
Don't forget to add it to the security filter chain:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(new OnboardingFilter(), UsernamePasswordAuthenticationFilter.class)...

spring-boot Error: Exceeded maxRedirects. Probably stuck in a redirect loop

I am trying to perform JWT auth in spring boot and the request are getting stuck in redirect loop.
JWTAuthenticationProvider
#Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
private JwtUtil jwtUtil;
#Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
}
#Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
String token = jwtAuthenticationToken.getToken();
JwtParsedUser parsedUser = jwtUtil.parseToken(token);
if (parsedUser == null) {
throw new JwtException("JWT token is not valid");
}
UserDetails user = User.withUsername(parsedUser.getUserName()).password("temp_password").authorities(parsedUser.getRole()).build();
return user;
}
JwtAuthenticationFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/**");
this.setAuthenticationManager(authenticationManager);
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer ")) {
throw new JwtException("No JWT token found in request headers");
}
String authToken = header.substring(7);
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(authRequest);
}
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
SecurityConfiguration
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationProvider jwtAuthenticationProvider;
#Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(jwtAuthenticationProvider);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().antMatchers("/secured-resource-1/**", "/secured-resource-2/**")
.hasRole("ADMIN").antMatchers("/secured-resource-2/**").hasRole("ADMIN").and().formLogin()
.successHandler(new AuthenticationSuccessHandler()).and().httpBasic().and().exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler()).authenticationEntryPoint(getBasicAuthEntryPoint())
.and()
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),
FilterSecurityInterceptor.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Bean
public CustomBasicAuthenticationEntryPoint getBasicAuthEntryPoint() {
return new CustomBasicAuthenticationEntryPoint();
}
}
MainController
#RestController
public class MainController {
#Autowired
private JwtUtil jwtUtil;
#GetMapping("/secured-resource-1")
public String securedResource1() {
return "Secured resource1";
}
}
When I hit the endpoint with the valid JWT token, the code goes in a loop from Filter to provider class and ends in Error:
Exceeded maxRedirects. Probably stuck in a redirect loop http://localhost:8000/ error.
Debug logs shows the following error:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: Cannot call sendError() after the response has been committed] with root cause
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
Any suggestions what am I missing here.
Thanks in advance.
I believe the the reason for this is because you have not actually set the AuthenticationSuccessHandler for the bean JwtAuthenticationFilter, since it is not actually set it will keep looping around super and chain and later when the error needs to be sent since response is already written in super() chain.doFilter will fail because once the response is written it cannot be again written hence the error call sendError() after the response has been committed.
To correct this in your SecurityConfiguration before setting this
.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()),
FilterSecurityInterceptor.class)
Instantiate the filter and set it's success manager like so
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager()),FilterSecurityInterceptor.class);
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
Now use the above variable to set the filter.
This is a great reference project: https://gitlab.com/palmapps/jwt-spring-security-demo/-/tree/master/.
I solved this problem with another approach.
In the JwtAuthenticationFilter class we need to set authentication object in context and call chain.doFilter. Calling super.successfulAuthentication can be skipped as we have overridden the implementation.
#Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//super.successfulAuthentication(request, response, chain, authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super("/**");
this.setAuthenticationManager(authenticationManager);
//this.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler());
}

JwtAuthorizationFilter implementtaion not working

In summary JwtAuthorizationFilter is not working.
If I leave //filterChain.doFilter(request, response) commented out I correctly get a 200 but the body is empty, which means two things:
1) The Controller/Response from the controller is executed but not the logic on it.
2) The function getAuthentication() correctly reads the Claims/token
The issue happens if I uncomment the line //filterChain.doFilter(request, response) because I get a 405. That line should be uncommented for the filter chain to exeute completely and get a response body with content.
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws IOException, ServletException {
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if (authentication == null) {
filterChain.doFilter(request, response);
return;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
**//filterChain.doFilter(request, response);**
}
Function in the controller:
#GetMapping("/foo")
#ResponseStatus(code = HttpStatus.OK)
public MyObject retrieve(#RequestBody MyModel obj) {
//code here is never called
}
For reference, this is my security config:
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/bar").permitAll()
.anyRequest().authenticated().and().addFilter(new AnotherFilter(authenticationManager()))
.addFilter(new JwtAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
What Am I doing wrong?
Just to say that I clean up the build, restart the IDE and worked.
Maybe there was some cache not allowing the JWT filter to work properly.
Thanks,

AuthenticationException thrown in AuthenticationProvider is mapped to AccessDeniedException in ExceptionTranslationFilter

Having written a custom AuthenticationProvider (which calls a service, after which that one calls an external URL to authenticate the given credentials), I wanted to customize the error message people get when their authentication fails, based on the message I pass to the instance of AuthenticationException (so the e.getMessage() passed to BadCredentialsExceptoin in the code below).
#Component
public class TapasAuthenticationProvider implements AuthenticationProvider {
#Override
public TapasAuthentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String password = authentication.getCredentials().toString();
try {
AuthenticationResponse authResponse = authenticationService.authenticate(userName, password);
return new TapasAuthentication(mapToAuthenticatedUser(authResponse, userName, password), password);
} catch (AuthenticationFailedException e) {
// Note that AuthenticationFailedException is a self-written exception, thrown by the authenticationService.
log.error("Authentication failed: ", e);
throw new BadCredentialsException(e.getMessage());
}
}
}
Now I looked up how to map AuthenticationExceptions and found that an AuthenticationEntryPoint should be used for this. So I created one and added it to my SecuritySetup:
#Component
public class TapasAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
System.out.println(authException.getMessage());
// More code to be added once exception is what I expect.
}
}
#Autowired
private TapasAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.authorizeRequests().anyRequest().authenticated().and().httpBasic()
.and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf().disable();
}
This successfully triggers the AuthenticationEntryPoint, but instead of a BadCredentialsException, I get an InsufficientAuthenticationException. I checked the origin of this exception and it comes from the ExceptionTranslationFilter's handleSpringSecurityException. Here the exception turns out to be an AccessDeniedException instead of an AuthenticationException.
private void handleSpringSecurityException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, RuntimeException exception)
throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
// I would except to enter this if-statement, but exception is not what I expect
sendStartAuthentication(request, response, chain,
(AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
....
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
// Instead code comes here, and creates an InsufficientAuthenticationException.
sendStartAuthentication(
request,
response,
chain,
new InsufficientAuthenticationException(
messages.getMessage(
"ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
...
}
}
Why does the exception not match my exception thrown in the AuthenticationProvider? And how would I be able to pass data from the AuthenticationProvider back to the user?
Turns out the HttpBasicConfigurer returned by the .basicSecurity() call also allows to register an AuthenticationEntryPoint. When registering it that way, the exception thrown by the Provider does end up in the entry point.
Security config looks like this:
#Autowired
private TapasAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.httpBasic().authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf().disable();
}

Filter for Url Pattern without authentification?

Help me please a little with setting up Spring Security.
I found something similar, but it somehow does not work very well for me ..
https://stackoverflow.com/a/36875726/1590594
The configuration specifies that each request must be authenticated.
It is necessary to do the following, that on the specified URL ("/ push") worked only one filter. The filter does the appropriate checking and skips the request further or rejecting. Without authentication.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().
authorizeRequests()
.anyRequest().authenticated().
and().
anonymous().disable().
exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint());
http.addFilterBefore(new UserAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class);
http.authorizeRequests().antMatchers(HttpMethod.POST, "/push").authenticated().and().addFilterBefore(new RPushFilter(),BasicAuthenticationFilter.class);
}
and filter
public class RPushFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
//IF NOT httpResponse.sendError(HttpStatus.BAD_REQUEST.value(), "Access denied");
chain.doFilter(request, response);
}
}

Resources