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.
- Build a spring starter project with spring security and h2 dependencies
- Create a UserData class which will have user details such as name, password and grantedAuthoritiesList
- Create sql scripts which will create a user table and insert few records into it.
- Create UserDAO class which will fetch user data from h2 database
- CustomUserDetailsService which will implement spring framework’s UserDetailsService
- Create a BasicController class with a simple ‘home’ endpoint.
- Extend spring oauth2 ‘AuthorizationServerConfigurerAdapter’ class and write custom code
- 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:
- clientId can be given any name – ‘javatrainingschool’ in this example
- clientSecret can also by any name – ‘my-secret-key’ in this example
- private key and public key needs to be generated. Go to step number 8 to generate the same.
- 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.