Employee Microservice - Resource Server

Employee Microservice as Resource Server

The idea is to create a simple spring boot application which provides a basic CRUD operation for the employee object. Once the application is up and running, we can transform the application to act as a resource server which would force it to accept requests only with valid Oauth tokens.

To read more about the resource server, refer the following article:


Generate project structure using spring initializer:

Generate a spring boot project using spring initializer:
https://start.spring.io/
with the following dependencies:
  • Spring Data JPA
  • Oauth2 Resource Server
  • Spring Web 
Also add the following additional dependencies:
  • Dozer : For mapping between objects
  • Spring-Oauth2 : Spring security using Oauth2
  • H2 : Serves as our in-memory database.
Final List of 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-oauth2-resource-server</artifactId>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  
  <dependency>
   <groupId>net.sf.dozer</groupId>
   <artifactId>dozer</artifactId>
   <version>5.5.1</version>
  </dependency>

  <dependency>
   <groupId>io.craftsman</groupId>
   <artifactId>dozer-jdk8-support</artifactId>
   <version>1.0.2</version>
  </dependency>
  

  
  <dependency>
   <groupId>org.springframework.security.oauth</groupId>
   <artifactId>spring-security-oauth2</artifactId>
   <version>2.3.5.RELEASE</version>
  </dependency>
  
  <dependency>
   <groupId>org.springframework.security.oauth.boot</groupId>
   <artifactId>spring-security-oauth2-autoconfigure</artifactId>
   <version>2.1.2.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-jwt</artifactId>
   <version>1.0.10.RELEASE</version>
  </dependency>
  

  
  <dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
  </dependency>
  
  
  <dependency>
   <groupId>com.fasterxml.jackson.datatype</groupId>
   <artifactId>jackson-datatype-jsr310</artifactId>
   
  </dependency>

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>

  <dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger2</artifactId>
   <version>2.9.2</version>
  </dependency>

  <dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
  </dependency>

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>

  </dependency>

Project Structure





Important classes / configuration are as follows:
  • Employee Entity
@Entity
@DynamicUpdate(true)
@JsonIgnoreProperties(ignoreUnknown = true)
@Table(name = "Employee")
public class Employee extends BaseEntity {

  /**
   * 
   */
  private static final long serialVersionUID = -8067210052397923732L;

  @NotNull
  @Column(name = "first_name")
  String firstName;
  @NotNull
  @Column(name = "last_name")
  String lastName;
  @NotNull
  @Column(name = "dob")
  @JsonFormat(pattern = "dd.MM.yyyy")
  LocalDate dateOfBirth;
  @NotNull
  @Column(name = "employee_id", unique = true)
  Long employeeId;

  • Employee Service
@Service
public class EmployeeService {

  @Autowired
  EmployeeRepo employeeRepo;

  @Autowired
  @Qualifier("dozerWithMapping")
  DozerBeanMapper dozerWithMapping;

  @Transactional(readOnly = true)
  @PreAuthorize("hasAuthority('READ_EMPLOYEE')")
  public EmployeeDTO findEmployeeById(Long employeeId) {
    Employee employee = employeeRepo.findById(employeeId).orElse(null);
    if (employee == null) {
      throw new BusinessException("Employee Not Found", "Employee with id: " + employeeId + " not found");
    } else {
      return dozerWithMapping.map(employee, EmployeeDTO.class);
    }
  }

  @Transactional(readOnly = true)
  @PreAuthorize("hasAuthority('READ_ALL_EMPLOYEE')")
  public Page<EmployeeDTO> findAllEmployees(Pageable pageable) {
    Page<Employee> employees = employeeRepo.findAll(pageable);
    return employees.map(this::convertDtoPage);
  }

  private EmployeeDTO convertDtoPage(Employee employee) {
    return dozerWithMapping.map(employee, EmployeeDTO.class);
  }

  @Transactional(propagation = Propagation.REQUIRED)
  @PreAuthorize("hasAuthority('CREATE_EMPLOYEE')")
  public void createEmployee(EmployeeDTO employeeDTO) {
    employeeRepo.save(dozerWithMapping.map(employeeDTO, Employee.class));
  }

}


  • Employee Repo

public interface EmployeeRepo extends JpaRepository {
  @Query(value = "select e from Employee e order by e.id desc")
  Page<Employee> findAll(Pageable pageable);
}
 
  • Exception Handling
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class RestExceptionHandler {

  public static final String APPLICATION_ERROR_JSON = "application/error+json";

  @ExceptionHandler(BusinessException.class)
  public HttpEntity<ErrorDetail> handleBusinessException(BusinessException e, final HttpServletRequest request) {

    ErrorDetail problem = new ErrorDetail(e.getSummary(), e.getMessage());
    problem.setStatus(HttpStatus.NOT_FOUND.value());

    return new ResponseEntity<>(problem, updateContentType(), HttpStatus.NOT_FOUND);
  }

  @ExceptionHandler(HttpMessageNotReadableException.class)
  public HttpEntity<ErrorDetail> handleHttpMessageNotReadableException(HttpMessageNotReadableException e,
    final HttpServletRequest request) {

    final ErrorDetail problem = new ErrorDetail("Message cannot be converted",
      String.format("Invalid request body: %s", e.getMessage()));
    problem.setStatus(HttpStatus.BAD_REQUEST.value());

    return new ResponseEntity<>(problem, updateContentType(), HttpStatus.BAD_REQUEST);
  }

  @ExceptionHandler(AccessDeniedException.class)
  public HttpEntity<ErrorDetail> handleAccessDeniedException(AccessDeniedException e,
    final HttpServletRequest request) {

    final ErrorDetail problem = new ErrorDetail(
      "Access Denied - You do not have permission to access the operation", e.getMessage());
    problem.setStatus(HttpStatus.FORBIDDEN.value());

    return new ResponseEntity<>(problem, updateContentType(), HttpStatus.FORBIDDEN);
  }

  @ExceptionHandler(AuthenticationCredentialsNotFoundException.class)
  public HttpEntity<ErrorDetail> handleCredentialsNotFound(AuthenticationCredentialsNotFoundException e,
    final HttpServletRequest request) {

    final ErrorDetail problem = new ErrorDetail(
      "Access Denied - You do not have permission to access the operation", e.getMessage());
    problem.setStatus(HttpStatus.FORBIDDEN.value());

    return new ResponseEntity<>(problem, updateContentType(), HttpStatus.FORBIDDEN);
  }

  @ExceptionHandler(javax.validation.ConstraintViolationException.class)
  public HttpEntity<ErrorDetail> handleConstraintViolationsFromJavax(javax.validation.ConstraintViolationException e,
    final HttpServletRequest request) {

    final ErrorDetail problem = new ErrorDetail("Constraint Violation", e.getMessage());
    problem.setStatus(HttpStatus.CONFLICT.value());

    return new ResponseEntity<>(problem, updateContentType(), HttpStatus.CONFLICT);
  }

  /**
   * Handles all unexpected situations
   *
   * @param e any exception of type {@link Exception}
   * @return {@link ResponseEntity} containing standard body in case of errors
   */
  @ExceptionHandler(Exception.class)
  public HttpEntity<ErrorDetail> handleException(Exception e, final HttpServletRequest request) {

    ErrorDetail problem = new ErrorDetail("Internal Error", "An unexpected error has occurred");
    problem.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());

    return new ResponseEntity<>(problem, updateContentType(), HttpStatus.INTERNAL_SERVER_ERROR);
  }

  private HttpHeaders updateContentType() {
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.set("Content-Type", APPLICATION_ERROR_JSON);
    return httpHeaders;
  }

}


  • Enabling Resource Server Configuration:
To make the Employee application to behave as a Resource server, the following changes are required :

Create Keystore:

The OAuth2.0 Server signs the tokens using a private key, and the resource server can verify the token using the Server’s public key.

In our example, when we create the authorization server (in the next section), it should contain the keystore and all the resource server should contain the public keys.
For our demo-application, we can create the public and private key by the following steps:





1) Generating key pair:
> keytool -genkeypair -alias microservice -keyalg RSA -keypass microservice -keystore c:\prashant\microservices.jks -storepass microservice
The generated keystore should be saved as this is required by the Oauth Server.

2) Extracting public key: (in bash)
keytool -list -rfc --keystore c:/prashant/microservices.jks | openssl x509 -inform pem -pubkey >> c:/prashant/microservices_public_key.txt

The generated public key file contains public key and certificate. We will open the text file and extract only the public key and save it in the employee apllications application.yml file as follows:
Resource Server & Security Configuration :


@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfigJwt extends ResourceServerConfigurerAdapter {

  @Value("${security.oauth2.resource.jwt.keyValue}")
  String oauthPublicKey;

  @Override
  public void configure(final ResourceServerSecurityConfigurer resources) throws Exception {
    resources.tokenStore(tokenStore());
  }

  @Bean
  @Primary
  public DefaultTokenServices tokenServices() {
    final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    defaultTokenServices.setSupportRefreshToken(true);
    return defaultTokenServices;
  }

  @Bean
  public TokenStore tokenStore() {
    return new JwtTokenStore(accessTokenConverter());
  }

  @Bean
  public JwtAccessTokenConverter accessTokenConverter() {
    final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setVerifierKey(getPublicKeyAsString());

    return converter;
  }

  private String getPublicKeyAsString() {
    return oauthPublicKey;
  }

  @Bean
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
}


Enabling Pre-Authorize annotation 
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {



Conclusion
With these changes, we have enabled the Employee microservice application to act as a Resource Server. This would mean that the applciation resources / endpoints can only be accessed with a valid Oauth2 token.

To test this we can create a request in post man as follows:

  
Note: The in-memory database has some pre-loaded entries. This is achieved by adding records via Dataloader:
@Component
public class DataLoader implements ApplicationRunner

Source Code :
The entire source code can be found in Git Hub


    Comments