1

I'm a junior web developper and I need to customize the 403 error page using react js, I've found other projects implementing the AccessDeniedHandler interface but I don't know how to use it in my Security Configuration class.

This is my CustomAccessDeniedHandler class :

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    private static Logger logger = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException e) throws IOException, ServletException {
        System.out.println("accessDenied");
        Authentication auth
                = SecurityContextHolder.getContext().getAuthentication();

        if (auth != null) {
            logger.info("User '" + auth.getName()
                    + "' attempted to access the protected URL: "
                    + httpServletRequest.getRequestURI());
        }

        httpServletResponse.sendRedirect(httpServletRequest.getContextPath() + "/accessDenied");
    }
}

This is the Security Configuration class :

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AppUserService userDetailsService;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .exceptionHandling()
                .authenticationEntryPoint(new Http403ForbiddenEntryPoint() {})
                .and()
                .authenticationProvider(getProvider())
                .formLogin()
                .loginPage("/login")
                .successHandler(new AuthentificationLoginSuccessHandler())
                .failureHandler(new SimpleUrlAuthenticationFailureHandler())
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler(new AuthentificationLogoutSuccessHandler())
                .invalidateHttpSession(true)
                .and()
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/logout").permitAll()
                .antMatchers("/api/categories").hasAuthority("USER")
                .antMatchers("/api/createCategory").hasAuthority("ADMIN")
                .anyRequest().permitAll();
    }

    private class AuthentificationLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
                throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_OK);
        }
    }

    private class AuthentificationLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                    Authentication authentication) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_OK);
        }
    }

    @Bean
    public AuthenticationProvider getProvider() {
        AppAuthProvider provider = new AppAuthProvider();
        provider.setUserDetailsService(userDetailsService);
        return provider;
    }

    @Bean
    public AccessDeniedHandler accessDeniedHandler(){
        return new CustomAccessDeniedHandler();
    }
}
Mahdi Ezzahir
  • 93
  • 2
  • 9

1 Answers1

1

What you have to do is to create @Bean

@Bean
public AccessDeniedHandler accessDeniedHandler(){
    return new CustomAccessDeniedHandler();
}

which you already have and then add that handler in http security object like this:

http.csrf()
            .disable()
            .exceptionHandling()
            .authenticationEntryPoint(new Http403ForbiddenEntryPoint() {}) //remove this line or use Http401UnauthorizedEntryPoint instead
            .and()
            .authenticationProvider(getProvider())
            .formLogin()
            .loginPage("/login")
            .successHandler(new AuthentificationLoginSuccessHandler())
            .failureHandler(new SimpleUrlAuthenticationFailureHandler())
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new AuthentificationLogoutSuccessHandler())
            .invalidateHttpSession(true)
            .and()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/logout").permitAll()
            .antMatchers("/api/categories").hasAuthority("USER")
            .antMatchers("/api/createCategory").hasAuthority("ADMIN")
            .anyRequest().permitAll()
            .and()
            .exceptionHandling().accessDeniedHandler(accessDeniedHandler());

As you can see you are missing:

.and().exceptionHandling().accessDeniedHandler(accessDeniedHandler());

Additional, remove

@Autowired
private AccessDeniedHandler accessDeniedHandler;

because you shouldn't autowire bean, you should create it with custom implementation.

EDIT: If you have @RestControllerAdvice or @ControllerAdvice as global exception handler you should do following:

@ExceptionHandler(Exception.class)
public ResponseEntity<?> exception(Exception exception) throws Exception {
        if (exception instanceof AccessDeniedException) {
            throw exception;
        } 
...

then it should work because when you throw exception it will go to custom handler which we do what you wrote. Also you can debug ExceptionTranslationFilter method handleSpringSecurityException

code from ExceptionTranslationFilter

private void handleSpringSecurityException(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, RuntimeException exception)
            throws IOException, ServletException {
        if (exception instanceof AuthenticationException) {
            logger.debug(
                    "Authentication exception occurred; redirecting to authentication entry point",
                    exception);

            sendStartAuthentication(request, response, chain,
                    (AuthenticationException) exception);
        }
        else if (exception instanceof AccessDeniedException) {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
                logger.debug(
                        "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
                        exception);

                sendStartAuthentication(
                        request,
                        response,
                        chain,
                        new InsufficientAuthenticationException(
                            messages.getMessage(
                                "ExceptionTranslationFilter.insufficientAuthentication",
                                "Full authentication is required to access this resource")));
            }
            else {
                logger.debug(
                        "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
                        exception);

                accessDeniedHandler.handle(request, response,
                        (AccessDeniedException) exception);
            }
        }
    }

where you can see that accessDeniedHandler.handle(request, response,(AccessDeniedException) exception);, in you caseCustomAccessDeniedHandler, get called.

I just tried it and it is working fine with (I have @ControllerAdvice as global exception handler)

EDIT2: You have to remove this line

.authenticationEntryPoint(new Http403ForbiddenEntryPoint() {})

from SecurityConfig, or change it to use Http401UnauthorizedEntryPoint instead. This is the problem in your case.

Lemmy
  • 2,437
  • 1
  • 22
  • 30
  • well, what you said is totally logic, but I tried exactly that and it didn't work. – Mahdi Ezzahir Apr 15 '20 at 11:59
  • Ok, then you must have some '@ControllerAdvice' or '@RestControllerAdvice' in your code? I edited my answer – Lemmy Apr 15 '20 at 12:37
  • thank you for tha @Lemmy but where should I add those two methods ? – Mahdi Ezzahir Apr 15 '20 at 13:38
  • You have to remove this line .authenticationEntryPoint(new Http403ForbiddenEntryPoint() {}) – Lemmy Apr 15 '20 at 13:39
  • @MahdiEzzahir i missed that part – Lemmy Apr 15 '20 at 13:39
  • I tried but I found an other error : 'Redirect has been blocked by CORS policy: No 'Access-Control-Allow-Origin' – Mahdi Ezzahir Apr 15 '20 at 14:40
  • you have to enable CORS like this '@Configuration' '@EnableWebMvc' public class WebConfig implements WebMvcConfigurer { '@Override' public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**"); } } See this tutorial https://www.baeldung.com/spring-cors – Lemmy Apr 15 '20 at 14:45
  • @MahdiEzzahir also you can check this one for CORS configuration https://stackoverflow.com/questions/32319396/cors-with-spring-boot-and-angularjs-not-working – Lemmy Apr 15 '20 at 14:48
  • 1
    I got another error and I couldn't achieve my purpose. Anyway I will mark it because you helped me a lot and it seems that resolved the concerned problem. Thank you @Lemmy – Mahdi Ezzahir Apr 16 '20 at 11:37
  • @MahdiEzzahir if you have another problem feel free to post another question. If error is about CORS please also take a look at https://stackoverflow.com/questions/32587570/how-to-enable-cors-properly-in-spring-restfull-webservice/32603261#32603261, there are CORSFilter example and how to use it – Lemmy Apr 16 '20 at 11:40