Master java skills

Spring Boot + Spring Security + OAuth2

In this tutorial, we will implement Authorization and Resource server.

Authorization Server : This will be responsible for authenticating the resource owner and generating jwt token.

Resource Server : This will be having the resource which can be accessed using jwt token.

Step 1 – Building Authorization Server

We will follow below steps.

  1. Build a spring starter project with spring security and h2 dependencies
  2. Create a UserData class which will have user details such as name, password and grantedAuthoritiesList
  3. Create sql scripts which will create a user table and insert few records into it.
  4. Create UserDAO class which will fetch user data from h2 database
  5. CustomUserDetailsService which will implement spring framework’s UserDetailsService
  6. Create a BasicController class with a simple ‘home’ endpoint.
  7. Extend spring oauth2 ‘AuthorizationServerConfigurerAdapter’ class and write custom code
  8. Extend ‘WebSecurityConfigurerAdapter’ class and write own code

Build a spring starter project and add following 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>1.5.9.RELEASE</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.javatrainingschool</groupId>
	<artifactId>OauthSecurityApp</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>OauthSecurityApp</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</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>org.springframework.security.oauth</groupId>
			<artifactId>spring-security-oauth2</artifactId>
		</dependency>

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

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</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>
	</dependencies>

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

</project>

application.properties file

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:bootapp;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.defer-datasource-initialization=true
spring.jpa.show-sql=true

spring.h2.console.enabled=true

security.oauth2.resource.filter-order=3

Step 2 – UserData class and CustomUser class

UserData.java

package com.javatrainingschool.model;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;

public class UserData {

	private String userName;
	private String password;
	private Collection<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public Collection<GrantedAuthority> getGrantedAuthoritiesList() {
		return grantedAuthoritiesList;
	}

	public void setGrantedAuthoritiesList(Collection<GrantedAuthority> grantedAuthoritiesList) {
		this.grantedAuthoritiesList = grantedAuthoritiesList;
	}

}

Write another class CustomUser class which will extend spring’s org.springframework.security.core.userdetails.User class and use UserData class to initialize it’s super constructor

CustomUser.java

package com.javatrainingschool.model;

import org.springframework.security.core.userdetails.User;

public class CustomUser extends User {

	private static final long serialVersionUID = 1L;

	public CustomUser(UserData user) {
		super(user.getUserName(), user.getPassword(), user.getGrantedAuthoritiesList());
	}

}

Step 4 – UserDetailsDAO class

In this class, we will fetch the password from the database against a username and return the UserData with role as ‘ROLE_SYSTEMADMIN’

package com.javatrainingschool.repository;

import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Repository;

import com.javatrainingschool.model.UserData;

@Repository
public class UserDetailsDAO {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	public UserData getUserDetails(String userName) {
		
		
		Collection<GrantedAuthority> grantedAuthoritiesList = new ArrayList<>();
		String sqlQuery = "SELECT * FROM USERS WHERE USERNAME=?";
		
		List<UserData> list = jdbcTemplate.query(sqlQuery, new String[] {userName}, 
				(ResultSet rs, int rowNum) -> {
					
				UserData user = new UserData();
				user.setUserName(userName);
				user.setPassword(rs.getString("PASSWORD"));
				return user;
		});
		
		if(list.size() > 0 ) {
			GrantedAuthority grantedAuthority  = new SimpleGrantedAuthority("ROLE_SYSTEMADMIN");
			grantedAuthoritiesList.add(grantedAuthority);
			list.get(0).setGrantedAuthoritiesList(grantedAuthoritiesList);
			return list.get(0);
		}
		
		else {
			return null;
		}
	}

}

Step 5 – CustomUserDetailsService.java

This is a service class which will use DAO class and get UserData

package com.javatrainingschool.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.javatrainingschool.model.CustomUser;
import com.javatrainingschool.model.UserData;
import com.javatrainingschool.repository.UserDetailsDAO;


@Service
public class CustomUserDetailsService  implements UserDetailsService {
	
	@Autowired
	UserDetailsDAO dao;

	@Override
	public UserDetails loadUserByUsername(String userName) /* throws UsernameNotFoundException */ {
		UserData userData = null;
		CustomUser customUser = null;
		try {
			userData = dao.getUserDetails(userName);
			customUser = new CustomUser(userData);
			return customUser;
		} catch(Exception ex) {
			ex.printStackTrace();
			//throw new UserNotFoundException("User " + userName + " not found in the database.");
		}
		return customUser;
	}
}

Step 6 – WebSecurityConfigurerAdapter

Create a @configuration class to enable the Web Security. Use @EnableWebSecurity annotation for the same purpose. In this class, define the Password encoder (BCryptPasswordEncoder), and define the AuthenticationManager bean. The Security configuration class should extend WebSecurityConfigurerAdapter class.

SecurityConfiguration.java

package com.javatrainingschool.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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 com.javatrainingschool.service.CustomUserDetailsService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private CustomUserDetailsService customUserDetailsService;
	
	public PasswordEncoder encoder() {
		return new BCryptPasswordEncoder();
	}
		
	@Override
	@Autowired
	protected void configure(AuthenticationManagerBuilder authMgrBuilder) throws Exception {
		authMgrBuilder.userDetailsService(customUserDetailsService).passwordEncoder(encoder());
	}
	
	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {
		httpSecurity.authorizeRequests().anyRequest().authenticated().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
	}
	
	@Override
	public void configure(WebSecurity webSecurity) throws Exception {
		webSecurity.ignoring();
	}
	
	@Override
	@Bean
	public AuthenticationManager authenticationManagerBean() throws Exception {
		
		return super.authenticationManagerBean();
	}
}

Step 7 – AuthorizationServerConfigurerAdapter

In this step, create one class extending AuthorizationServerConfigurerAdapter class

Points to note here:

  1. clientId can be given any name – ‘javatrainingschool’ in this example
  2. clientSecret can also by any name – ‘my-secret-key’ in this example
  3. private key and public key needs to be generated. Go to step number 8 to generate the same.
  4. Adding these private and public key values in the below class variables privateKey and publicKey
package com.javatrainingschool.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class OAuth2ConfigurerAdapter extends AuthorizationServerConfigurerAdapter {

	private String clientId = "javatrainingschool";
	private String clientSecret = "my-secret-key";
	private String privateKey = "PuTTY-User-Key-File-3: ssh-rsa\r\n"
			+ "Encryption: aes256-cbc\r\n"
			+ "Comment: rsa-key-20220521\r\n"
			+ "Public-Lines: 6\r\n"
			+ "AAAAB3NzaC1yc2EAAAADAQABAAABAQCemqpIWk0qUpI65w0lZ9G7WcGaUcLtLapi\r\n"
			+ "hq2uLqrZMexs3pP6WEnH1/71OdT2H0az78V7Of0n6ZN5h8iT1Uh5W84ryAkJlBXl\r\n"
			+ "O+zbNQwqyiisPY7hIXYo4CiFda1aTADu8VS8cBrUf9H7BJVz9hIP93BJZ+cZwRNp\r\n"
			+ "ODffaKRoEfPiPeAdyqSviSO0Mz1bO7Tj99rzq51UQRu8eAxOnDzr4E3POWrcek58\r\n"
			+ "l6cJUTCaPGVIMe3VPi02QeY/rFMXlu2iPTo54vkKbmATpKWZAkJzIwrmop/RP/H2\r\n"
			+ "8rLX2nRimCSnT1SJCDDh3awlQ3cGrfJaoxwIbHPFmSdNR5oYr+7t\r\n"
			+ "Key-Derivation: Argon2id\r\n"
			+ "Argon2-Memory: 8192\r\n"
			+ "Argon2-Passes: 8\r\n"
			+ "Argon2-Parallelism: 1\r\n"
			+ "Argon2-Salt: 5f51bd13828d89cc3f7c60dcc462ea2a\r\n"
			+ "Private-Lines: 14\r\n"
			+ "WnrT/xBnUu8EAWj+LkSqUl69w7KoD1eSt88S3GUsLDEDDUnzwbQ+K6d+pZR2xhAT\r\n"
			+ "IRnXJetphYQVzVCT2JnXF/Ss2Wsra2q1OiakEr9xKEgKetq3aDvYowrgc9Rit5ci\r\n"
			+ "rPcLHMaYnutIMD/iIagIvWQQq7ywMM5ZZ9vmnwBWlahZ85cYnWdX1GSQ7LmSDczO\r\n"
			+ "Txiinb+NhHinAvPfCMjBKVF1N7hjHjQ9EoX3V8nxQFylUIQZvdvKARPkI7Rj79CJ\r\n"
			+ "RiJqXBBhSa66pVbNktmUaCuOczJUAPetuu77EXltgbJKkHdFI8f1KX35yBKXyM1k\r\n"
			+ "aNHL/+cy+WjbjBZ0sYGS1/uFTajvTlOoPCuJ9p7Z8CYC8wUr5BA8K2bcsXB8zAct\r\n"
			+ "rup0rJLRQPLl1J6uMA8011UhQDYG/boVVh+YjbkEeWrbXhzNXA1M9lbxROpKQx7T\r\n"
			+ "F92wx/uIJBXoYegEesDXyiA/TjKEmr6qmInusEK0WZEURCwWcfX5toiusE/TcrNc\r\n"
			+ "xwuATbcmXjCx04Pqu3TrcLZ/RNb8MXPzKTv6YjUzuZAv6H13WlQopCYjL72XkCCz\r\n"
			+ "HvXPxrKgkWr/ogQF2zdLc8o2X5/cgJvJEoukp9Ljh1+gU8oxu5/VIW+O8gTtImau\r\n"
			+ "W6FwR8HXiWvRtUTWhiQkQYiVpCxIUP5hp0FzlNT2InR/hzjL3mNNP8b92DIYVdsl\r\n"
			+ "xB2LSSuUc67ST3NLGPVbg/0yxWoDBTxKSt2sGnRF4GFa8pJqZ3x8NlqMYcWruxpQ\r\n"
			+ "2mrGIsChUDCdcOGHd8Fo9z+oXT7b8koNE836jWyEakTVC1nhZ0OE158cqc/9wK3b\r\n"
			+ "by6wHIix9eU4Y5ma6gdYcV4rzeSsWan4z8lYmKZDqNm73HGsmvdci0/wfFRol/9D\r\n"
			+ "Private-MAC: d7c114b9471823f6c3bd96237742ed8d8e7be4912745da64d8878ed8be7f76fb\r\n"
			+ "";
	private String publicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCemqpIWk0qUpI65w0lZ9G7WcGaUcLtLapihq2uLqrZMexs3pP6WEnH1/71OdT2H0az78V7Of0n6ZN5h8iT1Uh5W84ryAkJlBXlO+zbNQwqyiisPY7hIXYo4CiFda1aTADu8VS8cBrUf9H7BJVz9hIP93BJZ+cZwRNpODffaKRoEfPiPeAdyqSviSO0Mz1bO7Tj99rzq51UQRu8eAxOnDzr4E3POWrcek58l6cJUTCaPGVIMe3VPi02QeY/rFMXlu2iPTo54vkKbmATpKWZAkJzIwrmop/RP/H28rLX2nRimCSnT1SJCDDh3awlQ3cGrfJaoxwIbHPFmSdNR5oYr+7t rsa-key-20220521";

	@Autowired
	@Qualifier("authenticationManagerBean")
	private AuthenticationManager authenticationManager;

	public JwtAccessTokenConverter tokenEnhancer() {

		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey(privateKey);
		converter.setVerifierKey(publicKey);
		return converter;

	}

	@Bean
	public JwtTokenStore tokenStore() {
		return new JwtTokenStore(tokenEnhancer());
	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager).tokenStore(tokenStore())
				.accessTokenConverter(tokenEnhancer());
	}

	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.inMemory().withClient(clientId).secret(clientSecret).scopes("read", "write")
				.authorizedGrantTypes("password", "refresh_token").accessTokenValiditySeconds(20000)
				.refreshTokenValiditySeconds(20000);
	}

}

Step 8 – Generate private/public key

Click here to generate private/public ssh keys using puttygen

Step 9 – Add the below property for spring boot version greater than 1.5

security.oauth2.resource.filter-order=3

Step 10 – SQL Scripts

Write two scripts as below and keep them in the src/main/resources folder to build the database tables

schema.sql

create table users (id int primary key, username varchar(100), password varchar(100));

data.sql

Please note that the passwords in the below queires are in Bcrypt hash encrypted. Use any online tool to encode these passwords. Here plain text password is ‘PASSWORD’ all in capital letters.

We have used ‘https://bcrypt-generator.com/’ to generate hash. No of rounds have been chosen as 4.

INSERT INTO USERS (ID, USERNAME,PASSWORD) VALUES (
1, '[email protected]','$2a$04$57/0V29aDffVocoBhsfE7.B/YNND7XXDXIqNG2lSInuTEZFJcKdEq');

INSERT INTO USERS (ID, USERNAME,PASSWORD) VALUES (
2, '[email protected]','$2a$04$57/0V29aDffVocoBhsfE7.B/YNND7XXDXIqNG2lSInuTEZFJcKdEq');

Step 11 – @EnableAuthorizationServer

Lastly, we need to add @EnableAuthorizationServer annotation on main class

package com.javatrainingschool;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

@SpringBootApplication
@EnableAuthorizationServer
public class SpringBootWithOauth2Application {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootWithOauth2Application.class, args);
	}

}

Step 12 – Run the authorization server and generate JWT token

After the service is run, use postman to prepare below request

In the Headers, add Authorization and Content-Type keys. Value for ‘Authorization’ has to be formed like below.

Open website ‘https://www.base64encode.org/’ to encode client-id and client-secret. Below window will appear. Form the client-id and client-secret in the following way – client-id:client-secret

Whatever encoded value is got, put it against Authorization key in postman after Basic as shown in the below diagram.

Next, in the Body tab, add below details

Now, hit the Send button and get the output.