I am working on a decoupled react.js and Java with Spring Boot application and have just finished the first part of the backend. I admittedly don't have a firm grasp of Spring Security when it comes to using JWTs but I have been following along with a video tutorial and have read the docs to the best of my ability. Unfortunately, the tutorial is somewhat outdated and I've had to refactor certain parts of it to account for deprecations. I have apparently made an error at some point and can't figure out what I did wrong.
When I go to localhost:8080 (the port the backend is running on), I get a 403 error. This suggested to me that it was an issue with the SecurityConfiguration
so I verified that the "/" endpoint was in the permitAll()
line and it was. Here is my entire SecurityConfiguration
:
package com.keyoflife.keyoflife.config;
import com.keyoflife.keyoflife.services.UserDetailsLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityConfiguration {
private final UserDetailsLoader usersLoader;
private final JwtFilter jwtFilter;
public SecurityConfiguration(UserDetailsLoader usersLoader, JwtFilter jwtFilter) {
this.usersLoader = usersLoader;
this.jwtFilter = jwtFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
//TODO: remove this
//get the hashed version of 'asdfasdf' to save in db
//System.out.println(new BCryptPasswordEncoder().encode("asdfasdf"));
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder)
throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(usersLoader).passwordEncoder(passwordEncoder);
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user-dash").authenticated()
.requestMatchers(HttpMethod.OPTIONS,"/api/auth/**", "/login", "/").permitAll()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(Customizer.withDefaults())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin((login) -> login
.loginPage("/login")
.failureUrl("/login")
.defaultSuccessUrl("/user-dash"))
/* Logout configuration */
.logout((logout) -> logout
.logoutSuccessUrl("/"))
.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://127.0.0.1:8080");
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
configuration.setAllowCredentials(true); // for cookies. bc everyone likes cookies
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Next, I went through the stack trace and saw that the error actually occurred in my JWTFilter
on the chain.doFilter
line seen here in the first if()
statement of the doFilterInternal
method: I will post the first part of this class as the rest seems inconsequential since the code isn't getting passed the first return:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// Get authorization header and validate
final String header = request.getHeader("Authorization");
System.out.println(header);
//ensure header has text and starts with "Bearer "
if (!StringUtils.hasText(header) || (StringUtils.hasText(header) && !header.startsWith("Bearer "))) {
chain.doFilter(request, response); //Access Denied Exception thrown here because header is null
return;
}
//rest of method below
}
I added the System.out
line to see what the value of header
was, and found that when I try to go to localhost8080, that value is "Basic (alpha-numeric string)" but when I try to access the /api/auth/login endpoint with
{
"username":"testUser",
"password": "asdfasdf"
}
using Postman with a JSON body and POST method, I get a 401 error and a null header
. I did verify that this is an actual user in my db.
I have seen several other SO posts regarding this issue and have read through them all. Several of the posts suggest to add a cors config which I have (see SecurityConfiguration
code). Another says to add the "Authorization" arg to the getHeader
line in the JWTFilter
which I already have. I'm at a loss for where to go from here and so this is my first SO post ever. I've been reading SO for years now but never got so stuck that I needed to ask a question before. I apologize if I've left something pertinent out. Please let me know and I'll edit my question. I just didn't want to flood the post with extraneous code.
Well it took me three days to figure out but I finally solved the issue I was having. As it turns out, I was not configuring Postman correctly and was sending the payload in the wrong place. I needed to go to the Authorization tab in Postman and include the secret I was using and enter the payload information on that same tab. I got the answer from the YouTube video below.
https://www.youtube.com/watch?v=dLxCpd3IGys
If the link doesn't work or you'd prefer not to click on a link, search for "How to Use JWT Authorization" by Postman.