How to Implement User Authentication and Authorization with SpringBoot and JWT
Simple steps to protect your app with Spring Security and JSON Web Token (JWT) without third party library.
If you do a quick search on how to secure REST API in SpringBoot using JWT you will find a lot of the same results. These results contain a method that involves writing a custom filter chain and using third party library for encoding and decoding JWTs.
JSON Web Tokens (JWT)
JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. In this compact form, JWTs consist of three parts separated by dots (.) which are:
Header - Consists of two properties: { "alg": "HS256", "typ": "JWT" }. alg is the algorithm that is used to encrypt the JWT.
Payload - This is where actual the data is stored (typically user data) to be sent.
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.
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
Getting started
To get started we are going to add the following dependencies in the pom.xml file into our project.
Spring Web
oAuth2 Resource Server
Spring Configuration Processor
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
In spring-boot-starter-oauth2-resource-server you will find that it includes the Spring Security Starter that contains everything you need.
Spring Security Configuration
To get started create a new class in the Config package called SecurityConfig. We will use SecurityFilterChain because WebSecurityConfigurerAdapter is deprecated after Spring Security 5.7.x. So this class will have the following configuration.
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.anyRequest ()
.authenticated ()) //1
.httpBasic (Customizer.withDefaults () ) //2
.csrf ( AbstractHttpConfigurer::disable ) //3
.sessionManagement (session -> session.sessionCreationPolicy ( SessionCreationPolicy.STATELESS)) //4
return http.build();
}
}
The user should be authenticated for any request in the application.
Spring Security’s HTTP Basic Authentication support is enabled by default. we will set in default.
Disable cross-site Request Forgery (CSRF)
Spring Security will never create a HttpSession and it will never use it to obtain the Security context.
OAuth2 Resource Server Configuration
In simple words resource server validates the token before serving or allowing resources to the client.
You can do this in your SecurityConfig class by setting .oauth2ResourceServer(). This could be a custom resource server configured or you can use the OAuth2ResourceServerConfigurer class provided by Spring Security.
We will use a custom resource server to convert and authenticate the JWT token.
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.anyRequest ()
.authenticated ()) //1
.oauth2ResourceServer (oauth2 -> oauth2.jwt (jwtCoverter ->
jwtCoverter.jwtAuthenticationConverter (jwtAuthenticationConverter())))
.httpBasic (Customizer.withDefaults () ) //2
.csrf ( AbstractHttpConfigurer::disable ) //3
.sessionManagement (session -> session.sessionCreationPolicy ( SessionCreationPolicy.STATELESS)) //4
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); // Customize based on your JWT claims
grantedAuthoritiesConverter.setAuthorityPrefix("");
// convert jwt inot Authentication object
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
A JWT can be encrypted using either a symmetric key (shared secret) or an asymmetric key (the private key of a private-public pair). Generally recommended to use asymmetric keys.
RSA Public and Private key
We are going to create public and private keys. You can do this via code also but here I'm going to create it manually in a new folder under /src/main/rescurces/certs.
# create rsa key pair
openssl genrsa -out keypair.pem 2048
# extract public key
openssl rsa -in keypair.pem -pubout -out public.pem
# create private key in PKCS#8 format
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.pem
Now we have to configure JwtDecode and JwtEncoder but before we create a new record class in the config package called RsaKeyProperties This will be used to externalize both the public and private key.
@ConfigurationProperties(prefix = "rsa")
public record RsaKeyProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey){
}
And after that add the following line into application.properties so that your application can find the key.
rsa.private-key=classpath:certs/private.pem
rsa.public-key=classpath:certs/public.pem
Next you have to enable configuration properties in your main class.
@SpringBootApplication
@EnableConfigurationProperties(SecurityConfig.RsaKeyProperties.class)
public class NotescollabApplication {
public static void main(String[] args) {
SpringApplication.run(NotescollabApplication.class, args);
}
}
Now we have to add the following lines in your SecurityConfig class.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final RsaKeyProperties rsaKeys;
public SecurityConfig(RsaKeyProperties rsaKeys) {
this.rsaKeys = rsaKeys;
}
Now let's configure the JwtDecode and JwtEncoder. Here, people usually use third-party libraries, but the Spring resource server provides a library called Nimbus Jose JWT.
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<> (new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
Create a new class called TokenService in a new package service which will use a new JwtEncoder to generate the token.
@Service
public class TokenService {
private final JwtEncoder encoder;
public TokenService(JwtEncoder encoder) {
this.encoder = encoder;
}
public String generateToken(Authentication authentication){
Instant now = Instant.now ();
String scope = authentication.getAuthorities ().stream ()
.map ( GrantedAuthority::getAuthority)
.collect( Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder ()
.issuer("self")
.issuedAt (now)
.expiresAt (now.plus(1, ChronoUnit.HOURS))
.subject (authentication.getName ())
.claim("roles", scope)
.build ();
return this.encoder.encode ( JwtEncoderParameters.from (claims)).getTokenValue ();
}
}
Now we need a Controller to test so create a LoginController in the Controller package that contains a POST API that will use token service to generate and return a token.
I assume you already have a real user. If you don't know how to get the user data from the Database and authenticate them then let me know I will add those sections here or write a separate article on that topic.
@RestController
public class LoginController {
private Logger logger = LoggerFactory.getLogger (LoginController.class);
@Autowired
TokenService tokenService;
@PostMapping("/token")
public String token(Authentication authentication){
logger.info("Token request for user: " +authentication.getName ());
String token = tokenService.generateToken (authentication);
logger.info("Token: - "+token);
return token;
}
}
Conclusion:
My whole goal was to let you know there was an easier way to secure your APIs using JWTs. If you have questions let me know in the comment section.