2

When the web session expires, Spring Security responds with a 403 HTTP status. Ideally, it would respond with a 401. Unauthorized and Forbidden are different. The request to a secured resource should only return a 403 if there is a valid session, but the user just doesn't have permissions to said resource. If a resource is secured and there is no authenticated session, then Spring Security should return a 401.

My application needs to be very specific about distinguishing between these 2 error codes.

My question is, how can I customize this behavior? For reference to my argument on the differences between 401 and 403, read this.

Gregg
  • 34,973
  • 19
  • 109
  • 214
  • Looking at the source code it doesn't appear to be customizable: https://github.com/grails-plugins/grails-spring-security-core/blob/144b235e1c45e3de7178089e7be0b478d0ce512c/src/java/grails/plugin/springsecurity/web/access/AjaxAwareAccessDeniedHandler.java – Joshua Moore Jun 02 '15 at 21:24
  • Well, that's disappointing. – Gregg Jun 02 '15 at 21:38
  • nothing is stopping you from replacing the default implementation with your own which implements this feature. Might make for a good pull request too (: – Joshua Moore Jun 02 '15 at 22:08

3 Answers3

0

Here is my solution for this:

@Configuration
public class WebCtxConfig  implements BeanPostProcessor {

        @Override
        public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof SessionManagementFilter) {
                SessionManagementFilter filter = (SessionManagementFilter) bean;
                filter.setInvalidSessionStrategy(new InvalidSessionStrategy() {

                    @Override
                    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
                        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    }
                });
            }
            return bean;
        }

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            return bean;
        }
    }
Modi
  • 2,200
  • 4
  • 23
  • 37
  • Interesting. Thanks for the response. Is there anything that I need to do in the resources.groovy to make this work or does the `@Configuration` and `BeanPostProcessor` subclass do it for me? – Gregg Jun 04 '15 at 14:29
  • This isn't working. Spring Security still returns with a 403. – Gregg Jun 05 '15 at 18:00
  • Are you sure that the configuration class is included in your context configuration? BTW, I'm working with Java, so I don't know about Groovy configuration. In Java, you just have to include it in your context configuration. – Modi Jun 06 '15 at 18:47
0

In Spring Boot 2.2.1 I have done it using class derived from AuthenticationEntryPoint:

import java.io.IOException;
import javassist.NotFoundException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;


@ControllerAdvice
public class AppAuthenticationEntryPoint implements AuthenticationEntryPoint{

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException auth) throws IOException, ServletException {
        // 401
        setResponseError(response, HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
    }

    @ExceptionHandler (value = {AccessDeniedException.class})
    public void commence(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        // 403
        setResponseError(response, HttpServletResponse.SC_FORBIDDEN, String.format("Access Denies: %s", accessDeniedException.getMessage()));
    }

    @ExceptionHandler (value = {NotFoundException.class})
    public void commence(HttpServletRequest request, HttpServletResponse response, NotFoundException notFoundException) throws IOException {
        // 404
        setResponseError(response, HttpServletResponse.SC_NOT_FOUND, String.format("Not found: %s", notFoundException.getMessage()));
    }

    @ExceptionHandler (value = {Exception.class})
    public void commence(HttpServletRequest request, HttpServletResponse response, Exception exception) throws IOException {
        // 500
        setResponseError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, String.format("Internal Server Error: %s", exception.getMessage()));
    }

    private void setResponseError(HttpServletResponse response, int errorCode, String errorMessage) throws IOException{
        response.setStatus(errorCode);
        response.getWriter().write(errorMessage);
        response.getWriter().flush();
        response.getWriter().close();
    }
}

And in You security config (I have ResourceServer that is accessible by OAuth2.0 token)

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.headers().frameOptions().sameOrigin();  // it is to fix issue with h2-console access

        http.csrf().disable()
            .authorizeRequests().antMatchers("/", "/callback", "/login**", "/webjars/**", "/error**").permitAll()
            .and()
            .authorizeRequests().antMatchers("/api/**").authenticated()
            .and()
            .authorizeRequests().antMatchers("/h2-console/**").permitAll()
            .and()
            .authorizeRequests().antMatchers("/swagger-ui.html").permitAll()
            .and()
            .authorizeRequests().antMatchers("/swagger-ui/**").permitAll()
            .and()
            .exceptionHandling().authenticationEntryPoint(new AppAuthenticationEntryPoint())
            .and()
            .logout().permitAll().logoutSuccessUrl("/");
    }

    @Bean
    public PrincipalExtractor getPrincipalExtractor(){
        return new KeyCloakUserInfoExtractorService();
    }

    @Autowired
    private ResourceServerTokenServices resourceServerTokenServices;
}
Michael Ushakov
  • 1,639
  • 1
  • 10
  • 18
0

I was going through the same issue, and discovered a solution for me. In my custom filter extending OncePerRequestFilter, I was originally using the following code:

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
                                @NonNull HttpServletResponse response,
                                @NonNull FilterChain filterChain) throws ServletException, IOException {
    // logic to check session expiration
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "message");
}

which would then get filtered out, and instead the server would send a 403. However, once I switched my code to the following, I was getting the correct 401 error. No idea why this is the case.

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("message");

Hope that helps!

tgoel-dev
  • 3
  • 3