Secure a Spring Boot REST API With JSON Web Token + Reference to Angular Integration

In this tutorial, I am going to walk you through how to secure a Spring Boot REST API with JSON Web Token (JWT) to exchange claims between a server and a client. This is Part 2 of a collaborative effort between my colleague Julia Passynkova and myself to demonstrate how to secure an Angular 2+ application using Spring Boot as a RESTful Backend. If you are impatient or prefer to learn directly from the code just checkout the code from my Github repository at https://github.com/nydiarra/springboot-jwt . You can find the Angular integration project at https://github.com/ipassynk/angular-springboot-jwt.

Quick Intro:

Over the past few years, Spring Boot has greatly simplified the configuration of a Spring Framework application. The unceremonious approach it takes enables developers among many other powerful features to enable a “Basic” security for an application by simply having Spring Security dependency on the classpath.

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, is compact and URL-safe. With JWT there it is needless to store access tokens in a database (although you may still do that and even need it based on the use case) or worry about sticky sessions, this makes building redundancy into your enterprise application more cost effective at least as far as the security aspect is concerned. You should however, want to deal with other aspects such as token revocation, but that is not covered here. The basis to understanding how useful is JWT is to first grasp OAuth 2.0. For a quick reference, below is an illustration of the OAuth dance.

Illustration of OAUTH 2. dance

What you will need for the project?

1. Spring Boot 1.5.3.RELEASE project with Maven or Gradle. This project uses Maven.

2. The following dependencies:

· spring-boot-starter-web

· spring-boot-starter-security

· spring-boot-starter-data-jpa

· spring-boot-starter-web

· com.h2database — H2 Database for storing sample test data

· spring-security-jwt version: 1.0.7.RELEASE

· spring-security-oauth2 version: 2.1.0.RELEASE

Final project structure

When you clone the project from Github and import it into your IDE for instance, its structure will look similar to the following. Here, I am using a screenshot from IntelliJ Community Edition IDE. Yours will look slightly different depending on the IDE you use.


1. Configure Spring Security

Thanks to Spring Boot’s autoconfiguration we will need minimal customizations to get set:

First of all the the application’s configuration file: application.properties. More on that in each section.

security.oauth2.resource.filter-order=3

security.signing-key
=MaYzkSjmkzPC57L
security.encoding-strength
=256
security.security-realm
=Spring Boot JWT Example Realm

security.jwt.client-id
=testjwtclientid
security.jwt.client-secret
=XY7kmzoNzl100
security.jwt.grant-type
=password
security.jwt.scope-read
=read
security.jwt.scope-write
=write
security.jwt.resource-ids
=testjwtresourceid

Then the security configuration class.

@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 UserDetailsService userDetailsService;

@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(new ShaPasswordEncoder(encodingStrength));
}

@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 JwtTokenStore(accessTokenConverter());
}

@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;
}
}

· @EnableWebSecurity : Enables spring security and hints Spring Boot to apply all the sensitive defaults

· @EnableGlobalMethodSecurity: Allows us to have method level access control

· JwtTokenStore and JwtAccessTokenConverter beans: A JwtTokenStore bean is needed by the authorization server and to enable the resource server to decode access tokens an JwtAccessTokenConverter bean must be used by both servers. In this case, we are providing a symmetric key signing.

· UserDetailsService: We inject a custom implementation of UserDetailsService, named AppUserDetailsService (see code in Github for details) in order to retrieve user details from the database.

· Encoding: SHA-256 is used to encode passwords. This is set in encodingStrength 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, etc.

2. Configure Authorization Server

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Value("${security.jwt.client-id}")
private String clientId;

@Value("${security.jwt.client-secret}")
private String clientSecret;

@Value("${security.jwt.grant-type}")
private String grantType;

@Value("${security.jwt.scope-read}")
private String scopeRead;

@Value("${security.jwt.scope-write}")
private String scopeWrite = "write";

@Value("${security.jwt.resource-ids}")
private String resourceIds;

@Autowired
private TokenStore tokenStore;

@Autowired
private JwtAccessTokenConverter accessTokenConverter;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(clientId)
.secret(clientSecret)
.authorizedGrantTypes(grantType)
.scopes(scopeRead, scopeWrite)
.resourceIds(resourceIds);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.authenticationManager(authenticationManager);
}

}

· @EnableAuthorizationServer: Enables an authorization server

· AuthorizationServerConfig which is our authorization server configuration class extends AuthorizationServerConfigurerAdapter which in turn is an implementation of AuthorizationServerConfigurer. The presence of a bean of type AuthorizationServerConfigurer simply tells Spring Boot to switch off auto-configuration and use the custom configuration. Also the AuthorizationServerConfig like any other configuration class has its definition automatically scanned,wired and applied by Spring Boot because of the @Configuration annotation.

· Client id: defines the id of the client application that is authorized to authenticate, the client application provides this in order to be allowed to send request to the server.

Client secret: is the client application’s password. In a non-trivial implementation client ids and passwords will be securely stored in a database and retrievable through a separate API that clients applications access during deployment. These pieces of information can also be shared and stored in environment variables although that would not be my preferred option.

· Grant type: we define grant type password here because it’s not enabled by default

· The scope: read, write defines the level of access we are allowing to resources

· Resource Id: The resource Id specified here must be specified on the resource server as well

· AuthenticationManager: Spring’s authentication manager takes care checking user credential validity

· TokenEnhancerChain: We define a token enhancer that enables chaining multiple types of claims containing different information

3. Configure Resource Server

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private ResourceServerTokenServices tokenServices;

@Value("${security.jwt.resource-ids}")
private String resourceIds;

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceIds).tokenServices(tokenServices);
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
.and()
.authorizeRequests()
.antMatchers("/actuator/**", "/api-docs/**").permitAll()
.antMatchers("/springjwt/**" ).authenticated();
}
}

@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. Hopefully this will be fixed in future releases.

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(“/springjwt/**”).authenticated()

Here notice that the resource and the authorization servers both use the same token service. That is because they are in the same code base so we are reusing the same bean.

4. Configure a Data Source

Spring Boot can fully auto configure the in-memory H2 datasource once it’s defined on the classpath. However, to give you a better sense of how you can take control of your application and customize your data source the following configuration is provided:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.nouhoun.springboot.jwt.integration.repository")
public class DatasourceConfig {

@Bean
public DataSource datasource() throws PropertyVetoException {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase dataSource = builder
.setType(EmbeddedDatabaseType.H2)
.addScript("sql-scripts/schema.sql")
.addScript("sql-scripts/data.sql")
.build();

return dataSource;
}

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(@Qualifier("datasource") DataSource ds) throws PropertyVetoException{
LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
entityManagerFactory.setDataSource(ds);
entityManagerFactory.setPackagesToScan(new String[]{"com.nouhoun.springboot.jwt.integration.domain"});
JpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
entityManagerFactory.setJpaVendorAdapter(jpaVendorAdapter);
return entityManagerFactory;
}

@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}

Replace H2 with any database (MariaDB, MySQL, Oracle, SQL Server, etc.) to fit your use case.

5. Database Scripts and Test Data

schema.sql

CREATE TABLE random_city (
id bigint(20) NOT NULL AUTO_INCREMENT,
name varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);

CREATE TABLE app_role (
id bigint(20) NOT NULL AUTO_INCREMENT,
description varchar(255) DEFAULT NULL,
role_name varchar(255) DEFAULT NULL,
PRIMARY KEY (id)
);


CREATE TABLE app_user (
id bigint(20) NOT NULL AUTO_INCREMENT,
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(20) NOT NULL,
role_id bigint(20) NOT NULL,
CONSTRAINT FK859n2jvi8ivhui0rl0esws6o FOREIGN KEY (user_id) REFERENCES app_user (id),
CONSTRAINT FKa68196081fvovjhkek5m97n3y FOREIGN KEY (role_id) REFERENCES app_role (id)
);

data.sql

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', '821f498d827d4edad2ed0960408a98edceb661d9f34287ceda2962417881231a', 'john.doe');
INSERT INTO app_user (id, first_name, last_name, password, username) VALUES (2, 'Admin', 'Admin', '821f498d827d4edad2ed0960408a98edceb661d9f34287ceda2962417881231a', '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');
INSERT INTO random_city(id, name) VALUES (6, 'Mopti');
INSERT INTO random_city(id, name) VALUES (7, 'Koulikoro');
INSERT INTO random_city(id, name) VALUES (8, 'Moscow');

6. Entities

User, Role, RandomCity entities are created to map to the data model

7. Exposing Resource via a REST Controller

@RestController
@RequestMapping("/springjwt")
public class ResourceController {
@Autowired
private GenericService userService;

@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

· /springjwt/cities: This endpoint is accessible to all authenticated users

· /springjwt/users: This endpoint is accessible only to an admin user

8. Running and Testing the Application

First you will need the following basic pieces of information:

Step 1: Generate an access token

Use the following generic command to generate an access token: $ curl client:secret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=pwd

For this specific application, to generate an access token for the non-admin user john.doe, run: $ curl testjwtclientid:XY7kmzoNzl100@localhost:8080/oauth/token -d grant_type=password -d username=john.doe -d password=jwtpass. You'll receive a response similar to below

`
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDk0NDU0MjgyLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiIwYmQ4ZTQ1MC03ZjVjLTQ5ZjMtOTFmMC01Nzc1YjdiY2MwMGYiLCJjbGllbnRfaWQiOiJ0ZXN0and0Y2xpZW50aWQifQ.rvEAa4dIz8hT8uxzfjkEJKG982Ree5PdUW17KtFyeec",
"token_type": "bearer",
"expires_in": 43199,
"scope": "read write",
"jti": "0bd8e450-7f5c-49f3-91f0-5775b7bcc00f"
}`

Step 2: Use the token to access resources through your RESTful API

Use the token to access resources through your RESTful API

Access content available to all authenticated users

  • Use the generated token as the value of the Bearer in the Authorization header as follows: curl http://localhost:8080/springjwt/cities -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDk0NDU0MjgyLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiIwYmQ4ZTQ1MC03ZjVjLTQ5ZjMtOTFmMC01Nzc1YjdiY2MwMGYiLCJjbGllbnRfaWQiOiJ0ZXN0and0Y2xpZW50aWQifQ.rvEAa4dIz8hT8uxzfjkEJKG982Ree5PdUW17KtFyeec"
  • The response will be: [ { "id": 1, "name": "Bamako" }, { "id": 2, "name": "Nonkon" }, { "id": 3, "name": "Houston" }, { "id": 4, "name": "Toronto" }, { "id": 5, "name": "New York" }, { "id": 6, "name": "Mopti" }, { "id": 7, "name": "Koulikoro" }, { "id": 8, "name": "Moscow" } ]

Access content available only to an admin user

As with the previous example first generate an access token for the admin user with the credentials provided above then run curl http://localhost:8080/springjwt/users -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDk0NDU0OTIzLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiIyMTAzMjRmMS05MTE0LTQ1NGEtODRmMy1hZjUzZmUxNzdjNzIiLCJjbGllbnRfaWQiOiJ0ZXN0and0Y2xpZW50aWQifQ.OuprVlyNnKuLkoQmP8shP38G3Hje91GBhu4E0HD2Fes" The result will be: [ { "id": 1, "username": "john.doe", "firstName": "John", "lastName": "Doe", "roles": [ { "id": 1, "roleName": "STANDARD_USER", "description": "Standard User - Has no admin rights" } ] }, { "id": 2, "username": "admin.admin", "firstName": "Admin", "lastName": "Admin", "roles": [ { "id": 1, "roleName": "STANDARD_USER", "description": "Standard User - Has no admin rights" }, { "id": 2, "roleName": "ADMIN_USER", "description": "Admin User - Has permission to perform admin tasks" } ] } ]

9. Access a non-secure or non-protected endpoint

The http://localhost:8080/health is not restricted. This is accessible to anonymous users and can be used for load balancing health check purpose.

References & Useful Readings: