In this article, we will be securing REST APIs with role based OAUTH2 implementation. To do so, we will be creating two custom roles as ADMIN and USER and we will use @secured annotation provided by spring security to secure our controller methods based on role. To some of the endpoints, we will provide access to ADMIN role and others will be accesible to user having ADMIN and USER role. All the user details, credentials and associated roles will be saved into MySQL DB and we will be using spring data to perform our DB operations. We will use spring boot to take care of our most of the configurations.
We will be using Postman to perform all of our CRUD operation and test all the APIs. You can visit my another article for an angular implementation with spring security and OAUTH2. Also, we will be using JwtTokenStore to translate access tokens to and from authentications. For an InMemoryTokenStore ou can visit here - here.
Technologies Used
Maven
Spring Boot 2.1.1.RELEASE
OAUTH 2.1.0.RELEASE
MySQL
JWT
Intellij
Project Structure
Head over to start.spring.io and download a sample spring boot app. Below is the snapshot of mine.
In this article, we will not be discussing much about the basics of OAUTH2 as we have discussed alot in our previous articles. For a complete list of articles on spring security, you can visit here - Spring Security Tutorials
Authorization Server Configuration in OAUTH2
Below is the implementation of our authorization server configuration that is responsible for generating authorization tokens. We have configuration of JWT token store along with the common code of OAUTH2 protocol to configure client id, client-secret and grant types. AuthorizationServerConfig.java
package com.devglan.rolebasedoauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.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 AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final String CLIEN_ID = "devglan-client";
private static final String CLIENT_SECRET = "$2a$04$1VGGg98BkCSvSLs4RDSyUu8MrYf0jkY3dgCLAy8GHJe6QA4VAM/X2";
private static final String GRANT_TYPE_PASSWORD = "password";
private static final String AUTHORIZATION_CODE = "authorization_code";
private static final String REFRESH_TOKEN = "refresh_token";
private static final String IMPLICIT = "implicit";
private static final String SCOPE_READ = "read";
private static final String SCOPE_WRITE = "write";
private static final String TRUST = "trust";
@Autowired
private AuthenticationManager authenticationManager;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("as466gf");
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
configurer
.inMemory()
.withClient(CLIEN_ID)
.secret(CLIENT_SECRET)
.authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT )
.scopes(SCOPE_READ, SCOPE_WRITE, TRUST);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter());
}
}
Resource Server Configuration
Resource in our context is the REST API which we have exposed for the crud operation. To access these resources, the client must be authenticated. We have not made any configurations here to make our endpoints secured as we will using method level security with annotation @Secured.
There is no exception handling done in resource server config. You can visit my another article for exception handling in spring securityResourceServerConfig.java
Now, to bootstrap the authorization server and resource server configuration, we have WebSecurityConfiguration. The parameter securedEnabled = true enables support for annotation @Secured. We will be using Bcypt password encoder for password encryption.
Below is the controller to expose the REST APIs for CRUD operations. Any user having a role USER can access the method getUser() and rest of the APIs is only accessible to user having ADMIN role. UserController.java
package com.devglan.rolebasedoauth2.controller;
import com.devglan.rolebasedoauth2.dto.ApiResponse;
import com.devglan.rolebasedoauth2.dto.UserDto;
import com.devglan.rolebasedoauth2.service.AuthenticationFacadeService;
import com.devglan.rolebasedoauth2.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
public static final String SUCCESS = "success";
public static final String ROLE_ADMIN = "ROLE_ADMIN";
public static final String ROLE_USER = "ROLE_USER";
@Autowired
private UserService userService;
@Autowired
private AuthenticationFacadeService authenticationFacadeService;
@Secured({ROLE_ADMIN})
@GetMapping
public ApiResponse listUser(){
log.info(String.format("received request to list user %s", authenticationFacadeService.getAuthentication().getPrincipal()));
return new ApiResponse(HttpStatus.OK, SUCCESS, userService.findAll());
}
@Secured({ROLE_ADMIN})
@PostMapping
public ApiResponse create(@RequestBody UserDto user){
log.info(String.format("received request to create user %s", authenticationFacadeService.getAuthentication().getPrincipal()));
return new ApiResponse(HttpStatus.OK, SUCCESS, userService.save(user));
}
@Secured({ROLE_ADMIN, ROLE_USER})
@GetMapping(value = "/{id}")
public ApiResponse update(@PathVariable long id){
log.info(String.format("received request to update user %s", authenticationFacadeService.getAuthentication().getPrincipal()));
return new ApiResponse(HttpStatus.OK, SUCCESS, userService.findOne(id));
}
@Secured({ROLE_ADMIN})
@DeleteMapping(value = "/{id}")
public void delete(@PathVariable(value = "id") Long id){
log.info(String.format("received request to delete user %s", authenticationFacadeService.getAuthentication().getPrincipal()));
userService.delete(id);
}
}
Service Implementation
The service implementation has common implementation for CRUD operation. The method loadUserByUsername() is important here which is an overriden method. This method is responsible for validating user and it's roles from DB.
package com.devglan.rolebasedoauth2.service.impl;
import com.devglan.rolebasedoauth2.dao.RoleDao;
import com.devglan.rolebasedoauth2.dao.UserDao;
import com.devglan.rolebasedoauth2.dto.UserDto;
import com.devglan.rolebasedoauth2.model.Role;
import com.devglan.rolebasedoauth2.model.RoleType;
import com.devglan.rolebasedoauth2.model.User;
import com.devglan.rolebasedoauth2.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Transactional
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
User user = userDao.findByUsername(userId);
if(user == null){
log.error("Invalid username or password.");
throw new UsernameNotFoundException("Invalid username or password.");
}
Set grantedAuthorities = getAuthorities(user);
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
private Set getAuthorities(User user) {
Set roleByUserId = user.getRoles();
final Set authorities = roleByUserId.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName().toString().toUpperCase())).collect(Collectors.toSet());
return authorities;
}
public List findAll() {
List users = new ArrayList<>();
userDao.findAll().iterator().forEachRemaining(user -> users.add(user.toUserDto()));
return users;
}
@Override
public User findOne(long id) {
return userDao.findById(id).get();
}
@Override
public void delete(long id) {
userDao.deleteById(id);
}
@Override
public UserDto save(UserDto userDto) {
User userWithDuplicateUsername = userDao.findByUsername(userDto.getUsername());
if(userWithDuplicateUsername != null && userDto.getId() != userWithDuplicateUsername.getId()) {
log.error(String.format("Duplicate username %", userDto.getUsername()));
throw new RuntimeException("Duplicate username.");
}
User userWithDuplicateEmail = userDao.findByEmail(userDto.getEmail());
if(userWithDuplicateEmail != null && userDto.getId() != userWithDuplicateEmail.getId()) {
log.error(String.format("Duplicate email %", userDto.getEmail()));
throw new RuntimeException("Duplicate email.");
}
User user = new User();
user.setEmail(userDto.getEmail());
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
user.setUsername(userDto.getUsername());
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
List roleTypes = new ArrayList<>();
userDto.getRole().stream().map(role -> roleTypes.add(RoleType.valueOf(role)));
user.setRoles(roleDao.find(userDto.getRole()));
userDao.save(user);
return userDto;
}
}
Now, let us define our model classes. Below is our User and Role class. User.java