Spring Security With JWT and Oauth2 With Spring Boot

Page content

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>
view raw pom.xml hosted with ❤ by GitHub

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;
}
view raw User.java hosted with ❤ by GitHub

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;
}
view raw Role.java hosted with ❤ by GitHub

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);
view raw data.sql hosted with ❤ by GitHub

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>
view raw pom.xml hosted with ❤ by GitHub

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
view raw application.yml hosted with ❤ by GitHub

The detail of the properties is as below -

  1. security.encoding-strength=256 size of the encoding.

  2. security.signing-key is the sign key used to encode the token.

  3. security.security-realm realm of the authentication. see

  4. security.jwt.client-id=client Client Id.

  5. security.jwt.client-secret It should be BCrypt format you can use this tool to encode any string to BCrypt

  6. security.jwt.grant-type=password Grant type, it can be “password”, “refresh-token”.

  7. security.jwt.scope-read=read read scope.

  8. security.jwt.scope-write=write write scope.

  9. 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.