最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

spring boot - Springboot Backend receives jwt from Angular Fetch request, but not HttpClient { withCredentials: true } - Stack O

programmeradmin3浏览0评论

I am developing a Springboot/Angular application. I am running into an annoying issue where my backend is able to receive and parse credentials (in this case, a jwt) from fetch requests but is not receiving the same credentials when I try to use the HttpClient api.

Here are some brief examples of the two different requests:

this.httpClient.get('http://localhost:8080/api/route', {
  withCredentials: true,
  headers: { 'Content-Type': 'application/json' }
});

fetch('http://localhost:8080/api/route', {
      method: 'GET',
      headers: { 'Accept': 'application/json' },
      credentials: 'include'
    });

Now, in my app.config.ts I do have this line:

provideHttpClient(withFetch()),

However, the angular docs say to set withFetch there in SSR apps. I have tried to remove that, or add withOptions([withCredentials: true]) - but the results are the same.

I have tried a number of things, and since nothing I am doing in angular seems blatantly wrong (it very well could be, please tell me if you see something that is wrong), I started taking a look at my Springboot cors/security configurations:

Security Configuration:

@Configuration
public class SecurityConfig {

    private final FirebaseAuthFilter firebaseAuthFilter;

    public SecurityConfig(FirebaseAuthFilter firebaseAuthFilter) {
        this.firebaseAuthFilter = firebaseAuthFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception {
        http.cors(cors -> cors.configurationSource(corsConfigurationSource))
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(PUBLIC_ENDPOINTS).permitAll()
                        .anyRequest().authenticated()
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(firebaseAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Firebase Filter (I have messed with this a significant amount, but this configuration successfully passes the jwt to the rest of the backend when FETCH requests are sent, so I do not think this is the issue):

@Component
public class FirebaseAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = request.getHeader("Authorization");

        if (token != null && token.startsWith("Bearer ")) {
            try {
                FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(token.substring(7));
                UserDetails userDetails = new User(decodedToken.getUid(), "", Collections.emptyList());

                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()));
            } catch (Exception e) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

and my Cors Configuration:

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowCredentials(true);
        configuration.setAllowedOriginPatterns(List.of("http://localhost:4200"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
        configuration.setExposedHeaders(List.of("Set-Cookie"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
}

My JWT is set as httpOnly, with SameSite as lax (I have tried both None and Strict, to no avail), and I can see it as such in the browser, so I know that the cookie is being set properly.

If I have provided too little information, please do ask for what you think you need to answer the question. I am somewhat experienced, but this is my first time going solo with writing the foundations for an app like this. Thank you.

I am developing a Springboot/Angular application. I am running into an annoying issue where my backend is able to receive and parse credentials (in this case, a jwt) from fetch requests but is not receiving the same credentials when I try to use the HttpClient api.

Here are some brief examples of the two different requests:

this.httpClient.get('http://localhost:8080/api/route', {
  withCredentials: true,
  headers: { 'Content-Type': 'application/json' }
});

fetch('http://localhost:8080/api/route', {
      method: 'GET',
      headers: { 'Accept': 'application/json' },
      credentials: 'include'
    });

Now, in my app.config.ts I do have this line:

provideHttpClient(withFetch()),

However, the angular docs say to set withFetch there in SSR apps. I have tried to remove that, or add withOptions([withCredentials: true]) - but the results are the same.

I have tried a number of things, and since nothing I am doing in angular seems blatantly wrong (it very well could be, please tell me if you see something that is wrong), I started taking a look at my Springboot cors/security configurations:

Security Configuration:

@Configuration
public class SecurityConfig {

    private final FirebaseAuthFilter firebaseAuthFilter;

    public SecurityConfig(FirebaseAuthFilter firebaseAuthFilter) {
        this.firebaseAuthFilter = firebaseAuthFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception {
        http.cors(cors -> cors.configurationSource(corsConfigurationSource))
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(PUBLIC_ENDPOINTS).permitAll()
                        .anyRequest().authenticated()
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(firebaseAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

Firebase Filter (I have messed with this a significant amount, but this configuration successfully passes the jwt to the rest of the backend when FETCH requests are sent, so I do not think this is the issue):

@Component
public class FirebaseAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String token = request.getHeader("Authorization");

        if (token != null && token.startsWith("Bearer ")) {
            try {
                FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(token.substring(7));
                UserDetails userDetails = new User(decodedToken.getUid(), "", Collections.emptyList());

                SecurityContextHolder.getContext().setAuthentication(
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()));
            } catch (Exception e) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        }
        chain.doFilter(request, response);
    }
}

and my Cors Configuration:

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowCredentials(true);
        configuration.setAllowedOriginPatterns(List.of("http://localhost:4200"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
        configuration.setExposedHeaders(List.of("Set-Cookie"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }
}

My JWT is set as httpOnly, with SameSite as lax (I have tried both None and Strict, to no avail), and I can see it as such in the browser, so I know that the cookie is being set properly.

If I have provided too little information, please do ask for what you think you need to answer the question. I am somewhat experienced, but this is my first time going solo with writing the foundations for an app like this. Thank you.

Share Improve this question asked Mar 27 at 19:47 Josef CreationsJosef Creations 112 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

Update:

Turns out this had nothing to do with CORS or with Angular's management of credentials - it had to do with Angular's lifecycle. See - I was calling CheckAuthStatus (a function that sends an httpClient request to and endpoint in my backend that expects a jwt token in an httpOnly cookie), from appponent.ts inside of an ngOnInit().

Apparently, even though I was subscribing to the observable returned from that function, angular doesn't check (likely because it can't as its an httpOnly cookie) that the credentials have been bound to the request by the browser before sending the request.

I figured this out by manually adding a button that calls checkAuthStatus, and the credentials came through! Leading me to the conclusion that it was a timing problem, rather than a configuration one.

TLDR: If you call a function in ngOnInit() without allowing time for angular/the browser to load, all credentials you send will send as null - as nothing is bound. Simply set a setTimeout or rxjs delay - and you're golden.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论