Search code examples
angularspringhttpcookiesspring-security

Angular Client not setting Cookie with JSessionID


I'm using an angular frontend, and a spring backend with spring security. I have secured my API with the following code

            http
                    .cors().and()
//                    .csrf().disable()
                    .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
                    .authorizeRequests()
//                        .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                        .antMatchers("/xrtool/*").permitAll()
                        .antMatchers("/advanced_search").hasRole("USER")
                        .antMatchers("/creation").hasRole("USER")
                        .antMatchers("/edit").hasRole("USER")
                        .antMatchers("/basicAuth").permitAll()
                        .antMatchers("/**").permitAll()
                    .anyRequest().authenticated().and()
                    .httpBasic();

I have no problems reaching the APIs with no protection (permitAll) but those which require a certain role give me a Status 401 unauthorized. I believe that is due to my client not automatically setting the cookies after the initial authentication request.

The code for my authorization request:

  authenticate(username : string, password : string){

    let authHeader = new HttpHeaders({
      authorization : 'Basic ' + btoa(username + ':' + password),
      withCredentials: 'true'
    });

    return this.http.get(`${API_URL}/basicAuth`, {headers: authHeader})
  }

My backend code:

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity http) {
        try {
            http
                    .cors().and()
                    .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
                    .authorizeRequests()
                        .antMatchers("/xrtool/*").permitAll()
                        .antMatchers("/advanced_search").hasRole("USER")
                        .antMatchers("/creation").hasRole("USER")
                        .antMatchers("/edit").hasRole("USER")
                        .antMatchers("/basicAuth").permitAll()
                        .antMatchers("/**").permitAll()
                    .anyRequest().authenticated().and()
                    .httpBasic();
        } catch (Exception e){
            System.out.println(e);
        }
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

The response header for my initial request:

HTTP/1.1 200 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://localhost:4200
Access-Control-Allow-Credentials: true
Set-Cookie: XSRF-TOKEN=475b11b6-39a4-4a46-a58d-e096a3bc07c6; Path=/
Set-Cookie: JSESSIONID=3E8382E29CB7C0AD9BBC168EA08ACF30; Path=/; HttpOnly
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 21 Jan 2023 17:56:43 GMT
Keep-Alive: timeout=60
Connection: keep-alive

And the subsequent request headers:

GET /xrtool/Test HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
withCredentials: true
Origin: http://localhost:4200
DNT: 1
Connection: keep-alive
Referer: http://localhost:4200/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site

The storage of the webclient also doesn't contain the Cookies.

I have tried it with different browsers (Chrome, Firefox, Edge), but the result is consistent.

I tried accessing the APIs directly (localhost:8080/api/request) and everything is fine. I can also see the cookies in the webclient storage.

Postman also works fine. I can see the cookies being saved in the header, and after a initial authorization request, subsequent request are authorized.

I tried adding the authorization header in every request through an interceptor, and i was able to reach the APIs.

EDIT:

Thanks to Tr1Monsters' observation i was able to find the issue. The withCredentials header was at the wrong place. Appearently it doesn't need to be in the initial authentification request, but every subsequent request. I have added it to my Http-Intercept like this:

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    let username = "b"
    let password = "b"
    let basicAuthHeaderString = "Basic " + window.btoa(username + ':' + password)

    request = request.clone({
      setHeaders: {
        // Authorization: basicAuthHeaderString
      }
      , withCredentials: true
    })


    return next.handle(request);
  }

I have left in the original way i've added the header to show that the withCredentials header is placed outside the setHeaders part.


Solution

  • Thanks to Tr1Monsters' observation i was able to find the issue. The withCredentials header was at the wrong place. Appearently it doesn't need to be in the initial authentification request, but every subsequent request. I have added it to my Http-Interceptor like this:

      intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    
        let username = "b"
        let password = "b"
        let basicAuthHeaderString = "Basic " + window.btoa(username + ':' + password)
    
        request = request.clone({
          setHeaders: {
            // Authorization: basicAuthHeaderString
          }
          , withCredentials: true
        })
    
    
        return next.handle(request);
      }
    

    I have left in the original way I've added the header, to show that the withCredentials header is placed outside the setHeaders part.