I am working with Spring Boot and React to authenticate and authorize a user and then to access a secured endpoint.
Entry point of the program: exclusion commented out (was not before)
@SpringBootApplication//(exclude = { SecurityAutoConfiguration.class })
@EnableJdbcHttpSession(maxInactiveIntervalInSeconds = 1800)
public class Backend3Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Backend3Application.class, args);
}
}
The API creates a token when a login request is made and then decodes it:
package com.example.demo.service;
import java.security.Key;
import java.util.Date;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.xml.bind.DatatypeConverter;
@Service
public class JwTTokenProviderService {
@Value("${jwt.secret}")
private String JWT_SECRET_KEY;
// the JWT_SECRET_KEY is populated in the application.properties file.
public String generateToken(String username, String role) {
//The JWT signature algorithm we will be using to sign the token
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
Key signingKey = new SecretKeySpec(DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY), signatureAlgorithm.getJcaName());
long jwtExpirationInMs = 36000;
System.out.println("Username at generating token is " + username);
System.out.println("Role at generating token is " + role);
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setSubject("Me")
.claim("hasRole", role)
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationInMs))
.signWith(signingKey, signatureAlgorithm)
.compact();
}
public String getAllClaimsFromToken(String token) {
System.out.println("Token at getAllClaimsFromToken is " + token);
try {
byte[] arrSecret = DatatypeConverter.parseBase64Binary(JWT_SECRET_KEY);
Key signingKey = new SecretKeySpec(arrSecret, SignatureAlgorithm.HS256.getJcaName());
Jws<Claims> jwsClaims = Jwts.parser()
.setSigningKey(signingKey)
.parseClaimsJws(token);
Claims claims = jwsClaims.getBody();
return claims.get("hasRole", String.class); //
} catch (Exception e) {
System.out.println("Could not get all claims TOken from passed token");
System.out.println("Exception is " + e);
return null;
}
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token);
return true;
} catch (Exception e) {
System.out.println("Errir is " + e);
}
return false;
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("username", String.class));
}
public String getRoleFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("hasRole", String.class));
}
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
}
this is the method that sends the request:
getInfoForUser(username) {
try {
console.log("Username at Axios is, ", username)
console.log("User token at getInfoForUser is: ", localStorage.getItem('userToken'))
return axios.post("http://myserver:8080/myapp/login/get-info-for-user", {params: {
username: username
},
headers: {
'Content-Type':'application/json; charset=UTF-8',
'Authorization': `${localStorage.getItem("userToken")}`,
'Accept': 'application/json'
}}
)
} catch (error) {
console.log("Error is, ", error)
}
}
This is the secured method in the controller:
@PreAuthorize("hasRole('ROLE_USER')")
@PostMapping("/get-info-for-user")
public Object[] getInfoForUser(@Param("username") String username) {
return userService.getInfoForUser(username);
}
This then leads into a repo with a SQL query, but this isn’t the problem as it functioned before the implementation of the token mechanism.
The JwtResponse file, or model for the authorization header:
package com.example.demo.model;
import java.util.List;
public class JwtResponse {
private String token;
private String type = "Bearer";
private String username;
private String hasRole;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String gethasRole() {
return hasRole;
}
public void sethasRole(String hasRole) {
this.hasRole = hasRole;
}
public JwtResponse(String token, String username, String hasRole) {
super();
this.token = token;
this.username = username;
this.hasRole = hasRole;
}
}
SecurityConfig enables the annotations:
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Autowired
private JwtTokenFilter JwtTokenFilter;
return http
.cors(cors -> cors.disable())
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement((session) ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(authorize -> {
authorize
.requestMatchers("/myapp/**").permitAll()
.requestMatchers("/public/**").permitAll()
.requestMatchers("/login/**").permitAll()
.anyRequest().permitAll();
}
)
.addFilterBefore(new JwtTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.httpBasic(Customizer.withDefaults())
.build();
}
}
Pom dependencies for reference:
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
I’ve created a JwtTokenFilter
package com.example.demo;
import java.io.IOException;
import java.util.Collections;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.example.demo.service.JwTTokenProviderService;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class JwtTokenFilter extends OncePerRequestFilter implements Filter {
private JwTTokenProviderService jwtTokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
System.out.println("JwtFilter hit for request: " + request.getRequestURI());
System.out.println("REquest.getHeader.getFilterName being received is " + request.getHeader(getFilterName()));
System.out.println("Request.BASIC_AUTH hit for request: " + request.BASIC_AUTH);
System.out.println("Request.BASIC_AUTH hit for request: " + request.CLIENT_CERT_AUTH);
System.out.println("Request.BASIC_AUTH hit for request: " + request.DIGEST_AUTH);
System.out.println("Request.BASIC_AUTH hit for request: " + request.FORM_AUTH);
System.out.println("Request.BASIC_AUTH hit for request: " + request.getAttributeNames());
String token = resolveToken(request);
if (token != null && jwtTokenService.validateToken(token)) {
Authentication auth = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private Authentication getAuthentication(String token) {
System.out.println("Token at authentication for secured endpoint is: " + token);
String username = jwtTokenService.getUsernameFromToken(token);
String role = jwtTokenService.getRoleFromToken(token);
System.out.println("Retrieved data for token is: " + username + " " + role);
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role);
return new UsernamePasswordAuthenticationToken(username, "", Collections.singletonList(authority));
}
private String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer " )) {
return bearerToken.substring(7);
}
return null;
}
}
This is the feedback once the user logs in:
The username, role, token and type "Bearer" all come through and then this information is submitted as the authorization header in the secured endpoint request.
The token is also verified, once I input the secret in from the application.properties file:
It is the first time I am attempting to do this and generally speaking, checked over these things:
- Used a POST method and not GET in the frontend and @PostMapping for the backend; I’ve tried switching but the result is the same.
- The JWT secret is secured in the application.properties file.
- I do get a warning that the parser() and setSigningKey() method from it are deprecated in the version of JJWT I am using, but I haven’t been able to locate a good example – though that should not necessarily trigger the 401 error.
- Because I am using the @PreAuthorize annotation, which looks for ‘ROLE_’ as a prefix, I’ve done that in the database, so it returns the same syntax.
- I thought about the "hasRole" attribute in the annotation and made it the same in the JwtResponse model, which builds the authorization header.
- I thought it was an issue with the secret being generated, as the token returned an invalid signature, but that got corrected when I put in the secret into the checker.
- Everything is encoded and secured and there are no plain text values as relevant to the token being passed around. Or rather, should there be nothing but the token in the authorization header of the request?
- Pre/Post is enabled in the security configuration.
This was perhaps the most helpful guide on SO from the myriad I saw:
Static secret as byte[], Key or String?
Any insight will be appreciated…
UPDATES:
- JwtTokenFilter class is a new addition
- SecurityConfig updated to reflect it
- Modifications to JwtTokenProviderService file
- Modified the token to include "Bearer " + token, as apparently this is how Spring Security needs to handle it.
- Updated the structure of the token being created; username and role are both claims.
- Removed ‘Bearer …" from the axios request, since the returned token now includes it, so it does not submit "Bearer Bearer token".
- Commented out //(exclude = { SecurityAutoConfiguration.class }, which was the reason why the JwtTokenFilter was not being contacted.
- Added Sysout statements to the filter that now reacts.
- Added a jwtFilter object to the SecurityConfiguration
- Screenshot for what the filter produces after the request reaches the API
I’ve put in some Sysout statements to see how the authentication manager handles the decoding of the token, but it is doing absolutely nothing and the output does not register. I am thinking – once the login request is submitted and the secured endpoint accessed, the token needs to be sent separately and those methods to extract a role and username have to be accessed specifically? If so, that would defeat the entire purpose of the framework, if I am sending an authentication header anyway.
UPDATE 2:
The JwtTokenFilter is registering the request, but does not seemingly get the the authorization from it…
I am not sure what attribute of the request to use to actually get the token to register correctly.
2
Answers
I feel like I've discovered fire, but here is the A-Z solution for a secure request from an Axios request in React to a Spring Boot API, using a JWT Token.
For context, this is done with Spring Boot 3, Java 17 and Spring Security 6.
This is the starting point with the Axios request:
The problem here was that the headers component was inside the params component. The concept is that the POST request has URL, PAYLOAD, HEADERS as the basic structure of the request. The issue here was one of a literal bracket to make the headers section its own.
The endpoint that the request hits. No changes here:
The API infrastructure that handles the request:
The JwtTokenProviderService:
In this file, the API generates a token and has methods to extract all information from it, validate it, or get the username and password separately. It is effectively the class to create and manipulate the token; you also have options on how to build it.
This is the JwtTokenFilter file, which will accept the incoming request from the endpoint and deconstruct the token to extract the relevant tokens and roles; note that it uses the methods specified in the above JwtTokenProviderService file:
Important to note here - have the @Component annotation on top of your code. The rest of the methods use boolean criteria to determine the validity of the token, based on the fact it's not blank and uses the "Bearer " prefix, before extracting the relevant content.
This is the JwtResponse model that build the JwtResponse object. It is what it used to create the value of the token, which can happen at the login process, for example and again, these attributes are filled in the token creation method in the JwtTokenProviderService file:
This is just a standard getter and setter file for the JwtResponse using basic data types.
This is the WebSecurityConfig file:
Ensure that you've added this line to your configuration:
This ensures that the request hits the jwtFilter when it is submitted.
You also don't necessarily need a @Bean configuration for the jwtFilter, but it is an option you also have in the configuration of the application.
In the main file of your Spring Boot Application:
Ensure that this line:
is commented out to not prevent Spring Security from working, or just not included at all.
Last but not least, the dependencies used have not changed:
That's it. Hopefully it helps someone.
I think you might not be setting the resource server for this. I’m relatively new to spring security but it looks like it’s one of the requirements in order for Spring Security to auto configure to accept JWTs.
https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-minimaldependencies
It’s a quick read and I recommend you check it out if you haven’t. Spring Sec has a lot of ways to do things but it might seem daunting to read. You might be sleep deprived or tired. I’m only pointing this out as I see you’re doing it in a different way. Maybe you missed or forgot some steps we all do sometimes. Anyways hope this helps.