Multiple Spring Security Configurations — Form-Based & Token-Based Authentication

Rakib Ahmed
5 min readSep 24, 2023

--

I will start by giving a practical scenario, Suppose you are building a monolithic application with a backend and frontend packed together. For securing some private routes you are using form-based and session-based login. A new requirement comes that you also need to expose some REST APIs for a third-party application or mobile application. Now for the REST APIs, you can not use session-based security you need to use a token-based security mechanism.

In this article, we will mimic the same scenario and step-by-step learn how we can use multiple security configurations in our Spring Boot project. We will use JWT authentication to secure our protected REST APIs at the same time we will use Form-Based and Session-based authentication mechanisms to secure the monolithic routes of our application.

Let's add all the required dependencies to our project pom.xml file.

<dependencies>  
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<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> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>

well, of course, we are using the SpringBoot — 3.

CONFIGURING SPRING SECURITY

Let’s start by creating a SecurityConfig class within a config package, and annotating it with @EnableWebSecurity and @Configuration. We will define beans of type SecurityFilterChain for each scenario.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

}

API SECURITY FILTER CHAIN

Create a securityFilterChain bean that also takes in an HttpSecurity object. Here I have kept all the routes protected except the routes that begin with /login and /error.

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests((requests) -> {
requests.requestMatchers("/login/**").permitAll();
requests.requestMatchers("/error").permitAll();
requests.anyRequest().authenticated();
}
)
.formLogin((form) ->form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/user/")
.permitAll()
)
.logout((logout) -> logout.permitAll());
return http.build();
}

When we use SpringSecurity and enable WebSecurity every HTTP request will go through SecurityFilterChain. Here we can configure which HTTP routes need to be authenticated and which do not. We can create multiple security filter chains and use them according to our use. So let's add another apiSecurityFilterChain bean in our SecurityConfig class. To ensure that each security filter chain is only invoked for the matching pattern, add securityMatcher annotation. All of my REST API endpoints will start with “/api”. We can also use securityMatcher(MvcRequestMatcher(“/api/**”)). Also to use JWT token-based authentication we used object of JwtAuthenticationFilter using addFilterBefore.

@Configuration
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint point;
@Autowired
private JwtAuthenticationFilter filter;

@Bean
@Order(1)
SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/api/auth/login").permitAll();
auth.anyRequest().authenticated();
}).exceptionHandling(ex -> ex.authenticationEntryPoint(point))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests((requests) -> {
requests.requestMatchers("/login/**").permitAll();
requests.requestMatchers("/error").permitAll();
requests.anyRequest().authenticated();
}
)
.formLogin((form) ->form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/user/")
.permitAll()
)
.logout((logout) -> logout.permitAll());
return http.build();
}
}

We have created our SecurityConfig class that will use apiSecurityFilterChain to handle token-based authentication for only the API routes. All other protected routes will be handled by securityFilterChain.

JwtAuthenticationEntryPoint

Create Class JWTAthenticationEntryPoint that implement AuthenticationEntryPoint. Method of this class is called whenever as exception is thrown due to unauthenticated user trying to access the resource that required authentication.

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("Access Denied !! " + authException.getMessage());
}
}

JwtAuthenticationFilter

Create JWTAuthenticationFilter that extends OncePerRequestFilter and override method and write the logic to check the token that is comming in header.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtHelper jwtHelper;


@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

//Authorization
String requestHeader = request.getHeader("Authorization");

String username = null;
String token = null;
if (requestHeader != null && requestHeader.startsWith("Bearer")) {
//looking good
token = requestHeader.substring(7);
try {

username = this.jwtHelper.getUsernameFromToken(token);

} catch (IllegalArgumentException e) {
logger.info("Illegal Argument while fetching the username !!");
e.printStackTrace();
} catch (ExpiredJwtException e) {
logger.info("Given jwt token is expired !!");
e.printStackTrace();
} catch (MalformedJwtException e) {
logger.info("Some changed has done in token !! Invalid Token");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();

}


} else {
logger.info("Invalid Header Value !! ");
}


//
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {


//fetch user detail from username
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
Boolean validateToken = this.jwtHelper.validateToken(token, userDetails);
if (validateToken) {

//set the authentication
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);


} else {
logger.info("Validation fails !!");
}


}
filterChain.doFilter(request, response);
}
}

JwtHelper

Create JWTHelper class This class contains method related to perform operations with jwt token like generateToken, validateToken etc.

@Component
public class JwtHelper {

//requirement :
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;

// public static final long JWT_TOKEN_VALIDITY = 60;
private String secret = "afafasfafafasfasfasfafacasdasfasxASFACASDFACASDFASFASFDAFASFASDAADSCSDFADCVSGCFVADXCcadwavfsfarvf";

//retrieve username from jwt token
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}

//retrieve expiration date from jwt token
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}

public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}

//for retrieveing any information from token we will need the secret key
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}

//check if the token has expired
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}

//generate token for user
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}

//while creating the token -
//1. Define claims of the token, like Issuer, Expiration, Subject, and the ID
//2. Sign the JWT using the HS512 algorithm and secret key.
//3. According to JWS Compact Serialization(https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)
// compaction of the JWT to a URL-safe string
private String doGenerateToken(Map<String, Object> claims, String subject) {

return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}

//validate token
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}

So, that’s all you need for authentication. Now create some controller accordingly and test it. Just remember that only the routes that starts with “/api” will be authenticated by JWT token using apiSecurityFilterChain and other routes will be authenticated by session-based login using securityFilterChain.

--

--