Spring Security With JWT and Oauth2 With Spring Boot
Introduction
In this article we will see example of how to secure a spring boot rest application with Spring Boot2, Spring Security, Oauth2, and JWT token.
1. Source Code Repository
The code used in this article is available in this repository GitHub.
2. Dependencies
<parent> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-parent</artifactId> | |
<version>2.1.3.RELEASE</version> | |
<relativePath/> <!-- lookup parent from repository --> | |
</parent> | |
<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-data-jpa</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-data-rest</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
<optional>true</optional> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-test</artifactId> | |
<scope>test</scope> | |
</dependency> | |
<dependency> | |
<groupId>mysql</groupId> | |
<artifactId>mysql-connector-java</artifactId> | |
</dependency> | |
</dependencies> | |
<build> | |
<plugins> | |
<plugin> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-maven-plugin</artifactId> | |
</plugin> | |
</plugins> | |
</build> | |
</project> |
2.1. Versions
- Spring Boot: 2.1.3.RELEASE
- Java: 1.8
3. Entity Class
We create two Entity Classes, one is User and another is Role. User entity class contains user details like name, password, roles details, and role entity contains the role details. One User may have many roles hence we to create many-to-many relationship between User and Role Entity.
3.1. User Entity.java
Entity class that contains the user details.
package com.example.springrestjwt.controller.entity; | |
import java.util.Set; | |
import javax.persistence.CascadeType; | |
import javax.persistence.Entity; | |
import javax.persistence.FetchType; | |
import javax.persistence.GeneratedValue; | |
import javax.persistence.GenerationType; | |
import javax.persistence.Id; | |
import javax.persistence.JoinColumn; | |
import javax.persistence.JoinTable; | |
import javax.persistence.ManyToMany; | |
import lombok.AllArgsConstructor; | |
import lombok.Builder; | |
import lombok.Getter; | |
import lombok.NoArgsConstructor; | |
import lombok.Setter; | |
@Setter | |
@Getter | |
@NoArgsConstructor | |
@AllArgsConstructor | |
@Entity | |
@Builder | |
public class User { | |
@Id | |
@GeneratedValue(strategy = GenerationType.AUTO) | |
private Long id; | |
private String username; | |
private String password; | |
private String firstName; | |
private String lastName; | |
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) | |
@JoinTable( | |
name = "user_role", | |
joinColumns = | |
@JoinColumn(name = "user_id", referencedColumnName = "id"), | |
inverseJoinColumns = | |
@JoinColumn(name = "role_id", referencedColumnName = "id") | |
) | |
private Set<Role> roles; | |
} |
3.2. Role.java
Entity class that contains the user role data.
package com.example.springrestjwt.controller.entity; | |
import javax.persistence.Entity; | |
import javax.persistence.GeneratedValue; | |
import javax.persistence.GenerationType; | |
import javax.persistence.Id; | |
import lombok.AllArgsConstructor; | |
import lombok.Builder; | |
import lombok.Getter; | |
import lombok.NoArgsConstructor; | |
import lombok.Setter; | |
@Entity | |
@Getter | |
@Setter | |
@NoArgsConstructor | |
@AllArgsConstructor | |
@Builder | |
public class Role { | |
@Id | |
@GeneratedValue(strategy = GenerationType.IDENTITY) | |
private Long id; | |
private String roleName; | |
private String description; | |
} |
4. Create CRUD Repository
We create spring CRUD repository class, to access user credentials from the database.
Note: we create repository only for User entity class, because in User entity class we specify Many to Many relationship between User and Role entity, and @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
at line number 35 in User.java, will ensure that when new record save in User entity it will also save in Role entity.
4.1. UserRepository.java
public interface UserRepository extends CrudRepository<User, Long> { | |
Optional<User> findOneByUsername(String username); | |
} |
5. Create a Spring boot Initialize class
This will boot the spring application.
@SpringBootApplication | |
public class SpringRestJwtApplication { | |
public SpringRestJwtApplication(UserRepository userRepository) { | |
this.userRepository = userRepository; | |
} | |
} |
Now we need to create initial data for users create data.sql file and put it into resource directory.
-- create user data | |
insert into user (id, first_name, last_name, password, username) values | |
(1, 'vikas','verma','$2a$04$lZj8KgBFkcPwgRWjH8DwBeCIR7HE6AsIZqTXu2VyeEw5sYLySNAGe', 'vikas'); | |
insert into user (id, first_name, last_name, password, username) values | |
(2, 'james','james','$2a$04$P2GbxPDh1MYNYyNn/bj.4.QxwDC2jze0xPQF4u6/cNpdkrPq3OdPy', 'james'); | |
-- create role data | |
insert into role (id, description, role_name) values (1, 'admin role', 'ADMIN'); | |
insert into role (id, description, role_name) values (2, 'user role', 'USER'); | |
-- create foreign key relation data | |
insert into user_role(user_id, role_id) values (1, 2); | |
insert into user_role(user_id, role_id) values (2, 1); | |
insert into user_role(user_id, role_id) values (2, 2); |
6. Controller class
Now create controller class to access the resources.
package com.example.springrestjwt.controller; | |
import com.example.springrestjwt.controller.entity.User; | |
import com.example.springrestjwt.controller.repo.UserRepository; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestMethod; | |
import org.springframework.web.bind.annotation.RestController; | |
@RestController | |
@RequestMapping("/app") | |
public class AppController { | |
private final UserRepository userRepository; | |
public AppController(UserRepository userRepository) { | |
this.userRepository = userRepository; | |
} | |
@RequestMapping( value = "/hello", method = RequestMethod.GET) | |
public String hello() { | |
return "Hello!!!"; | |
} | |
@GetMapping("/listAll") | |
public Iterable<User> findAll() { | |
return userRepository.findAll(); | |
} | |
} |
Now our basic application is created, just the start the application server to see the output.
use mvn spring-boot:run
to run the application
After the server start just curl the below URL to check if everything is working correct or not
curl http://localhost:8080/app/listAll
Response -
[
{
"id": 1,
"username": "vikas",
"password": "$2a$04$lZj8KgBFkcPwgRWjH8DwBeCIR7HE6AsIZqTXu2VyeEw5sYLySNAGe",
"firstName": "vikas",
"lastName": "verma",
"roles": [
{
"id": 1,
"roleName": "ADMIN",
"description": "admin role"
}
]
},
{
"id": 2,
"username": "james",
"password": "$2a$04$P2GbxPDh1MYNYyNn/bj.4.QxwDC2jze0xPQF4u6/cNpdkrPq3OdPy",
"firstName": "james",
"lastName": "james",
"roles": [
{
"id": 1,
"roleName": "ADMIN",
"description": "admin role"
},
{
"id": 2,
"roleName": "USER",
"description": "user role"
}
]
}
]
7. Authentication and Authorization
Now since our basic application is working, it’s time to add authentication and authorization to our application.
Add Dependencies for the Oauth2 Security
<dependency> | |
<groupId>org.springframework.security.oauth.boot</groupId> | |
<artifactId>spring-security-oauth2-autoconfigure</artifactId> | |
<version>2.1.3.RELEASE</version> | |
</dependency> |
7.1. Set the properties related to security
security: | |
encoding-strength: 256 | |
signing-key: yWWcHrTa | |
security-realm: Spring Rest JWT | |
jwt: | |
client-id: client | |
client-secret: $2a$04$Ck8Y6nyCIkN...8pXOz4d.PY6PDtFjbcEk0oiOXL39GElUteq6.eS | |
grant-type: password | |
scope-read: read | |
scope-write: write | |
resource-ids: resource |
The detail of the properties is as below -
security.encoding-strength=256
size of the encoding.security.signing-key
is the sign key used to encode the token.security.security-realm
realm of the authentication. seesecurity.jwt.client-id=client
Client Id.security.jwt.client-secret
It should be BCrypt format you can use this tool to encode any string to BCryptsecurity.jwt.grant-type=password
Grant type, it can be “password”, “refresh-token”.security.jwt.scope-read=read
read scope.security.jwt.scope-write=write
write scope.security.jwt.resource-ids=resource
resource id.
7.3. JWTConfigProperties.java
Create a class to access the properties set in application property file.
package com.example.springrestjwt.config.param; | |
import org.springframework.boot.context.properties.ConfigurationProperties; | |
import org.springframework.stereotype.Component; | |
import lombok.Data; | |
@ConfigurationProperties(prefix = "security") | |
@Component | |
@Data | |
public class JWTConfigProperties { | |
private String signingKey; | |
private int encodingStrength; | |
private String securityRealm; | |
private JWT jwt; | |
@Data | |
public static class JWT { | |
private String clientId; | |
private String clientSecret; | |
private String grantType; | |
private String scopeRead; | |
private String scopeWrite; | |
private String resourceIds; | |
} | |
} |
7.4. UserDetailServiceImpl.java
To access the user credentials from database, we need to implement interface UserDetailsService. It is used throughout the framework as a user DAO. The interface requires only one read-only method, which simplifies support for new data-access strategies.
package com.example.springrestjwt.controller.repo.service; | |
import com.example.springrestjwt.controller.entity.User; | |
import com.example.springrestjwt.controller.repo.UserRepository; | |
import org.springframework.security.core.authority.SimpleGrantedAuthority; | |
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 java.util.List; | |
import java.util.stream.Collectors; | |
@Service | |
public class UserDetailsServiceImpl implements UserDetailsService { | |
private final UserRepository userRepository; | |
public UserDetailsServiceImpl(UserRepository userRepository) { | |
this.userRepository = userRepository; | |
} | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
User user = userRepository.findOneByUsername(username) | |
.orElseThrow(() -> new UsernameNotFoundException(username + " not found.")); | |
List<SimpleGrantedAuthority> collect = user.getRoles().stream() | |
.map(roles -> new SimpleGrantedAuthority(roles.getRoleName())) | |
.collect(Collectors.toList()); | |
return new org.springframework.security.core.userdetails.User(user.getUsername(), | |
user.getPassword(), collect); | |
} | |
} |
7.5. WebSecurityConfig.java
Configure the web security by extending the WebSecurityConfigurerAdapter class. It provides a convenient base class for creating a WebSecurityConfigurer instance. The implementation allows customization by overriding methods.
@Override | |
protected void configure(AuthenticationManagerBuilder auth) throws Exception { | |
auth.userDetailsService(userDetailsService) | |
.passwordEncoder(encoder()); | |
} | |
@Bean | |
public BCryptPasswordEncoder encoder(){ | |
return new BCryptPasswordEncoder(); | |
} | |
Step#1
Configure UserDetailsService to AuthenticationManager class, and add a password encoder.
*Autowire the userDetailsService instance.
@Override | |
@Bean | |
protected AuthenticationManager authenticationManager() throws Exception { | |
return super.authenticationManager(); | |
} |
Step#2
Now create AuthenticationManager bean
@Override | |
@Bean | |
protected AuthenticationManager authenticationManager() throws Exception { | |
return super.authenticationManager(); | |
} |
Since we are using password as grant-type we need to provide the AuthenticationManager implementation.
Step#3
Configure the JWT Token related beans -
@Bean | |
public TokenStore tokenStore() { | |
return new JwtTokenStore(accessTokenConverter()); | |
} | |
@Bean | |
public JwtAccessTokenConverter accessTokenConverter() { | |
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); | |
jwtAccessTokenConverter.setSigningKey(jwtConfigProperties.getSigningKey()); | |
return jwtAccessTokenConverter; | |
} | |
@Bean | |
@Primary | |
public DefaultTokenServices tokenServices() { | |
DefaultTokenServices tokenServices | |
= new DefaultTokenServices(); | |
tokenServices.setTokenStore(tokenStore()); | |
tokenServices.setSupportRefreshToken(true); | |
return tokenServices; | |
} |
tokenStore()
Create a new JwtTokenStore
with this token enhancer(accessTokenConverter
.
accessTokenConverter()
Configure JWT signing key. It can be either a simple MAC key or an RSA key. RSA keys should be in OpenSSH format, as produced by ssh-keygen.
tokenServices()
it is default implimentation of the tokenServices, it used to configure the resource related properties. In the tokenServices we used the default implementation of the TokeService interface, and set persistence strategy for token storage. the tokenStore, and configure to support the refresh token.
Now enable Web Security in the application, and Global Method security-
@Configuration | |
@EnableWebSecurity | |
@Order(SecurityProperties.BASIC_AUTH_ORDER) | |
@EnableGlobalMethodSecurity(prePostEnabled = true) |
@EnableWebSecurity
: It allow the Spring Security configuration defined in any WebSecurityConfigurer or more likely by extending the WebSecurityConfigurerAdapter base class and overriding individual methods.
@EnableGlobalMethodSecurity(prePostEnabled = true)
: Enables Spring Security global method security.
@Order
: Define the order the security filter chain. The priority of the WebSecurityConfigurerAdapter is more than the resourceServerConfigurationAdapter, hence we re-define the order WebSecurityConfigurerAdapter
8. Define Authentication Manager
Create a class AuthenticationServerConfig.java
that extends AuthorizationServerConfigurerAdapter.java
to provides the default implementation for the AuthorizationServer. It is used to register the clients that can access the resource of the application, and also endpoints of the authorization server.
8.1. Configure Clients of the application
The code is self-explanatory, we jest configure the client details that are stored in memory. All the clients’ details are store in the application.property file.
8.2. Configure the Authorization endpoints
@Configuration | |
@EnableAuthorizationServer | |
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter { | |
private final JWTConfigProperties jwtConfigProperties; | |
private final JwtAccessTokenConverter jwtAccessTokenConverter; | |
private final TokenStore tokenStore; | |
private final AuthenticationManager authenticationManager; | |
public AuthenticationServerConfig(JWTConfigProperties jwtConfigProperties, JwtAccessTokenConverter jwtAccessTokenConverter, TokenStore tokenStore, AuthenticationManager authenticationManager) { | |
this.jwtConfigProperties = jwtConfigProperties; | |
this.jwtAccessTokenConverter = jwtAccessTokenConverter; | |
this.tokenStore = tokenStore; | |
this.authenticationManager = authenticationManager; | |
} | |
@Override | |
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { | |
TokenEnhancerChain tokenEnhancerChain = | |
new TokenEnhancerChain(); | |
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter)); | |
endpoints.tokenStore(tokenStore) | |
.authenticationManager(authenticationManager) | |
.tokenEnhancer(tokenEnhancerChain); | |
} |
*Autowire the jwtConfigProperties, jwtAccessTokenConverter, tokenStore and authenticationManager instance.
Configure the tokenstore, and tokenEnhancer with the AuthorizationServerEndpointsConfigurer
Configure the TokenStore, authenticationManager, and tokenEnhancerChain in the AuthorizationServerEndpointsConfigurer class.
8.3. Enable Authorization Server in the current application context
@Configuration | |
@EnableAuthorizationServer | |
public class AuthenticationServerConfig extends AuthorizationServerConfigurerAdapter { |
9. Resource Server Configuration
Create a class ResourceServerConfig.java
that extends ResourceServerConfigurerAdapter.java
This class is used to configure the resourceIds, and the http request URLs that are allowed to access the application, and the URLs that need to be authenticated.
9.1. Configure Resource Id
@Override | |
public void configure(ResourceServerSecurityConfigurer resources) throws Exception { | |
resources.tokenServices(defaultTokenServices) | |
.resourceId(jwtConfigProperties.getJwt().getResourceIds()); | |
} |
*Autowire the jwtConfigProperties, defaultTokenService bean instances.
Resource id is configured in application.property
file. defaultTokenService
is created in class WebSecurityConfig
.
9.2. Configure HttpSecurity
@Override | |
public void configure(HttpSecurity http) throws Exception { | |
http.requestMatchers().and() | |
.authorizeRequests() | |
.antMatchers("/app/**").authenticated(); | |
} |
This configuration says, authenticate all request that contains "/app/**"
in there URL.
9.3. Enable the resource server configuration
@Configuration | |
@EnableResourceServer | |
public class ResourceServerConfig extends ResourceServerConfigurerAdapter { |
10. Modify the controller class
Finally set the roles that can access the application using @PostAuthorize
annotation
package com.example.springrestjwt.controller; | |
import com.example.springrestjwt.controller.entity.User; | |
import com.example.springrestjwt.controller.repo.UserRepository; | |
import org.springframework.security.access.prepost.PostAuthorize; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestMethod; | |
import org.springframework.web.bind.annotation.RestController; | |
@RestController | |
@RequestMapping("/app") | |
public class AppController { | |
private final UserRepository userRepository; | |
public AppController(UserRepository userRepository) { | |
this.userRepository = userRepository; | |
} | |
@RequestMapping( value = "/hello/user", method = RequestMethod.GET) | |
@PostAuthorize("hasAuthority('USER')") | |
public String helloUser() { | |
return "Hello User!!!"; | |
} | |
@RequestMapping( value = "/hello/admin", method = RequestMethod.GET) | |
@PostAuthorize("hasAuthority('ADMIN')") | |
public String helloAdmin() { | |
return "Hello Admin!!!"; | |
} | |
@GetMapping("/listAll") | |
@PostAuthorize("hasAuthority('ADMIN')") | |
public Iterable<User> findAll() { | |
return userRepository.findAll(); | |
} | |
} |
11. Run The application
11.1 Run MySQL Database
Run below command to run the mysql db in docker
docker-compose -f docker/docker-compose up
Now all the configurations has done, now we can run the application using below maven command in terminal
mvn spring-boot:run
11.1 Access Resource with User credentials
After server started, We can get the access token. Run below command to get the access token-
11.1.1. Get Access Token
export TOKEN=`curl -s -X POST http://localhost:8080/oauth/token \ | |
-H 'authorization: Basic Y2xpZW50OnNlY3JldA==' \ | |
-d 'grant_type=password&username=vikas&password=vikas' | jq -r .access_token` | |
curl -H "authorization: Bearer $TOKEN" http://localhost:8080/app/hello/user |
“Y2xpZW50OnNlY3JldA==” is Base64 encoded client-id:secret. you can encode and decode in base64 format using this site. The format of client-id and client-secret should be ‘client-id:secret’.
Result-
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiXSwidXNlcl9uYW1lIjoidmlrYXMiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTUyMDA5ODM2LCJhdXRob3JpdGllcyI6WyJBRE1JTiJdLCJqdGkiOiI3YWJkOThhNC1hNjM4LTRmYmQtOWYzMC0zZWJiNDQ0M2FhMTciLCJjbGllbnRfaWQiOiJjbGllbnQifQ.P0UgLpAuswCs8iHsxT4q23TI1infsIMqZ1YtMlbfWe8",
"token_type": "bearer",
"expires_in": 43199,
"scope": "read write",
"jti": "7abd98a4-a638-4fbd-9f30-3ebb4443aa17"
}
11.2 Access Resource
11.2.1. Get the access token
curl -X POST \ | |
http://localhost:8080/oauth/token \ | |
-H 'authorization: Basic Y2xpZW50OnNlY3JldA==' \ | |
-H 'cache-control: no-cache' \ | |
-H 'content-type: application/x-www-form-urlencoded' \ | |
-d 'grant_type=password&username=admin&password=password' |
Result-
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzb3VyY2UiXSwidXNlcl9uYW1lIjoidmlrYXMiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTUyMDA5ODM2LCJhdXRob3JpdGllcyI6WyJBRE1JTiJdLCJqdGkiOiI3YWJkOThhNC1hNjM4LTRmYmQtOWYzMC0zZWJiNDQ0M2FhMTciLCJjbGllbnRfaWQiOiJjbGllbnQifQ.P0UgLpAuswCs8iHsxT4q23TI1infsIMqZ1YtMlbfWe8"
"token_type": "bearer",
"expires_in": 43199,
"scope": "read write",
"jti": "7abd98a4-a638-4fbd-9f30-3ebb4443aa17"
}
11.2.2. Access Admin Resources
To access the resource that required admin privilege you need to get the access token with user james/james, because this user have admin privilege
export TOKEN=`curl -s -X POST http://localhost:8080/oauth/token \ | |
-H 'authorization: Basic Y2xpZW50OnNlY3JldA==' \ | |
-d 'grant_type=password&username=james&password=james' | jq -r .access_token` | |
echo Access admin resources - | |
curl -H "authorization: Bearer $TOKEN" http://localhost:8080/app/hello/admin | |
echo Access user resources - | |
curl -H "authorization: Bearer $TOKEN" http://localhost:8080/app/hello/user |
Result-
Access admin resources -
Hello Admin!!!
Access user resources -
Hello User!!!
11.2.3. Access the User resources
export TOKEN=`curl -s -X POST http://localhost:8080/oauth/token \ | |
-H 'authorization: Basic Y2xpZW50OnNlY3JldA==' \ | |
-d 'grant_type=password&username=vikas&password=vikas' | jq -r .access_token` | |
curl -H "authorization: Bearer $TOKEN" http://localhost:8080/app/hello/user |
Result-
Hello User!!!
That’s all for the Authentication and Authorization with Oauth2. You can access the resource from GitHub.