Spring Security with OAuth 2
What is Spring Security ?
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
OAuth2 with Spring Security
- OAuth2 is a standard for authorization.
- A high level goal of OAuth is allowing a Resource Owner to give access to a third party in a limited way, without giving away the password.
Roles and Actors in OAuth
- The Resource Owner (the user) is capable of granting access to a Resource.
- The Resource Server (the API) is the host of the protected Resources.
- The Authorization Server is capable of issuing Access Tokens to the Client.
- The Client (the front end app) is capable of making requests on behalf of the Resource Owner.
Dependencies Required for oauth 2
Git hub Url of complete project:
ssh:
git@github.com:pulkitpushkarna/spring-security-bootcamp.git
http:
https://github.com/pulkitpushkarna/spring-security-bootcamp.git
Maven Project:
https://github.com/pulkitpushkarna/spring-security-oauth2-maven
build.gradle
plugins {
id 'org.springframework.boot' version '2.2.5.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.spring-bootcamp'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.2.5.RELEASE'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.springframework.security:spring-security-test'
}
test {
useJUnitPlatform()
}
pom.xml (in case you are using maven)
<?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.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.spring-security.demo</groupId>
<artifactId>spring-security-oauth2-bootcamp-maven</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-oauth2-bootcamp-maven</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
--
- Spring Security needs an instance of Type UserDetails for authentication so we need to create that class
package com.springbootcamp.springsecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails {
private String username;
private String password;
List<GrantAuthorityImpl> grantAuthorities;
public User(String username, String password, List<GrantAuthorityImpl> grantAuthorities) {
this.username = username;
this.password = password;
this.grantAuthorities = grantAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- We also need to create a GrantedAuthority type class which will be used by our User class.
- This Authority type will be used by Spring Security to identify the Role
package com.springbootcamp.springsecurity;
import org.springframework.security.core.GrantedAuthority;
public class GrantAuthorityImpl implements GrantedAuthority {
String authority;
public GrantAuthorityImpl(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return authority;
}
}
Let's Create a UserDao to access the user
package com.springbootcamp.springsecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Repository;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Repository
public class UserDao {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
List<User> userList = Arrays.asList(
new User("user", passwordEncoder.encode("pass"), Arrays.asList(new GrantAuthorityImpl("ROLE_USER"))),
new User("admin", passwordEncoder.encode("pass"), Arrays.asList(new GrantAuthorityImpl("ROLE_ADMIN"))));
User loadUserByUsername(String username) {
User user = null;
Optional<User> userOptional = userList.stream().filter(e -> e.getUsername().equals(username)).findFirst();
if(userOptional.isPresent()){
user= userOptional.get();
}else {
throw new RuntimeException("User not found");
}
return user;
}
}
package com.springbootcamp.springsecurity;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String encryptedPassword = passwordEncoder.encode("pass");
System.out.println("Trying to authenticate user ::" + username);
System.out.println("Encrypted Password ::"+encryptedPassword);
UserDetails userDetails = userDao.loadUserByUsername(username);
return userDetails;
}
}
When we try to login spring security looks up for loadUserByUsername method in a bean of Type UserDetailsService and executes this method to return the User object by username provided in login credentiasls
Authorization Server is required to perform token related tasks like:
- Determine the type of token i.e JWT or UUID to be returned while authentication.
- Manage Validity of access token and refresh token
package com.springbootcamp.springsecurity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
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.configuration.EnableAuthorizationServer;
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.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserDetailsService userDetailsService;
public AuthorizationServer() {
super();
}
@Bean
@Primary
DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore()).userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter());
}
@Bean
JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("1234");
return jwtAccessTokenConverter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
// @Bean
// public TokenStore tokenStore() {
// return new InMemoryTokenStore();
// }
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("live-test")
.secret(passwordEncoder.encode("abcde"))
.authorizedGrantTypes("password", "refresh_token")
.refreshTokenValiditySeconds(30 * 24 * 3600)
.scopes("app")
.accessTokenValiditySeconds(7 * 24 * 60);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {
authorizationServerSecurityConfigurer.allowFormAuthenticationForClients();
authorizationServerSecurityConfigurer.checkTokenAccess("permitAll()");
}
}
JWT token
A simple high level structure of JWT Token is:
headers.payloads.signature
Resource Server performs following tasks:
- It defined password encoder which will be used for authentication.
- It also define the authorities over different resources
package com.springbootcamp.springsecurity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
AppUserDetailsService userDetailsService;
public ResourceServerConfiguration() {
super();
}
@Bean
public static BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider() {
final DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
return authenticationProvider;
}
@Autowired
public void configureGlobal(final AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(authenticationProvider());
}
@Override
public void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").anonymous()
.antMatchers("/admin/home").hasAnyRole("ADMIN")
.antMatchers("/user/home").hasAnyRole("USER")
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable();
}
}
In current Spring Boot Version authentication manager bean is not provided so we need that bean for authentication
package com.springbootcamp.springsecurity;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class AuthenticationManagerProvider extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Now let's define some end points to try oauth in main class
package com.springbootcamp.springsecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class SpringSecurityApplication {
@GetMapping("/")
public String index(){
return "index";
}
@GetMapping("/admin/home")
public String adminHome(){
return "Admin home";
}
@GetMapping("/user/home")
public String userHome(){
return "User home";
}
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
Request for login
Login Request:
http://localhost:8080/oauth/token <Post Request>
grant_type : password
client_id : live-test
username : admin
password : pass
client_secret : abcde
Access the admin home
<GET Request> http://localhost:8080/admin/home
<Header>Authorization:Bearer <access token>
If you try to access user home page with admin access token you will get access denied error
In case if your access token expires you can get a new access token with the help of refresh token
Refresh Token
Request:
http://localhost:8080/oauth/token
grant_type : refresh_token
refresh_token: <refresh_token>
client_id:<client_id>
client_secret:<client_secret>
Using UUID for authentication
- We can also go for more simpler approach of exchanging UUID token in spite of using JWT token for that we need to make following changes in AuthorizationServerConfiguration
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore()).userDetailsService(userDetailsService)
.authenticationManager(authenticationManager)
// .accessTokenConverter(accessTokenConverter())
;
}
// @Bean
// JwtAccessTokenConverter accessTokenConverter(){
// JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// jwtAccessTokenConverter.setSigningKey("1234");
// return jwtAccessTokenConverter;
// }
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
// return new JwtTokenStore(accessTokenConverter());
}
Implement logout
@Autowired
private TokenStore tokenStore;
@GetMapping("/doLogout")
public String logout(HttpServletRequest request){
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
String tokenValue = authHeader.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
}
return "Logged out successfully";
}
In case of JWT token there is no straight forward way to implement logout. Generally a blacklist is maintained to keep the track of the token which are not supposed to be used any further. There are other ways also which are subject to different project.
Spring data JPA configuration
- application.properties
- build.gradle
spring.datasource.url=jdbc:mysql://localhost/mydb
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create
server.port=8080
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
Exercise
- Implement oauth 2 using spring security and authenticate a user which is saved in database using spring data jpa.
- Grant different Roles to different users and make sure that only authorized users of a given type can access the resource.
Creating seperate resource servers in case of microservices
git@github.com:pulkitpushkarna/resource-server-maven.git
https://github.com/pulkitpushkarna/resource-server-maven
Future of OAuth 2 in spring security
- Authorization server is deprecated from Spring Boot 2.3
- The new Authorization server developed by Spring team is in experimental phase and it might take few months to make it production ready.
- So for now we have no choice but to use the old Authorization server
- https://mvnrepository.com/artifact/org.springframework.security.experimental/spring-security-oauth2-authorization-server
- https://github.com/spring-projects-experimental/spring-authorization-server
Spring Security with OAuth 2
By Pulkit Pushkarna
Spring Security with OAuth 2
- 1,644