In this article i’m going to talk about how to secure your REST APIs, adding role based authentication to your REST API.

1.Creating REST API using Spring boot

Go to Spring Initializr portal and create spring boot application with web & AOP

Screenshot 2018-12-31 at 5.58.47 PM.png

2. Import the pom.xml in your IDE

Generate the project as a zip file. Extract it to a some place in your computer. Import the project in to your IDE

3. Check maven dependencies

Check the maven file should have spring-boot-starter-web,spring-boot-starter-aop dependency in it and add the additional dependecies as below(spring-boot-starter-json, commons-codec) your pom.xml will looks like below

<dependencies>
    <!-- Add typical dependencies for a web application -->
    <!-- Adds Tomcat and Spring MVC, along others -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
    </dependency>
    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
        <version>1.11</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

4. Create REST APIs

  1. Create REST API for login as below

LoginController.java

package com.bokks.micro.springbootrestapi.controller;

import com.bokks.micro.springbootrestapi.model.APIToken;
import com.bokks.micro.springbootrestapi.model.Credentials;
import com.bokks.micro.springbootrestapi.model.User;
import com.bokks.micro.springbootrestapi.service.TokenService;
import com.bokks.micro.springbootrestapi.service.UserService;
import com.bokks.micro.springbootrestapi.util.CustomErrorType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/authenticate")
public class LoginController {

    public static final Logger logger = LoggerFactory.getLogger(LoginController.class);

    @Autowired
    UserService userService; //Service which will do all data retrieval/manipulation work

    @Autowired
    TokenService tokenService;

    /**
     * This is the login api it requires input as json
     * {
     *   "username": "admin",
     *   "password": "admin123"
     * }
     * @param credentials
     * @return
     */

    @RequestMapping(value = "/login", method = RequestMethod.POST, produces = "application/json", consumes = "application/json")
    @ResponseBody
    public ResponseEntity<?> loginUser(@RequestBody Credentials credentials) {

        try {

            String username = credentials.getUsername();
            String password = credentials.getPassword();

            // Authenticate the user using the credentials provided
            authenticate(username, password);

            // Issue a token for the user
            String token = issueToken(username);

            // Return the token on the response
            return  new ResponseEntity<APIToken>(new APIToken(token), HttpStatus.OK);

        } catch (Exception e) {
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.FORBIDDEN);
        }
    }

    private void authenticate(String username, String password) throws Exception {
        // Authenticate against a database, LDAP, file or whatever
        // Throw an Exception if the credentials are invalid
        // In this case authentiating using hardcoded values

        logger.info("Fetching User with username {}", username);
        User user = userService.findByUsername(username);
        if (user == null) {
            logger.error("User with username {} not found.", username);
            throw new Exception("No user found for the user name : " + username);
        }

        if(user.getPassword().equals(password)){
            logger.info("User with username {}  found. Going to issue the token", username);
        }else{
            throw new Exception("User credentials are invalid for user name: " + username);
        }


    }

    private String issueToken(String username) {
        // Issue a token (can be a random String persisted to a database or a JWT token)
        // The issued token must be associated to a user
        // Return the issued token
        logger.info("Creating Token for username {}", username);
        return  tokenService.createToken(username);

    }
}

This API requires additional classes to create Users and the token

User.java

package com.bokks.micro.springbootrestapi.model;

public class User {

   private String username;
   
   private String name;
   
   private int age;
   
   private double salary;

   private String password;

   private UserRoles userRole;

   public User(){
      username ="";
   }
   
   public User(String username, String name, int age, double salary, String password, UserRoles role ){
      this.username = username;
      this.name = name;
      this.age = age;
      this.salary = salary;
      this.password = password;
      this.userRole = role;
   }
   
   public String getUsername() {
      return username;
   }

   public void setUsername(String username) {
      this.username = username;
   }

   public String getName() {
      return name;
   }

   public void setName(String name) {
      this.name = name;
   }

   public int getAge() {
      return age;
   }

   public void setAge(int age) {
      this.age = age;
   }

   public double getSalary() {
      return salary;
   }

   public void setSalary(double salary) {
      this.salary = salary;
   }

   public String getPassword() {
      return password;
   }

   public void setPassword(String password) {
      this.password = password;
   }

   public UserRoles getUserRole() {
      return userRole;
   }

   public void setUserRole(UserRoles userRole) {
      this.userRole = userRole;
   }

// @Override
// public int hashCode() {
//    final int prime = 31;
//    int result = 1;
//    result = prime * result + (int) (username ^ (username >>> 32));
//    return result;
// }

   @Override
   public boolean equals(Object obj) {
      if (this == obj)
         return true;
      if (obj == null)
         return false;
      if (getClass() != obj.getClass())
         return false;
      User other = (User) obj;
      if (username != other.username)
         return false;
      return true;
   }

   @Override
   public String toString() {
      return "User [username=" + username + ", name=" + name + ", age=" + age
            + ", salary=" + salary + "]";
   }


}

Token.java

package com.bokks.micro.springbootrestapi.model;

import org.apache.commons.codec.binary.Base64;

public class Token {

    private long timestamp;

    private String token;

    private String username;

    public Token(long timestamp,String token,String username){
        this.token = token;
        this.timestamp = timestamp;
        this.username = username;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        String encodedToken = timestamp + "##" + token+ "##" + username;
        byte[] encodedBytes = Base64.encodeBase64(encodedToken.getBytes());

        return new String(encodedBytes);
    }


}

Create the API that you need to secure

RestApiController.java

package com.bokks.micro.springbootrestapi.controller;

import java.util.List;

import com.bokks.micro.springbootrestapi.filters.AuthenticationFilter;
import com.bokks.micro.springbootrestapi.filters.Secured;
import com.bokks.micro.springbootrestapi.model.User;
import com.bokks.micro.springbootrestapi.model.UserRoles;
import com.bokks.micro.springbootrestapi.service.UserService;
import com.bokks.micro.springbootrestapi.util.CustomErrorType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;

@RestController
@RequestMapping("/expose")
public class RestApiController {

    public static final Logger logger = LoggerFactory.getLogger(RestApiController.class);

    @Autowired
    UserService userService; //Service which will do all data retrieval/manipulation work

    // -------------------Retrieve All Users---------------------------------------------

    @Secured(authorizedBy = {UserRoles.ADMIN,UserRoles.ROLE1}) // Annotation will allow Role based authorization fot the APIs
    @RequestMapping(value = "/version", method = RequestMethod.GET)
    public ResponseEntity<String> getVersion() {
        logger.info("API version details : SpringBootRestAPI Verion 1.0");
        return new ResponseEntity<String>("SpringBootRestAPI Verion 1.0", HttpStatus.OK);
    }

    @Secured(authorizedBy = UserRoles.ADMIN)
    @RequestMapping(value = "/user/", method = RequestMethod.GET)
    public ResponseEntity<List<User>> listAllUsers() {
        List<User> users = userService.findAllUsers();
        if (users.isEmpty()) {
            logger.info("No Users found");
            return new ResponseEntity(HttpStatus.NO_CONTENT);
            // You many decide to return HttpStatus.NOT_FOUND
        }
        return new ResponseEntity<List<User>>(users, HttpStatus.OK);
    }


    // -------------------Retrieve Single User------------------------------------------

    @RequestMapping(value = "/user/{username}", method = RequestMethod.GET)
    public ResponseEntity<?> getUser(@PathVariable("username") String username) {
        logger.info("Fetching User with username {}", username);
        User user = userService.findByUsername(username);
        if (user == null) {
            logger.error("User with username {} not found.", username);
            return new ResponseEntity(new CustomErrorType("User with username " + username
                    + " not found"), HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<User>(user, HttpStatus.OK);
    }

    // -------------------Create a User-------------------------------------------

    @RequestMapping(value = "/user/", method = RequestMethod.POST)
    public ResponseEntity<?> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) {
        logger.info("Creating User : {}", user);

        if (userService.isUserExist(user)) {
            logger.error("Unable to create. A User with name {} already exist", user.getName());
            return new ResponseEntity(new CustomErrorType("Unable to create. A User with name " +
                    user.getName() + " already exist."), HttpStatus.CONFLICT);
        }
        userService.saveUser(user);

        HttpHeaders headers = new HttpHeaders();
        headers.setLocation(ucBuilder.path("/api/user/{id}").buildAndExpand(user.getUsername()).toUri());
        return new ResponseEntity<String>(headers, HttpStatus.CREATED);
    }

    // ------------------- Update a User ------------------------------------------------

    @RequestMapping(value = "/user/{username}", method = RequestMethod.PUT)
    public ResponseEntity<?> updateUser(@PathVariable("username") String username, @RequestBody User user) {
        logger.info("Updating User with username {}", username);

        User currentUser = userService.findByUsername(username);

        if (currentUser == null) {
            logger.error("Unable to update. User with username {} not found.", username);
            return new ResponseEntity(new CustomErrorType("Unable to upate. User with username " + username + " not found."),
                    HttpStatus.NOT_FOUND);
        }

        currentUser.setName(user.getName());
        currentUser.setAge(user.getAge());
        currentUser.setSalary(user.getSalary());

        userService.updateUser(currentUser);
        return new ResponseEntity<User>(currentUser, HttpStatus.OK);
    }

    // ------------------- Delete a User-----------------------------------------

    @RequestMapping(value = "/user/{username}", method = RequestMethod.DELETE)
    public ResponseEntity<?> deleteUser(@PathVariable("username") String username) {
        logger.info("Fetching & Deleting User with username {}", username);

        User user = userService.findByUsername(username);
        if (user == null) {
            logger.error("Unable to delete. User with username {} not found.", username);
            return new ResponseEntity(new CustomErrorType("Unable to delete. User with username " + username + " not found."),
                    HttpStatus.NOT_FOUND);
        }
        userService.deleteUserByUsername(username);
        return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
    }

    // ------------------- Delete All Users-----------------------------

    @RequestMapping(value = "/user/", method = RequestMethod.DELETE)
    public ResponseEntity<User> deleteAllUsers() {
        logger.info("Deleting All Users");

        userService.deleteAllUsers();
        return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
    }

}

You can see in this API i have created annotation to secure the API “@Secured(authorizedBy = UserRoles.ADMIN)” . This is where i’m going to filter the roles and validate the user based on the token send in the HTTP request header.

5. Create the custom annotation

Creating custom annotation is similar to writing an interface, except that it interface keyword is prefixed with @ symbol. We can declare methods in annotation. Let’s see annotation example and then we will discuss it’s features.

My custom annotation class will looks like below.

Secured.java

package com.bokks.micro.springbootrestapi.filters;

import com.bokks.micro.springbootrestapi.model.UserRoles;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Secured {
    UserRoles[] authorizedBy() default {};
}

Now i will explain what is a custom annotation class

  • Annotation methods can’t have parameters.
  • Annotation methods return types are limited to primitives, String, Enums, Annotation or array of these.
  • Annotation methods can have default values.
  • Annotations can have meta annotations attached to them. Meta annotations are used to provide information about the annotation. There are four types of meta annotations:
    1. @Documented – indicates that elements using this annotation should be documented by javadoc and similar tools. This type should be used to annotate the declarations of types whose annotations affect the use of annotated elements by their clients. If a type declaration is annotated with Documented, its annotations become part of the public API of the annotated elements.
    2. @Target – indicates the kinds of program element to which an annotation type is applicable. Some possible values are TYPE, METHOD, CONSTRUCTOR, FIELD etc. If Target meta-annotation is not present, then annotation can be used on any program element.
    3. @Inherited – indicates that an annotation type is automatically inherited. If user queries the annotation type on a class declaration, and the class declaration has no annotation for this type, then the class’s superclass will automatically be queried for the annotation type. This process will be repeated until an annotation for this type is found, or the top of the class hierarchy (Object) is reached.
    4. @Retention – indicates how long annotations with the annotated type are to be retained. It takes RetentionPolicy argument whose Possible values are SOURCE, CLASS and RUNTIME

You can see in this annotation class i’m expecting the UserRoles, thats where i’m defining which APIs can use which roles.

My UserRoles class will looks like below

UserRoles.java

package com.bokks.micro.springbootrestapi.model;

public enum UserRoles{
    ADMIN,ROLE1,ROLE2,ROLE3,ROLE4;
}

How to use this annotation in your APIs. you can see in the RestApiController.java i have used it

@Secured(authorizedBy = {UserRoles.ADMIN,UserRoles.ROLE1}) // Annotation will allow Role based authorization fot the APIs
@RequestMapping(value = "/version", method = RequestMethod.GET)
public ResponseEntity<String> getVersion() {
    logger.info("API version details : SpringBootRestAPI Verion 1.0");
    return new ResponseEntity<String>("SpringBootRestAPI Verion 1.0", HttpStatus.OK);
}

Creating this annotation will not be enough to trigger your logic. So you need to implement your logic to check the authentication header.

6. Creating Filter to check the API token.

To do this i have to use Spring Around aspect. Before dive into it, I will explain what is Spring AOP.

Aspect Oriented Programming Core Concepts

  1. Aspect: An aspect is a class that implements enterprise application concerns that cut across multiple classes, such as transaction management. Aspects can be a normal class configured through Spring XML configuration or we can use Spring AspectJ integration to define a class as Aspect using @Aspect annotation.
  2. Join Point: A join point is the specific point in the application such as method execution, exception handling, changing object variable values etc. In Spring AOP a join points is always the execution of a method.
  3. Advice: Advices are actions taken for a particular join point. In terms of programming, they are methods that gets executed when a certain join point with matching pointcut is reached in the application. You can think of Advices as Struts2 interceptors or Servlet Filters.
  4. Pointcut: Pointcut are expressions that is matched with join points to determine whether advice needs to be executed or not. Pointcut uses different kinds of expressions that are matched with the join points and Spring framework uses the AspectJ pointcut expression language.
  5. Target Object: They are the object on which advices are applied. Spring AOP is implemented using runtime proxies so this object is always a proxied object. What is means is that a subclass is created at runtime where the target method is overridden and advices are included based on their configuration.
  6. AOP proxy: Spring AOP implementation uses JDK dynamic proxy to create the Proxy classes with target classes and advice invocations, these are called AOP proxy classes. We can also use CGLIB proxy by adding it as the dependency in the Spring AOP project.
  7. Weaving: It is the process of linking aspects with other objects to create the advised proxy objects. This can be done at compile time, load time or at runtime. Spring AOP performs weaving at the runtime.

AOP Advice Types

Base on the execution strategy of advices, they are as per following types.

  1. Before Advice: These advices runs before the execution of join point methods. We can use @Beforeannotation to mark an advice type as Before advice.
  2. After (finally) Advice: An advice that gets executed after the join point method finishes executing, whether normally or by throwing an exception. We can create after advice using @After annotation.
  3. After Returning Advice: Sometimes we want advice methods to execute only if the join point method executes normally. We can use @AfterReturning annotation to mark a method as after returning advice.
  4. After Throwing Advice: This advice gets executed only when join point method throws exception, we can use it to rollback the transaction declaratively. We use @AfterThrowing annotation for this type of advice.
  5. Around Advice: This is the most important and powerful advice. This advice surrounds the join point method and we can also choose whether to execute the join point method or not. We can write advice code that gets executed before and after the execution of the join point method. It is the responsibility of around advice to invoke the join point method and return values if the method is returning something. We use @Around annotation to create around advice methods.

The points mentioned above may sound confusing but when we will look at the implementation of Spring AOP, things will be more clear. Let’s start looking in to our scenario.

AuthenticationFilter.java

package com.bokks.micro.springbootrestapi.filters;

import com.bokks.micro.springbootrestapi.model.UserRoles;
import com.bokks.micro.springbootrestapi.service.TokenService;
import com.bokks.micro.springbootrestapi.service.UserService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

@Aspect
@Component
public final class AuthenticationFilter {

    public static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);

    private static final String REALM = "example";
    private static final String AUTHENTICATION_SCHEME = "Bearer";
    @Autowired
    HttpServletRequest request;
    @Autowired
    TokenService tokenService;
    @Autowired
    UserService userService;

    private boolean isTokenBasedAuthentication(String authorizationHeader) {

        // Check if the Authorization header is valid
        // It must not be null and must be prefixed with "Bearer" plus a whitespace
        // The authentication scheme comparison must be case-insensitive
        logger.info("Check the token is in valid format it should followed with 'Bearer' plus a whitespace");
        return authorizationHeader != null && authorizationHeader.toLowerCase()
                .startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
    }

    private boolean isValidToken(String token) {

        // Check if the Authorization header is valid
        // It must not be null and must be prefixed with "Bearer" plus a whitespace
        // The authentication scheme comparison must be case-insensitive
        return tokenService.isTokenValid(token);
    }

    /**
     * This method will be invoke when the APIs having enable the @Secured annotation
     * Where this api will extract the roles assign for the specific apis and authetication
     * header send in the HTTP request.
     * Based on the authorization header method will revoke accessing to the API.
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around(" @annotation(com.bokks.micro.springbootrestapi.filters.Secured)")
    public Object validateAspect(ProceedingJoinPoint pjp) throws Throwable {

        try {
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            Method method = signature.getMethod();
            boolean authorizedrequest = false;

            Secured authorizedByType = method.getAnnotation(Secured.class);
            UserRoles[] rolesOfTheAPI = authorizedByType.authorizedBy();


            String authorizationHeaderString = request.getHeader(HttpHeaders.AUTHORIZATION);
            logger.info("Header received for Authorization {}",authorizationHeaderString);
            String authorizationTokenWithoutAuthScehma = authorizationHeaderString.substring(AUTHENTICATION_SCHEME.length()).trim();
            logger.info("Token received for Authorization {}",authorizationTokenWithoutAuthScehma);

            if (!isTokenBasedAuthentication(authorizationHeaderString)||!isValidToken(authorizationTokenWithoutAuthScehma)) {
                logger.info("Unauthorized token found");
                return new ResponseEntity(HttpStatus.UNAUTHORIZED);
            }

            Enum userRole = getUserRolerFromToken(authorizationTokenWithoutAuthScehma);

            for (Enum rolesAllowed : rolesOfTheAPI) {
                logger.info("Roles allowed by the API {}",rolesAllowed);
                if (rolesAllowed.equals(userRole)) {
                    logger.info("Valid authorization found in the token provided for the {}",userRole);
                    authorizedrequest = true;
                }
            }

            if (authorizedrequest) {
                logger.info("Valid Authorization provided for method{}",method.getName());
                return pjp.proceed();
            }
            logger.info("Request UnAuthorized");
            return new ResponseEntity(HttpStatus.UNAUTHORIZED);
        }catch (Exception e){
            logger.error("Exception thwon while processing  : {}",e.getCause());
            return new ResponseEntity(e.getMessage(),HttpStatus.UNAUTHORIZED);
        }

    }

    private UserRoles getUserRolerFromToken(String token) {

        String userName = tokenService.findByToken(token).getUsername();
        UserRoles userRoles = userService.findByUsername(userName).getUserRole();

        return userRoles;
    }
}


If you check my code you can see my point cut as ” @annotation(com.bokks.micro.springbootrestapi.filters.Secured)” which means whenever this annotation triggers it will goes through my filter method. There you can implement your logic to check the authorization.

You can see on the top of the class i have autowired “HttpServletRequest” which provides me to get the header details of the request.

Ok we are now done with coding.

7. Build Application

since this is a maven application you can use spring boot maven plugin to bundle it as a spring boot application.

Add below on your pom.xml

<build>
    <plugins>
        <plugin><!-- Include if you want to make an executable jar[FAT JAR which
   includes all dependencies along with sprinboot loader] that you can run on 
   commandline using java -jar NAME -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

then build application using;

mvn clean install

8. Running the application

Since this is a SpringBoot application it carries embedded tomcat and you can bundle it as jar;

go to the build jar location

java -jar youapplication.jar

Screenshot 2018-12-31 at 6.43.52 PM.png

9. Testing

Get The API token from my service. invoke the login API.

Screenshot 2018-12-31 at 6.49.48 PM

Use the token received from the API to invoke the version API.

Screenshot 2018-12-31 at 6.54.58 PM.png

Now let’s see a user who is having ROLE3 as the user role, is able to access this version API.

Screenshot 2018-12-31 at 6.58.09 PM.png

Our API is not allowing user3 to access this API, because in the annotation i have only given permission to ADMIN and ROLE1.

Thats the end of it…

Happy Coding 🙂

You can find complete implementation in my github repo.

More over similar kind of annotation based implementation you can use in jersey as well. but its using ContainerRequestFilter to filter your request. You can see this on how to implement it in jersey based REST applications. This way it impressed me to implement in REST based spring application.