Securing Spring Boot REST API with JSON Web Token and JDBC Token Store
In this tutorial, we will learn how to secure Spring Boot REST API with OAuth 2.0 and JSON Web Token (JWT).
Let’s begin by understanding what is JWT and OAuth. OAuth 2.0 defines a protocol, that is, it specifies how tokens are transferred. JWT defines the token format.
OAuth 2.0 and JWT authentication have similar appearance when it comes to the stage where the Client presents the token to the Resource Server. But JWT authentication is not a standard and does not specify how the Client obtains the token in the first place. That is where the perceived complexity of OAuth comes from. It also defines various ways in which the Client can obtain an access token from something that is called an Authorization Server.
Choosing JWT to secure your API endpoints is a great choice because it ensures a stateless exchange of tokens between the client and the server. Also, it is compact and URL-safe.
System Components and Description
The following system components are involved:
- Client : It can be any Web service consumer on any platform. Simply put it can be another WebService, UI application or Mobile platform, which wants to read-write data in a secure way with an Application.
- Authorization Server : Validates user credentials, and issues tokens. It issues tokens depending upon the different grant types. The most common OAuth 2.0 grant types are listed below:
We will use Password grant type in this article.
3. Resource Server : The REST API endpoints which we want to secure.
We will use JdbcTokenStore to store all the tokens issued to various clients. In the event that we require to revoke any token issued to any user, we can just delete the token from the database. Also, in case of Server restart the issued tokens remain valid till the expiry time.
Authentication Workflow
- The user sends a request to an Authorization Server to procure a token presenting his credentials.
- The Authorization server validates the credentials and sends back a bearer and a refresh token.
- With every subsequent request, the user has to provide the bearer token, which the server will validate.
- If the bearer token expires, then the refresh token will be used to fetch new tokens.
Now let’s get started with the implementation.
Implementing the Workflow
We will use Spring Boot 1.5.9.RELEASE project with following dependencies:
spring-boot-starter-data-jpa
postgresql
spring-boot-starter-web
spring-boot-starter-security
spring-security-jwt
spring-security-oauth2
Step 1: Configure Spring Security
We need minimal customizations to get started because of Spring Boot’s auto-configuration.
Include the following properties in the Spring Boot application’s configuration file application.properties.
security.oauth2.resource.filter-order=3
security.signing-key=MaYzkSjmkzPC57L
security.encoding-strength=256
security.security-realm=Spring Boot JWT Example Realm
spring.application.name=jwt-springBoot-oauthspring.datasource.url=jdbc:postgresql://localhost:5432/auth?ApplicationName=authentication
spring.datasource.username=${DATABASE_USERNAME:postgres}
spring.datasource.password=${DATABASE_PASSWORD:postgres}
spring.datasource.driver-class-name=org.postgresql.Driver
Next, let’s create our own security config class by extending WebSecurityConfigurationAdapter and include the following properties.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${security.signing-key}")
private String signingKey;
@Value("${security.encoding-strength}")
private Integer encodingStrength;
@Value("${security.security-realm}")
private String securityRealm;
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.realmName(securityRealm)
.and()
.csrf()
.disable();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(jdbcTemplate.getDataSource());
}
@Bean
@Primary
//Making this primary to avoid any accidental duplication with another token service instance of the same name
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
Some of the core components of the security config class have been explained below.
@EnableWebSecurity : Enables spring security and hints Spring Boot to apply all the sensitive defaults.
@EnableGlobalMethodSecurity: Allows us to have method level access control. It provides AOP security on methods, some of annotations it will enable are PreAuthorize
PostAuthorize
.
TokenStore and JwtAccessTokenConverter beans: A JdbcTokenStore bean is needed by the authorization server and to enable the resource server to decode access tokens . JwtAccessTokenConverter bean must be used by both Authorization and Resource servers. In this case, we are using a symmetric key signing.
BCryptPasswordEncoder bean : This is used for password encryption and decryption. We can use custom implementation of encryptor if required.
Encoding: SHA-256 is used to encode passwords. This is set in encoding-strength application property.
Realm: The security realm name is defined in securityRealm property. This name is arbitrary. A realm is basically all that define our security solution from provider, to roles, to users, and so on.
AuthenticationManager: Spring’s authentication manager takes care checking user credential validity.
Step 2: Configure Authorization Server
Create a class extending AuthorizationServerConfigurerAdapter and override the below methods.
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.jdbc(jdbcTemplate.getDataSource());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
.reuseRefreshTokens(false)
.accessTokenConverter(accessTokenConverter)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("hasAuthority('ROLE_TRUSTED_CLIENT')").checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
}
Some of the components of the authorization server config class are explained below.
@EnableAuthorizationServer: Enables an authorization server.
UserDetailsService: We inject a custom implementation of UserDetailsService in order to retrieve user details from the database.
Step 3: Configure Resource Server
Create a class extending ResourceServerConfigurerAdapter and override the below methods.
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerTokenServices tokenServices;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(tokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/actuator/**", "/api-docs/**","/oauth/*").permitAll()
.antMatchers("/jwttest/**" ).authenticated();
}
}
Some of the components of the resource server config class have been explained below.
@EnableResourceServer: Enables a resource server. By default this annotation creates a security filter which authenticates requests via an incoming OAuth2 token. The filter is an instance of WebSecurityConfigurerAdapter which has an hard-coded order of 3 (Due to some limitations of Spring Framework). You need to tell Spring Boot to set OAuth2 request filter order to 3 to align with the hardcoded value. You do that by adding security.oauth2.resource.filter-order = 3 in the application.properties file.
The resource server has the authority to define the permission for any endpoint. The the endpoint permission is defined with:
.antMatchers(“/actuator/**”, “/api-docs/**”).permitAll()
.antMatchers(“/jwttest/**”).authenticated()
Note that the Resource server and the Authorization server are using the same SecurityConfiguration as they are in same Project in this example. However, in Production system Authorization Server and Resource Server will be in different projects, with their own SecurityConfiguration.
Step 4: Configure UserDetailsService
Create a class extending UserDetailsService and override the below methods.
@Component
public class AppUserDetailsService implements UserDetailsService{
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
if(user == null) {
throw new UsernameNotFoundException(String.format("The username %s doesn't exist", s));
}
List<GrantedAuthority> authorities = new ArrayList<>();
user.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
});
UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
return userDetails;
}
}
Database Scripts
Use the following database scripts to create the required tables to execute the program.
- DDL
CREATE TABLE random_city (
id bigint NOT NULL,
name varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE TABLE app_role (
id bigint NOT NULL ,
description varchar(255) DEFAULT NULL,
role_name varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);
CREATE TABLE app_user (
id bigint NOT NULL ,
first_name varchar(255) NOT NULL,
last_name varchar(255) NOT NULL,
password varchar(255) NOT NULL,
username varchar(255) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE user_role (
user_id bigint NOT NULL,
role_id bigint NOT NULL,
CONSTRAINT FK859n2jvi8ivhui0rl0esws6o FOREIGN KEY (user_id) REFERENCES app_user (id),
CONSTRAINT FKa68196081fvovjhkek5m97n3y FOREIGN KEY (role_id) REFERENCES app_role (id)
);drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
drop table if exists oauth_client_token;
create table oauth_client_token (
token_id VARCHAR(255),
token bytea ,
authentication_id VARCHAR(255),
user_name VARCHAR(255),
client_id VARCHAR(255)
);
drop table if exists oauth_access_token;
create table oauth_access_token (
token_id VARCHAR(255),
token bytea,
authentication_id VARCHAR(255),
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication bytea,
refresh_token VARCHAR(255)
);
drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
token_id VARCHAR(255),
token bytea,
authentication bytea
);
drop table if exists oauth_code;
create table oauth_code (
code VARCHAR(255), authentication bytea
);
2. DML
INSERT INTO app_role (id, role_name, description) VALUES (1, 'STANDARD_USER', 'Standard User - Has no admin rights');
INSERT INTO app_role (id, role_name, description) VALUES (2, 'ADMIN_USER', 'Admin User - Has permission to perform admin tasks');
-- USER
-- non-encrypted password: jwtpass
INSERT INTO app_user (id, first_name, last_name, password, username) VALUES (1, 'John', 'Doe', '$2a$10$qtH0F1m488673KwgAfFXEOWxsoZSeHqqlB/8BTt3a6gsI5c2mdlfe', 'john.doe');
INSERT INTO app_user (id, first_name, last_name, password, username) VALUES (2, 'Admin', 'Admin', '$2a$10$qtH0F1m488673KwgAfFXEOWxsoZSeHqqlB/8BTt3a6gsI5c2mdlfe', 'admin.admin');
INSERT INTO user_role(user_id, role_id) VALUES(1,1);
INSERT INTO user_role(user_id, role_id) VALUES(2,1);
INSERT INTO user_role(user_id, role_id) VALUES(2,2);
-- Populate random city table
INSERT INTO random_city(id, name) VALUES (1, 'Bamako');
INSERT INTO random_city(id, name) VALUES (2, 'Nonkon');
INSERT INTO random_city(id, name) VALUES (3, 'Houston');
INSERT INTO random_city(id, name) VALUES (4, 'Toronto');
INSERT INTO random_city(id, name) VALUES (5, 'New York City');
INSERT INTO random_city(id, name) VALUES (6, 'Mopti');
-- insert client details
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
authorities, access_token_validity, refresh_token_validity)
VALUES
('testjwtclientid', 'XY7kmzoNzl100', 'read,write', 'password,refresh_token,client_credentials,authorization_code', 'ROLE_CLIENT,ROLE_TRUSTED_CLIENT', 900, 2592000);
Entities
User, Role, RandomCity entities are created to map to the data model.
REST APIs
@RestController
@RequestMapping("/jwttest")
public class ResourceController {
@Autowired
private GenericService userService;
@Autowired
private TokenEndpoint tokenEndpoint;
@Autowired
private TokenStore tokenStore;
@RequestMapping(value ="/cities")
@PreAuthorize("hasAuthority('ADMIN_USER') or hasAuthority('STANDARD_USER')")
public List<RandomCity> getUser(){
return userService.findAllRandomCities();
}
@RequestMapping(value ="/users", method = RequestMethod.GET)
@PreAuthorize("hasAuthority('ADMIN_USER')")
public List<User> getUsers(){
return userService.findAllUsers();
}
}
Here, two endpoints are exposed:
· /jwttest/cities: This endpoint is accessible to all authenticated users.
· /jwttest/users: This endpoint is accessible only to an admin user.
Verifying the Workflow
Following are the basic pieces of information required for testing and verification:
- client: testjwtclientid
- secret: XY7kmzoNzl100
- Non-admin username and password: john.doe and jwtpass
- Admin user: admin.admin and jwtpass
- Example of resource accessible to all authenticated users: http://localhost:8080/jwttest/cities
- Example of resource accessible to only an admin user: http://localhost:8080/jwttest/users
You can test as follows:
- Generate an Access Token
Request
curl -X POST http://testjwtclientid:XY7kmzoNzl100@localhost/oauth/token -H 'Authorization: Basic dGVzdGp3dGNsaWVudGlkOlhZN2ttem9OemwxMDA=' -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=password&username=admin.admin&password=jwtpass&undefined='
Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDQxNjQzMzEsInVzZXJfbmFtZSI6ImFkbWluLmFkbWluIiwiYXV0aG9yaXRpZXMiOlsiU1RBTkRBUkRfVVNFUiIsIkFETUlOX1VTRVIiXSwianRpIjoiMzNiNzUzNjUtY2E0OS00NDE0LWFiYWYtZmI4Njk0MmIwMzJiIiwiY2xpZW50X2lkIjoidGVzdGp3dGNsaWVudGlkIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl19.OyTSO5_Qd1daEPbfDvbCw0-owgXJCd4WPpdOXyrbNaY",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbi5hZG1pbiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiIzM2I3NTM2NS1jYTQ5LTQ0MTQtYWJhZi1mYjg2OTQyYjAzMmIiLCJleHAiOjE1NDY2ODg3MzAsImF1dGhvcml0aWVzIjpbIlNUQU5EQVJEX1VTRVIiLCJBRE1JTl9VU0VSIl0sImp0aSI6IjUyMjQ3NTFkLTk0YzYtNDYxZi1hNTc0LTQ0YzMwZTI4ZjZmYiIsImNsaWVudF9pZCI6InRlc3Rqd3RjbGllbnRpZCJ9.QF4QXbgc9f3ilH5iXWaM9wfQeZmQmgXeKWxCQuRcYwA",
"expires_in": 84,
"scope": "read write",
"jti": "33b75365-ca49-4414-abaf-fb86942b032b"
}
2. Use the Token to Access Resources
Use the generated token as the value of the Bearer in the Authorization header as follows:
Request
curl -X GET \
http://localhost:8080/jwttest/cities \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDQyOTM5MjksInVzZXJfbmFtZSI6ImFkbWluLmFkbWluIiwiYXV0aG9yaXRpZXMiOlsiU1RBTkRBUkRfVVNFUiIsIkFETUlOX1VTRVIiXSwianRpIjoiNjFkZWEzZjAtODYxZi00Nzc1LWEyMTMtMDQ1ZWVmN2EyODQ1IiwiY2xpZW50X2lkIjoidGVzdGp3dGNsaWVudGlkIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl19.DUW7PLTuF7Kk0y16I-srjaeJUE_wTSfvLr4Sb_5X3hc'
Response
[{
"id": 1,
"name": "Bamako"
}, {
"id": 2,
"name": "Nonkon"
}, {
"id": 3,
"name": "Houston"
}, {
"id": 4,
"name": "Toronto"
}, {
"id": 5,
"name": "New York City"
}, {
"id": 6,
"name": "Mopti"
}, {
"id": 7,
"name": "Koulikoro"
}, {
"id": 8,
"name": "Moscow"
}]
If you would like to refer to the full code, do check https://github.com/sumanentc/springboot-oauth-jwt.
References & Useful Readings
- JSON Web Token go to https://jwt.io/ to decode your generated token and learn more
- Why you need to set security.oauth2.resource.filter-order = 3 in application.properties file: https://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configuration/EnableResourceServer.html
- http://www.bubblecode.net/en/2016/01/22/understanding-oauth2/
- https://www.baeldung.com/spring-security-oauth-jwt
- https://www.devglan.com/spring-security/spring-boot-oauth2-jwt-example