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
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: registered, public, 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
- Spring Boot 2.2.x
- Spring Security
- Spring Data JPA
- Java JWT library
- 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>
- Create an empty spring boot project with spring boot, security dependencies and add dependencies as shown above
- 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
- This project uses in H2 memory database as database. And
schema.sql
file in src/main/resources directory creates the required tables anddata.sql
file inserts sample users and roles - Run the spring boot class JwtSpringSecurityApplication to start the application
- Now go to http://localhost:8080/h2-console to see the database and enter the credentials(shown below)
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
Conclusion
Code is uploaded to Github for reference, Happy Coding 🙂