43

I have a problem with default behaviour in spring security with authorize requests provided with Java Config.

http
       ....
       .authorizeRequests()
          .antMatchers("/api/test/secured/*").authenticated()

When I do a call to for example /api/test/secured/user without login (with anonymous user), it returns 403 Forbidden. Is there an easy way to change status to 401 Unauthorized when anonymous user wants to get secured by authenticated() or @PreAuthorize resource?

Mati
  • 2,476
  • 2
  • 17
  • 24
  • 2
    Using Spring Boot 2 I did it like this: https://stackoverflow.com/questions/49241384/401-instead-of-403-with-spring-boot-2/49241557#49241557 – lealceldeiro Oct 16 '18 at 19:40

6 Answers6

34

As of Spring Boot 2 class Http401AuthenticationEntryPoint has been removed (see Spring Boot Issue 10725).

Instead of Http401AuthenticationEntryPoint use HttpStatusEntryPoint with HttpStatus.UNAUTHORIZED:

http.exceptionHandling()
    .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
Tai Truong
  • 668
  • 1
  • 8
  • 11
21

With spring security 4.x there is already a class for that

org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint 

Spring boot also includes one

org.springframework.boot.autoconfigure.security.Http401AuthenticationEntryPoint

and both benefits that they require the developer to use spec compliant as 401 responses requires that header WWW-Authenticate must be set, example 401 response could be:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
                   error="invalid_token",
                   error_description="The access token expired"

So in your security configuration you define and autowire a bean of class

So for instance with spring boot app:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    @Bean
    public Http401AuthenticationEntryPoint securityException401EntryPoint(){

        return new Http401AuthenticationEntryPoint("Bearer realm=\"webrealm\"");
    }

...
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
                .antMatchers("/login").anonymous()
                .antMatchers("/").anonymous()
                .antMatchers("/api/**").authenticated()
            .and()
            .csrf()
                .disable()
                .headers()
                .frameOptions().disable()
            .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .logout()
                .permitAll()
         .exceptionHandling().authenticationEntryPoint(securityException401EntryPoint());
}

the relevant line is:

 .exceptionHandling().authenticationEntryPoint(securityException401EntryPoint());
Anton Hlinisty
  • 1,441
  • 1
  • 20
  • 35
le0diaz
  • 2,488
  • 24
  • 31
  • 3
    Instead of injecting the bean you just created, you could call the function directly: `.exceptionHandling().authenticationEntryPoint(securityException401EntryPoint());`. It would get the same instance because the call to `@Bean` annotated functions are proxyfied. – EliuX Aug 15 '17 at 18:16
  • 6
    That class [has been removed](https://github.com/spring-projects/spring-boot/issues/10715) in Spring Boot 2. I just recreated it in my app from Spring Boot 1.5.10 source control [here](https://github.com/spring-projects/spring-boot/blob/v1.5.10.RELEASE/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/Http401AuthenticationEntryPoint.java) – CorayThan Feb 01 '18 at 19:04
16

I've got solution here:

http
   .authenticationEntryPoint(authenticationEntryPoint)

AuthenticationEntryPoint source code:

@Component
public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint {

    private final Logger log = LoggerFactory.getLogger(Http401UnauthorizedEntryPoint.class);

    /**
     * Always returns a 401 error code to the client.
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException arg2) throws IOException,
            ServletException {

        log.debug("Pre-authenticated entry point called. Rejecting access");
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied");
    }
}
Meiblorn
  • 2,522
  • 2
  • 18
  • 23
  • Tanks, this solution also worked for my problem! I wanted to do the opposite: change 401 to 403. it seams authenticationEntryPoint has moved to httpBasic(), I requested a edit for this. – switch87 Feb 21 '18 at 09:22
  • I recommend this solution for those who want to customize the error response. If you are contented with changing just the status from 403 to 401... and using the default error message format.. le0diaz answer below works. As for my case, I needed to customize the error response format (JSON), and the idea from this solution did the trick. Thanks Meiblorn! – Borgy Manotoy Apr 24 '18 at 11:02
  • @adil Check jhipster project – Meiblorn Aug 10 '18 at 13:55
6

A simple approach in Spring Boot 2 using lambda expressions:

@Override
public void configure(HttpSecurity http) throws Exception {
    http.
        ...
        .exceptionHandling()
            .authenticationEntryPoint((request, response, e) -> {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType("application/json");
                response.getWriter().write("{ \"error\": \"You are not authenticated.\" }");
            })
        ...
}
Hamid Mohayeji
  • 3,977
  • 3
  • 43
  • 55
5

You need to extend AuthenticationEntryPoint to do customization based upon the exceptions.

@ControllerAdvice
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
      throws IOException, ServletException {
    // 401
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Failed");
  }

  @ExceptionHandler (value = {AccessDeniedException.class})
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AccessDeniedException accessDeniedException) throws IOException {
    // 401
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Failed : " + accessDeniedException.getMessage());
  }
}

Specify the above custom AuthenticationEntryPoint in your SecurityConfig like below:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity (prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.exceptionHandling()
        .authenticationEntryPoint(new MyAuthenticationEntryPoint());
  }
}
Sahil Chhabra
  • 10,621
  • 4
  • 63
  • 62
1

Who interested in mechanism of work.
If you don't set http.exceptionHandling().authenticationEntryPoint() spring will use defaultAuthenticationEntryPoint() and method ExceptionHandlingConfigurer.createDefaultEntryPoint() will return new Http403ForbiddenEntryPoint()
So, just create Http401UnauthorizedEntryPoint(). Above answers how to do it, didn't duplicate it.

P.S. It's actual for Spring Security 5.2.5.RELEASE

Anton
  • 604
  • 2
  • 11
  • 22