Skip to content

Build fintech application with Spring Boot, Vavr and Vue.js (Part 8 – Security – backend part)

Hello! We almost finished our project. The last question to talk about is security. In this tutorial we will develop token-based authentication flow for Spring Boot API.

Table of contents

What we will do in this part

With this post I’d like to tackle the last topic of my tutorial – last, but the most important. Yes, we will talk about security, and how we can create JWT security in Spring Boot apps. Well, there are many options, ranging from manual implementations with Spring Security API to ready-made platforms, as Okta or Auth0. In this tutorial, I planned initially to use Okta, but then changed my mind, as it will leave many important (for tutorial) topics outside. So, we will do everything manually to understand how JWT works under the hood (it may not seem to be one-fits-all production solution, although our goal here is education).

What does JWT means?

We build a project that consists of two independent parts – REST API and client-side single page app. The natural choice for such systems is token-based security. Here comes a thing called JWT – or JSON Web Tokens. From technical point of view, JWT is a compact and self-contained way for securely transmitting information between parties as a JSON object (reference). I need to note, that by itself, tokens are not encrypted, only signed, so you should not place any critical data in payload. The role of tokens is not to transfer credentials between parties, but to verify an identity. Basically, we can define following steps:

  1. User logs in (using username/password or security provider or whatever)
  2. Server – if credentials are valid – issues a token
  3. Server sends the token to user
  4. User attaches the token as Authorization header to each request that calls protected routes

Also, we need to distinguish such important concepts as authorization and authentication:

  • Authorization = the process by which an app determines what level of access a specific user should have to protected resources
  • Authentication = the process by which an app may securely identify its users

Now, let dive in code

Step 1. Define IAuthService

Unlike previous parts, where we started with entities, here we will first define a service itself. IAuthService is a heart of all security and is responsible for:

  • Log in process
  • Creating of new user account
  • Validation of tokens

The code snippet below defines the specification for IAuthService:

public interface IAuthService {

    TokenResponse createAccount (SignupRequest request);

    TokenResponse login (LoginRequest request);

    TokenPayload parseToken (String token);
}

Note, that like with subscription service, IAuthService accepts request entities and return response entities. We will not cover them, but you can take them in the github repo for this project. We can’t do this by means of service alone – of course we need to have the repository to persist data from/to database, but we also need to create helper utilities.

Step 2. Define ITokenUtil and its implementation

This component will issue and validate tokens. In the utils package, that we formed in the first part create a new ITokenUtil.java class:

public interface ITokenUtil {

    String issue(String userId);

    TokenPayload parse (String token);
}

Next, let implement it:

@Component("ITokenUtil")
public class TokenUtilImpl implements ITokenUtil {

    private Key key;

    public TokenUtilImpl() {
        // for test purposes keys are generated
        this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    }

    @Override
    public String issue(String userId) {
        String token = Jwts.builder().setSubject(userId).setAudience(userId).signWith(key).compact();
        return token;
    }

    @Override
    public TokenPayload parse(String token) {
        try {
            Jws<Claims> result = Jwts.parser().setSigningKey(key).parseClaimsJws(token);
            String userId = result.getBody().getSubject();
            TokenPayload response = new TokenPayload(userId);
            return response;
       } catch (JwtException ex){
           throw new AuthenticationFailedException();
       }
    }

}

Note, that we implement utilities as interfaces first to mock them during testing. Usually, utility classes mean static methods. We also use here Java-JWT library. Another approach can be to use Nimbus JOSE-JWT library, also the first option is better for beginners as have really simple API.

Finally, let verify that everything works as expected:

public class TokenUtilImplTest {

    @Test
    public void tokenTest() {
        TokenUtilImpl util = new TokenUtilImpl();
        String userId = "user-id";
        String token = util.issue(userId);
        TokenPayload result = util.parse(token);
        assertNotNull(result);
        assertEquals(userId, result.getUserId());
    }
}

Run and assert results:

Image 1. Testing TokenUtilImpl implementation

Step 3. Define IAuthUtil and its implementation

This utility component will do hashing. I need to point out there, that for educational purposes only we will keep things simple: we will generate a salt using UUID and hash data using SHA-512 function. It is not suitable for production, please read this article before doing security in production apps!

Ok, as usual start with a specification of an interface:

public interface IAuthUtil {

    String hashPassword (String password, String salt);

    String generateSalt ();

    String hashEmail(String email);

}

Note, that the method hashEmail is used to generate MD5 hash of email which we will use as the user ID. This is done by some APIs, like Gravatar or Mailchimp – this way it will give us some interoperability with them.

Let do implementation:

@Component("IAuthUtil")
public class AuthUtilImpl implements IAuthUtil {

    @Override
    public String hashPassword(String password, String salt) {
        String data = password.concat(salt);
        String result = DigestUtils.sha512Hex(data);
        return result;
    }

    @Override
    public String generateSalt() {
        return UUID.randomUUID().toString();
    }

    @Override
    public String hashEmail(String email) {
        String result = DigestUtils.md5Hex(email.toLowerCase().trim());
        return result;
    }

}

Next, let write an unit test to verify that AuthUtilImpl works as expected:

public class AuthUtilImplTest {

    @Test
    public void hashPasswordTest(){
        AuthUtilImpl util = new AuthUtilImpl();
        String result = util.hashPassword("secret", "12345678");
        assertNotNull(result);
    }
}

Run this test and check results:

Image 2. AuthUtilImpl implementation test

Step 4. Create a new user account

Signup process (a process of creating a new user account) consits here of two parts:

  1. Implementation of AuthService method
  2. Providing /auth/signup handler in the auth controller

First step is to do the service’s component. Go to AuthServiceImpl.java and following implementation of previously specified method:

@Override
public TokenResponse createAccount(SignupRequest request) {
  String salt = authUtil.generateSalt();
  String hash = authUtil.hashPassword(request.getPassword(), salt);
  User user = new User("", hash, request.getEmail(), salt, request.getName());
  User result = repository.save(user);
  String token = tokenUtil.issue(result.getEmail());
  String userId = authUtil.hashEmail(result.getEmail());
  TokenResponse response = new TokenResponse(token, userId);
  return response;
}

What we do here is: first generate new hash and salt and then create a User object. We save the entity to database (using repository) and finally generate token data to send as TokenResponse back. Although, it seems a good idea to provide a separate IUserService to allocate the first half of work there:

@Component("IUserService")
public class UserServiceImpl implements IUserService {

    @Autowired
    private IUserRepository repository;
    @Autowired
    private IAuthUtil authUtil;

    @Override
    public User createUser(SignupRequest request) {
        String salt = authUtil.generateSalt();
        String hash = authUtil.hashPassword(request.getPassword(), salt);
        User user = new User("", hash, request.getEmail(), salt, request.getName());
        User result = repository.save(user);
        return result;
    }

}

And refactor AuthServiceImpl class accordingly:

@Component("IAuthService")
public class AuthServiceImpl implements IAuthService {

    @Autowired
    private IUserService userService;
    @Autowired private ITokenUtil tokenUtil;
    @Autowired private IAuthUtil authUtil;

    @Override
    public TokenResponse createAccount(SignupRequest request) {
        User user = userService.createUser(request);
        String token = tokenUtil.issue(user.getEmail());
        String userId = authUtil.hashEmail(user.getEmail());
        TokenResponse response = new TokenResponse(token, userId);
        return response;
    }

    // ...
}

Finally, let do controller handler that will do signup process:

@RestController
@RequestMapping("/v1/auth")
@CrossOrigin(origins = "*")
public class AuthRestController {

    @Autowired private IAuthService authService;

    @PostMapping("/signup")
    public ResponseEntity<TokenResponse> signup(@RequestBody @Valid SignupRequest payload){
        TokenResponse response = authService.createAccount(payload);
        return ResponseEntity.ok(response);
    }
}

We can test this handler with Postman now:

Image 3. Testing signup API

NB while we send here auth credentials as plain JSON object, you should not do it in real life (while I saw a lot of examples of developers that do it). Please encrypt these credentials as well use HTTPS.

Step 5. Do login process

After we created a new user, we also can do login flow. Likewise in previous step it consists of two parts:

  1. AuthService implementation of login process
  2. REST handler to do login

Start with AuthServiceImpl class. The code snippet below demonstrates an implementation of login process on AuthService behalf:

@Override
    public TokenResponse login(LoginRequest request) {
        Option<User> result = userService.getByEmail(request.getEmail());
        if (result.isEmpty())
            throw new RuntimeException();

        User user = result.get();
        String userId = authUtil.hashEmail(user.getEmail());
        String hash = authUtil.hashPassword(request.getPassword(), user.getSalt());
        if (hash.equalsIgnoreCase(user.getHash())) {
            String token = tokenUtil.issue(userId);
            TokenResponse response = new TokenResponse(token, userId);
            return response;
        } else {
            throw new RuntimeException();
        }
    }

What we do here:

  1. Find a user in database (using decomposed IUserService)
  2. Verify that user does exist
  3. Check that hashed password+salt combination in database is same as salt+provided password
  4. If everything ok, we issue a token

Now, define a controller handler for login:

@PostMapping("/signup")
public ResponseEntity<TokenResponse> signup(@RequestBody @Valid SignupRequest payload){
  TokenResponse response = authService.createAccount(payload);
  return ResponseEntity.ok(response);
}

We can now use Postman to test the login flow:

Image 4. Test login API

Step 6. Token validation

Final step of this part is to create a token validation. It is a bit tricky, as with AuthServiceImpl we need to create a totally new thing – TokenInterceptor. It will responsible for intercepting of all requests that comes to secured routes and validate their Authorization header. In case that everything is ok, it will simply let requests to continue, but in case of failure it will throw an exception and interrupt a flow of request.

Traditionally, first define a service’s method:

@Override
public TokenPayload parseToken(String token) {
  return tokenUtil.parse(token);
}

Yes! That is all, as we already did whole process in TokenUtilImpl. Now, create a new class called TokenInterceptor.java which will implement HandlerInterceptor interface from Spring Web:

@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Autowired private IAuthService authService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (request.getMethod().equalsIgnoreCase("OPTIONS")){
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        } else {
            String token = request.getHeader("Authorization");
            if (token==null) throw new AuthenticationFailedException();
            authService.parseToken(token);
            return true;
        }
    }
}

We also need to tell Spring to use this component. Define a new class for configuration called TokenInterceptorConfig.java:

@Configuration
public class TokenInterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    private final List<String> routes = List.of("/v1/invoices/*", "/v1/bills/*", "/v1/companies/*", "/v1/subscriptions/*");

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor).addPathPatterns(routes.asJava());
    }

}

Here we defined Spring to use our interceptor class on all paths that are contained in routes list. As we use use Vavr collections in our tutorial, we call asJava() method to create an immutable Java list for addPathPatterns method.

So, this is all for backend part – the final part would be devoted to client side security implementation. Meanwhile, don’t forget to signup for newsletter and follow me in socials. You can grab full code for this tutorial in my github repository.

Copy link
Powered by Social Snap