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 badges1 Answer
Reset to default 0Update:
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.