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
Roles and Actors in OAuth
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>
--
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;
}
}
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:
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:
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
@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
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
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