Summary

When http basic auth is used and the request has an 'X-Requested-With: XMLHttpRequest' header set the server still responds with www-authenticate when credentials are wrong

Actual Behavior

  • http basic auth is configured and working
  • Javascript client calls /api/user with http basic auth credentials that are wrong
  • server responds with 401 and www-authenticate header set
  • browser opens basic auth popup

Expected Behavior

The Server should not add the 'www-authenticate' header

Configuration

@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        return http
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers("/*").permitAll()
            .pathMatchers("/api/*").authenticated()
            .and().httpBasic()
            .and()
            .build();
    }

    @Bean
    ReactiveUserDetailsService userDetailService(PasswordEncoder encoder) {
        User.UserBuilder user = User.withUsername("username")
            .roles(USER)
            .password(encoder.encode("password"));

        return new MapReactiveUserDetailsService(user.build());
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

Version

./gradlew dI --dependency spring-boot-starter-security
:dependencyInsight
org.springframework.boot:spring-boot-starter-security:2.0.0.RELEASE (selected by rule)

org.springframework.boot:spring-boot-starter-security: -> 2.0.0.RELEASE
\--- compileClasspath

Sample

curl 'http://localhost:8080/api/user' -u user:wrong_pass -H 'X-Requested-With: XMLHttpRequest' -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'user'
> GET /api/user HTTP/1.1
> Host: localhost:8080
> Authorization: Basic dXNlcjp3cm9uZ19wYXNz
> User-Agent: curl/7.59.0
> Accept: */*
> X-Requested-With: XMLHttpRequest
> 
< HTTP/1.1 401 Unauthorized
* Authentication problem. Ignoring this.
< WWW-Authenticate: Basic realm="Realm"
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Content-Type-Options: nosniff
< X-Frame-Options: DENY
< X-XSS-Protection: 1 ; mode=block
< content-length: 0
< 
* Connection #0 to host localhost left intact

Comment From: zpf7879

I am facing the same issue. Any workaround?

Comment From: rwinch

A workaround is that you can provide a custom ServerAuthenticationEntryPoint via:

http
   .exceptionHandling()
       .authenticationEntryPoint(customEntryPoint);

Comment From: zpf7879

Exactly, I found out the same solution but thanks anyway:

.exceptionHandling()
                .authenticationEntryPoint((exchange, denied) -> {
                    ServerHttpResponse response = exchange.getResponse();
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    List<String> requestedWith = exchange.getRequest().getHeaders().get("X-Requested-With");
                    if (requestedWith == null || !requestedWith.contains("XMLHttpRequest")) {
                        response.getHeaders().set(WWW_AUTHENTICATE, 
                        String.format(WWW_AUTHENTICATE_FORMAT, DEFAULT_REALM));
                    }
                    exchange.mutate().response(response);
                    return Mono.empty();
                })

`

**Comment From: vnobo**

I am facing the same issue. Any workaround?
@zpf7879 I still get it when I make a mistake

**Comment From: vnobo**

@rwinch  hi, Now when I don't certify it, it's normal.
But when I add certification "Authorization: Basic YWRtaW46MzIxMzIxMzE=" wrong password ,It reappeared
return response :
`HTTP/1.1 401 Unauthorized
X-Powered-By: Express
www-authenticate: Basic realm="Realm"
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1 ; mode=block
referrer-policy: no-referrer
content-length: 0
connection: close
Date: Tue, 03 Sep 2019 08:57:15 GMT`
Now I don't know what to do. Could you help me? Thank you very much

**Comment From: rwinch**

@vnobo Since 5.2.0.M1 (see gh-6270) you can configure an entry point for the failures.

```java
http
    .httpBasic()
        .authenticationEntryPoint(custom)

Comment From: gdadon

I faced this same issue, except I've disabled the basic auth in the filters via httpBasic().disable()

in my case, I've removed this header during Nginx response with hide header.

Comment From: meredrica

We gave up on Spring and switched to Micronaut.