1

I could not find a proper solution to my problem yet. I'm using Spring Security Oauth2 resource server to authenticate my requests. And that works fine. But when tested with different scenario it is found that spring security returns with 403 instead of 401 if there is no Authorization header present or if there is Authorization header present but the value doesn't begin with Bearer .

Spring Boot Starter - 2.6.7
Spring Boot Starter Security - 2.6.7
Spring Security Config & Web - 5.6.3
Spring Security Core - 5.3.19
Spring Boot Starter OAuth2 Resource Server - 2.6.7
Spring OAuth2 Resource Server - 5.6.3

I was referring to this answer and added below code for BearerTokenAuthenticationEntryPoint. The difference is I'm using introspection url instead jwt. But it doesn't help and that part doesn't get executed. If the Bearer token is present, then only it gets executed.

What am I missing here?

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class CustomResourceServerSecurityConfiguration {

    @Value("${spring.security.oauth2.resourceserver.opaque-token.introspection-uri}")
    String introspectionUri;

    @Value("${spring.security.oauth2.resourceserver.opaque-token.client-id}")
    String clientId;

    @Value("${spring.security.oauth2.resourceserver.opaque-token.client-secret}")
    String clientSecret;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
                .oauth2ResourceServer(oauth2 -> oauth2
                        .opaqueToken(opaque -> opaque.introspectionUri(this.introspectionUri)
                                .introspectionClientCredentials(this.clientId, this.clientSecret))
                        .authenticationEntryPoint((request, response, exception) -> {
                            System.out.println("Authentication failed");
                            BearerTokenAuthenticationEntryPoint delegate = new BearerTokenAuthenticationEntryPoint();
                            delegate.commence(request, response, exception);
                        }))

                .exceptionHandling(
                        (exceptions) -> exceptions.authenticationEntryPoint((request, response, exception) -> {
                            System.out.println("Authentication is required");
                            BearerTokenAuthenticationEntryPoint delegate = new BearerTokenAuthenticationEntryPoint();
                            delegate.commence(request, response, exception);
                        }));
        return http.build();
    }
}
iCode
  • 8,892
  • 21
  • 57
  • 91

3 Answers3

2

If your scenario is that you get 403 for POST and 401 for GET if the Bearer token is missing it's related to csrf.

44.2.14 I get a 403 Forbidden when performing a POST

If an HTTP 403 Forbidden is returned for HTTP POST, but works for HTTP GET then the issue is most likely related to CSRF. Either provide the CSRF Token or disable CSRF protection (not recommended).

Here's source

and if you use JWT token then if doesn't have any ther additional requirements then you can disable this.

  @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
          http.authorizeHttpRequests(authorize -> authorize
                        .anyRequest().authenticated())
                        .csrf().disable()           
                 return http.build();
    } 

4. Stateless Spring API

If our stateless API uses token-based authentication, such as JWT, we don't need CSRF protection, and we must disable it as we saw earlier

Source

witosh
  • 79
  • 3
  • 1
    JWT or opaque is not important. What you should ensure before disabling CSRF protection is that sessions are disabled too (stateless session-management) – ch4mp Nov 08 '22 at 11:28
  • Yes this did the trick. And also I added session policy to stateless. `http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().csrf().disable()` – iCode Nov 11 '22 at 08:30
1

With default conf, you should have a 302 (redirect to login) when authorization header is missing or invalid (malformed, expired, wrong issuer,...). If you have a 403, then you are facing another exception (CSRF, CORS or whatever). Set logging.level.org.sprngframework.security=DEBUG and carefully inspect the logs

To change this default behavior (401 instead of 302), do like it is done in those tutorials:

        http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> {
            response.addHeader(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"Restricted Content\"");
            response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
        });

In samples, like the one for servlets and token introspection, which meets your exact use-case, you can even find unit tests which assert that http status are what you expect: 401 when not authorized and 403 when denied:

    @Test
// security-context not set for this test => anonymous
    void greetWitoutAuthentication() throws Exception {
        api.get("/greet").andExpect(status().isUnauthorized());
    }

    @Test
@WithMockBearerTokenAuthentication(authorities = "ROLE_AUTHORIZED_PERSONNEL", attributes = @OpenIdClaims(sub = "Ch4mpy"))
    void securedRouteWithAuthorizedPersonnelIsOk() throws Exception {
        api.get("/secured-route").andExpect(status().isOk());
    }

    @Test
@WithMockBearerTokenAuthentication(authorities = "NOT_A_REQUIRED_ROLE")
    void securedMethodWithoutAuthorizedPersonnelIsForbidden() throws Exception {
        api.get("/secured-method").andExpect(status().isForbidden());
    }

Off course, those tests pass...

ch4mp
  • 6,622
  • 6
  • 29
  • 49
  • Hi. Still it doesn't work. And as per my understanding Barer token also sets this header. – iCode Nov 08 '22 at 07:21
  • No,, it doesn't. Actually, it sets 302 (redirect to login) and not 403 (forbidden) by default. You are probably rizing an exception which is mapped to 403. Fix that before you can apply the conf in my answer. – ch4mp Nov 08 '22 at 11:24
  • I use 403 for an exception in the exception handler class @RestControllerAdvice. But that's not in Security config and it is dynamically setting the response status for an exception based on some other value. Not with any annotation. Could that be causing this? – iCode Nov 08 '22 at 13:16
0

I have the same problem ,I just don't understand why the security team return 403 in default AuthenticationEntryPoint when there is not token in the request header.

My solution is custom AuthenticationEntryPoint:

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private static final Log logger = LogFactory.getLog(CustomAuthenticationEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        logger.debug("Pre-authenticated entry point called. Rejecting access");
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied");
    }
}