JWT authentication in Spring Security and Angular

This blog post explains the JSON web token(JWT) authentication using Spring Security, Spring Boot, Spring Data and Angular. Source code uploaded to Github repository

Image by MasterTux from Pixabay

Introduction

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

JWT Token structure

In its compact form, JSON Web Tokens consist of three parts separated by dots (.), which are:

  • Header
  • Payload
  • Signature

Therefore, a JWT typically looks like the following.

xxxxx.yyyyy.zzzzz

Header

The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.

For example:

{
"alg": "HS256",
"typ": "JWT"
}

Then, this JSON is Base64Url encoded to form the first part of the JWT.

Payload

The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registeredpublic, and private claims.

Signature

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that. Please read https://jwt.io/introduction/ for detailed workflow and description

Technologies

  1. Spring Boot 2.2.x
  2. Spring Security
  3. Spring Data JPA
  4. Java JWT library
  5. H2 embedded database

Implementation

There are many open-source JWT implementations available for all languages. In this blog post, we use Java jjwt library in this blog post.

pom.xml


<?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.3.3.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.pj</groupId>
	<artifactId>JwtSpringSecurity</artifactId>
	<version>1.0.0</version>
	<name>JwtSpringSecurity</name>
	<description>Spring Security with JWT authentication</description>

	<properties>
		<java.version>11</java.version>
		<jjwt.version>0.11.2</jjwt.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<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>io.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<version>${jjwt.version}</version>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-impl</artifactId>
			<version>${jjwt.version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
			<version>${jjwt.version}</version>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</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>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>
  1. Create an empty spring boot project with spring boot, security dependencies and add dependencies as shown above
  2. Create UserController class that accepts username and password parameters and authenticates users through UsernamePasswordAuthenticationToken class
@PostMapping(value = {"/authenticate","/login"})
public Object loginUser(@RequestParam String username, @RequestParam String password)
{
Authentication authentication=authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(username, password));

return mapUserAndReturnJwtToken(authentication,true);
}

4. Create SecurityConfig class(shown below) that defines standard Spring Security configuration for the project.

5. Method public void configure(HttpSecurity http)allows all requests to login URLs since authentication is being done manually through in UserController class

UserController.java


package com.pj.jwt.security;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
	private final JwtRequestFilter jwtRequestFilter;
	private final CustomUserDetailsService customUserDetailsService;

	public SecurityConfig(JwtRequestFilter jwtRequestFilter, CustomUserDetailsService customUserDetailsService)
	{
		this.jwtRequestFilter = jwtRequestFilter;
		this.customUserDetailsService = customUserDetailsService;
	}

	@Override
	public void configure(WebSecurity webSecurity)
	{
		webSecurity.ignoring().antMatchers("/static/**");
	}

	@Override
	public void configure(HttpSecurity http) throws Exception
	{
		http.authorizeRequests()
				.antMatchers("/api/v1/user/login","/api/v1/user/authenticate", "/api/v1/user/logout","/h2-console/**").permitAll()
				.anyRequest().authenticated()
				.and()
				.httpBasic()
				.and()
				.logout().invalidateHttpSession(true).clearAuthentication(true)
				.and().headers().frameOptions().sameOrigin();

		http.csrf().disable();
		http.cors();
		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
	}

	@Override
	public void configure(AuthenticationManagerBuilder auth) throws Exception
	{
		auth.authenticationProvider(getDaoAuthenticationProvider());
	}

	@Bean
	public CustomDaoAuthenticationProvider getDaoAuthenticationProvider()
	{
		CustomDaoAuthenticationProvider daoAuthenticationProvider = new CustomDaoAuthenticationProvider();
		daoAuthenticationProvider.setUserDetailsService(customUserDetailsService);
		daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
		return daoAuthenticationProvider;
	}

	@Bean
	public PasswordEncoder passwordEncoder()
	{
		return new BCryptPasswordEncoder(12);
	}

	//Cors filter to accept incoming requests
	@Bean
	CorsConfigurationSource corsConfigurationSource()
	{
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.applyPermitDefaultValues();
		configuration.setAllowedMethods(Collections.singletonList("*"));
		configuration.setAllowCredentials(true);
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}

	@Bean(BeanIds.AUTHENTICATION_MANAGER)
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
}

6. JwtUtil class is responsible to issue and validate the tokens. In particular, createToken() method creates token with 24 hours expiration and sign with custom key from properties file(make sure keep this long and hard to guess)

private String createToken(Map<String, Object> claims, String subject)
	{
		return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
				.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
				.signWith(Keys.hmacShaKeyFor(coreProperties.getJwtSecret().getBytes()), SignatureAlgorithm.HS512).compact();
	}
	
public boolean validateToken(String token, UserDetails userDetails)
	{
		final String username = extractUsername(token);
		return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
	}

7. validateToken() method validates the supplied token by validating the expiration date

8. Create JwtRequestFilter filter that intercepts all requests from client and looks for Bearer token. If the token is present, extract the username and validate the expiration date.

9. If the token is valid, create new UsernamePasswordAuthenticationToken and set userDetails and userDetails authorities. Save this as Spring Security authentication object, which tells spring security that this user is authenticated and continue with security chain.

10. In order for this filter to work, in SecurityConfig add it before UsernamePasswordAuthenticationFilter

http.addFilterBefore(jwtRequestFilter,UsernamePasswordAuthenticationFilter.class);

11. To show case the demo, generated Angular project with 2 pages. Login and Home page

Testing

  1. This project uses in H2 memory database as database. And schema.sql file in src/main/resources directory creates the required tables and data.sql file inserts sample users and roles
  2. Run the spring boot class JwtSpringSecurityApplication to start the application
  3. Now go to http://localhost:8080/h2-console to see the database and enter the credentials(shown below)
h2 in memory database

4. Check the existing users with the query SELECT * FROM CORE_USER. If you do not see any results, copy SQL statements from data.sql in src/main/resources and execute it

5. Now go to src/webapp directory and install all dependencies

$ npm install

6. Start the Angular application with the following command

$ npm run start --watch

7. Now go to http://localhost:4200 and you will be redirected to login page

8. Enter credentials admin/admin and you will be redirected to home page.

9. In home page, during initial load we use the token from previous page(stored as cookie) and get user information by presenting that token to spring boot application(just to make sure that token is valid)

9. See the network tab for JWT token with expiration date

Home page

Conclusion

Code is uploaded to Github for reference, Happy Coding 🙂

Pavan Kumar Jadda
Pavan Kumar Jadda
Articles: 36

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.