Spring Boot Project: Security with JJWT Authentication Token

{getToc} $title={Table of Contents}

Spring Security & JWT Authentication


Introduction

This is the blog about how to create spring boot maven project and implementing Spring Security with JJWT Web Token as the authentication token.

I'm going to setup spring boot maven project from scratch. And adding the neccessary dependencies as needed step by step and implement code line by using postman request testing for each step.


Using Java, Spring, Other Libs & JJWT:

  • java: 11
  • spring boot: V2.7.3
  • spring-web: 5.3.22
  • spring-security-web: 5.7.3
  • jjwt: V0.11.5
  • loombox: 1.18.24

Simple Project Classes & Packages Structured


Spring Boot MVN Project Class&Packages Structured


What Are JWTs?

JWTs are an encoded representation of a JSON object. The JSON object consists of zero or more name/value pairs, where the names are strings and the values are arbitrary JSON values. JWT is useful to send such information in the clear (for example in an URL) while it can still be trusted to be unreadable (i.e. encrypted), unmodifiable (i.e. signed) and url-safe (i.e. Base64 encoded).

Java JWT: JSON Web Token for Java and Android

JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM and Android.

JJWT is a pure Java implementation based exclusively on the JWT, JWS, JWE, JWK and JWA RFC specifications and open source under the terms of the Apache 2.0 License.

The library was created by Les Hazlewood and is supported and maintained by a community of contributors.


This Project Dependencies



<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.3</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-boot-security-jwt-auth</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-security-jwt-auth</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</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-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</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> <!-- or jjwt-gson if Gson is preferred -->
			<version>0.11.5</version>
			<scope>runtime</scope>
		</dependency>
		<!-- Uncomment this next dependency if you are using JDK 10 or earlier 
			and you also want to use RSASSA-PSS (PS256, PS384, PS512) algorithms. JDK 
			11 or later does not require it for those algorithms: <dependency> <groupId>org.bouncycastle</groupId> 
			<artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> <scope>runtime</scope> 
			</dependency> -->
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>



Understanding JJWT Dependencies

Notice the above dependency declarations all have only one compile-time dependency and the rest are declared as runtime dependencies.

This is because JJWT is designed so you only depend on the APIs that are explicitly designed for you to use in your applications and all other internal implementation details - that can change without warning - are relegated to runtime-only dependencies. This is an extremely important point if you want to ensure stable JJWT usage and upgrades over time:

JJWT guarantees semantic versioning compatibility for all of its artifacts except the jjwt-impl .jar. No such guarantee is made for the jjwt-impl .jar and internal changes in that .jar can happen at any time. Never add the jjwt-impl .jar to your project with compile scope - always declare it with runtime scope.

This is done to benefit you: great care goes into curating the jjwt-api .jar and ensuring it contains what you need and remains backwards compatible as much as is possible so you can depend on that safely with compile scope. The runtime jjwt-impl .jar strategy affords the JJWT developers the flexibility to change the internal packages and implementations whenever and however necessary. This helps us implement features, fix bugs, and ship new releases to you more quickly and efficiently.


Security Configuration

In SecurityConfiguration class, I have custom user & password storage (UserDetailsService) by using spring security UserDetailsService.

@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfiguration {

	@Autowired
	@Lazy
	private LoginFilter loginFilter;
        @Autowired
	private JwtAuthFilter jwtAuthFilter;

@Bean
	UserDetailsService userDetailsService() {
		var userDt = new InMemoryUserDetailsManager();
		userDt.createUser(User.builder().username("user").password("{noop}user").roles("USER").build());
		userDt.createUser(User.builder().username("admin").password("{noop}admin").roles("USER", "ADMIN").build());
		return userDt;
	}


	@Bean
	AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
		var daoAuth = new DaoAuthenticationProvider();
		daoAuth.setUserDetailsService(userDetailsService);
		return new ProviderManager(daoAuth);
	}

        /* Expose AuthenticationManager bean */

	@Bean
	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

		http.csrf().disable();
		http.authorizeHttpRequests().anyRequest().authenticated();
		http.addFilterAt(loginFilter, BasicAuthenticationFilter.class);
                http.addFilterAfter(jwtAuthFilter, BasicAuthenticationFilter.class);
http.sessionManagement().sessionCreationPolicy(
				SessionCreationPolicy.STATELESS);
                /* Custom error message */
		http.exceptionHandling()
                    .accessDeniedHandler((request, response, accessDeniedException) -> {
                        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                        response.getWriter().print(accessDeniedException.getMessage());
                    }).authenticationEntryPoint((request, response, authException) -> {
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                        response.getWriter().print(authException.getMessage());
                    });

		return http.build();
	}
}


I have used In-memory data-store
@Lazy to avoid cyclic dependencies error.

Prefix which specifies hashing algorithm. Example: 
    * {noop}: use raw password
    * {bcrypt}: use Bcrypt algorithm


Expose AuthenticationManager bean to create LoginFilter (user, pass authentication).

@Bean
AuthenticationManager authenticationManager(UserDetailsService userDetailsService) {
    var daoAuth = new DaoAuthenticationProvider();
    daoAuth.setUserDetailsService(userDetailsService);
    return new ProviderManager(daoAuth);
}


Due to WebSecurityConfigurerAdapter is deprecated since version 5.7, I have created the Component-based security configuration using SecurityFilterChain

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.authorizeHttpRequests().anyRequest().authenticated();
    http.addFilterAt(loginFilter, BasicAuthenticationFilter.class);
    http.addFilterAfter(jwtAuthFilter, BasicAuthenticationFilter.class);
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    /* Custom error message */
    http.exceptionHandling()
        .accessDeniedHandler((request, response, accessDeniedException) -> {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().print(accessDeniedException.getMessage());/* Define your own message */
        }).authenticationEntryPoint((request, response, authException) -> {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().print(authException.getMessage());/* Define your own message */
        });

    return http.build();
}


The SessionCreationPolicy.STATELESS is mean Token-based authentication does not require session.
I have created the Custom error message exceptionHandling. 

For the simple message, I use lambda instead of create new class and implement AccessDeniedHandler.
You can Define your own message.

Before using JJWT Token, I have add filter by using simeple username & password login:

http.addFilterAt(loginFilter, BasicAuthenticationFilter.class)

In SecurityConfiguration, I have using spring @Autowired to inject LoginFilter class, and using @Lazy to avoid cyclic dependencies error.

@Autowired
@Lazy
private LoginFilter loginFilter;


I have injected JwtAuthFilter in order to add JJWT BasicAuthenticationFilter filter authorization.

@Autowired
private JwtAuthFilter jwtAuthFilter;

Login Filter


I prefer Filter to handle login action, you can create rest controller with login end-point instead.

public class LoginFilter extends OncePerRequestFilter {

	private final AuthenticationManager authenticationManager;

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

		var username = request.getHeader("username");
		var password = request.getHeader("password");

		var authenticated = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password)

		);
		//response.setHeader(HttpHeaders.AUTHORIZATION, username);
                /* Add jwt-token for login */
		response.setHeader(HttpHeaders.AUTHORIZATION, generateJwtAuthToken(authenticated));
	}

        private String generateJwtAuthToken(Authentication authentication) {
		
		var user = (User) authentication.getPrincipal();
		var roles = user.getAuthorities().stream()
				.map(GrantedAuthority::getAuthority)
				.collect(Collectors.joining(","));
		
		return jwtAuthHelper.generateJwtAuthToken(user.getUsername(), Map.of("roles", roles));
	}
	@Override
	protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
		var reqMethod = request.getMethod();
		var reqUri = request.getRequestURI();
		var isLogin = HttpMethod.POST.matches(reqMethod) && reqUri.startsWith("/login");

		return !isLogin;
	}

}


I have extended OncePerRequestFilter: Filter base class that aims to guarantee a single execution per requestdispatch, on any servlet container. It provides a doFilterInternalmethod with HttpServletRequest and HttpServletResponse arguments. 

Overided the shouldNotFilter method to make sure this filter only with login request.


Jwt Auth Helper


@Component
public class JwtAuthHelper {

	private final Key key;

	public JwtAuthHelper(@Value("${jwt.secret.key}") String secretKey) {
		key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
	}

	public String generateJwtAuthToken(String subject, Map<String, Object> claims) {
		return Jwts.builder().setSubject(subject).addClaims(claims)
				.setExpiration(Date.from(Instant.now().plus(5, ChronoUnit.MINUTES))).signWith(key).compact();
	}

	public Map<String, Object> parseClaims(String token) {

		return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

	}
}

I have set the Expired in 5mns since login.

SecretKey Formats

If you want to sign a JWS using HMAC-SHA algorithms and you have a secret key String or encoded byte array, you will need to convert it into a SecretKey instance to use as the signWith method argument.

I have used online random tools to generate random string for secret key.
I have set the key property in application.properties file.

jwt.secret.key=Iwr4oPxIR3tnGMDP0p8z8BXxWxk1ahdJHA3BpbESz2hpS1xLWalwMRMKSJESfLnt

A raw (non-encoded) string (e.g. a password String):


key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));

It is always incorrect to call secretString.getBytes() (without providing a charset).

However, raw password strings like this, e.g. correcthorsebatterystaple should be avoided whenever possible because they can inevitably result in weak or susceptible keys. Secure-random keys are almost always stronger. If you are able, prefer creating a new secure-random secret key instead.


Jwt Auth Helper

To create Jwt Filter (jwt authentication).

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

	private final JwtAuthHelper jwtAuthHelper;

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

		var getToken = getToken(request);
		var claims = jwtAuthHelper.parseClaims((String) getToken);

		SecurityContextHolder.getContext().setAuthentication(createJwtAuthentication(claims));
		filterChain.doFilter(request, response);

	}

	private Authentication createJwtAuthentication(Map<String, Object> claims) {

		var roles = Arrays.stream(claims.get("roles").toString().split(",")).map(SimpleGrantedAuthority::new)
				.collect(Collectors.toList());
		return new UsernamePasswordAuthenticationToken(claims.get(Claims.SUBJECT), null, roles);
	}

	private Object getToken(HttpServletRequest request) {
		return Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION))
				.filter(auth -> auth.startsWith("Bearer ")).map(auth -> auth.replace("Bearer ", ""))
				.orElseThrow(() -> new BadCredentialsException("Invalid token."));
	}

	@Override
	protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
		return request.getRequestURI().startsWith("/login");
	}
}
  

The same as in Login Filter class, I have extended OncePerRequestFilter: Filter base class that aims to guarantee a single execution per requestdispatch, on any servlet container. It provides a doFilterInternalmethod with HttpServletRequest and HttpServletResponse arguments. 

Overided the shouldNotFilter method to make sure this filter only with login request.

Claims

Claims are a JWT's 'body' and contain the information that the JWT creator wishes to present to the JWT recipient(s).

Set authenticated user to context if token is valid (no exception when parsing).

SecurityContextHolder.getContext().setAuthentication(createJwtAuthentication(claims));
    filterChain.doFilter(request, response);


Login Controller

In LoginController class, I have created with 3 end-points:
  • Welcom home page.
  • Simple user page.
  • And admin page.

@RestController
@Slf4j
public class LoginController {

	@GetMapping("/")
	public String homePage() {
		return "Home page.";
	}

	@GetMapping("/user")
	@PreAuthorize("hasRole('USER')")
	public String userPage(Authentication authentication) {
		log.info("Username={}, roles={}", authentication.getPrincipal(), authentication.getAuthorities());
		return "User page.";
	}

	@GetMapping("/admin")
	@PreAuthorize("hasRole('ADMIN')")
	public String adminPage(Authentication authentication) {
		log.info("Username={}, roles={}", authentication.getPrincipal(), authentication.getAuthorities());
		return "Admin page";
	}
}

- Enable annotation (ex: @PreAuthorize) to protect method.

- Using Authentication to access authentication object Spring auto inject Authenticated user from SecurityContext

@PreAuthorize("hasRole('USER')"): using Spring-EL expression.


Postman Request

I'm using postman tool this case.

This request is using Admin user to get Jwt Token authorization key.

Request jwt token by Admin user


In postman tool Header Authorization, we got jwt token authoriztion key after successfully reequested.

eyJhbGciOiJIUzUxMiJ9.
eyJzdWIiOiJhZG1pbiIsInJvbGVzIjoiUk9MRV9BRE1JTixST0xFX1VTRVIiLCJleHAiOjE2NjE0MTcwNjR9.
f6ANIOdcc9Qn_4NEhJYyI_fzKz13tuRfDINpzYwstWbL61vVFdAVTaJzPMHazIm4cFsGNlOTD4wfdToMLg0nRQ


Now Let's use then admin authorizatin key to access admin page.

Access Admin page by admin authorizaton jwt key


Let's see log after request access via postman, the log which we have already create in controller.

access page logging

Let's check the access token data in JSON Web Tokens - jwt.io



And when you use simple user token key to access admin page, then it will response with access denied.

access denied

That's it. Happy Coding :)


Conclusion


Hope this article was helpful.






Previous Post Next Post