6. How to easily secure and encode user credential Java

The code for this project can now be found here.

Note that this repository is branched from the Micronaut template that was created in previous chapters. The original template can be found here.

This is a short post which is an extension to previous post about making JWT authentication with Micronaut.

Here we will encode the user password to prevent storing it in plain text, this is strongly advised when publishing application to production.

For reference, this post was based on implementation provided in Micronaut documentation found here.

You can follow the overview of this posts contents in the following video:

Youtube overview of using encode in Micronaut/Springboot application

Use BCrypt to encode password

To do this with Micronaut is very simple, we will utilise the BCrypt library that’s commonly used in SpringBoot. Just add the following dependency to your build.gradle file:

    implementation "org.springframework.security:spring-security-crypto:5.2.1.RELEASE"

This will bring in the BCryptPasswordEncoder class and PasswordEncoder interface that we can use.

Therefore we can start creating java/server/security/BCryptPasswordEncoderService.java

package server.security;

import edu.umd.cs.findbugs.annotations.NonNull;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.inject.Singleton;
import javax.validation.constraints.NotBlank;

@Singleton
public class BCryptPasswordEncoderService implements PasswordEncoder {

    PasswordEncoder delegate = new BCryptPasswordEncoder();

    @Override
    public String encode(@NotBlank @NonNull CharSequence rawPassword) {
        return delegate.encode(rawPassword);
    }

    @Override
    public boolean matches(@NotBlank @NonNull CharSequence rawPassword,
                           @NotBlank @NonNull String encodedPassword) {
        return delegate.matches(rawPassword, encodedPassword);
    }
}

Let’s go through what we’ve introduced:

  • Encode
  • Matches

Encode function takes in a CharSequence or a String and simply encodes it using the PasswordEncoder.

The matches function takes two parameters, one is the raw password and another an encoded password (that’s stored in the database). It then compares them using the PasswordEncoder and gives a boolean with the result.

We will look at how to integrate this in code in our register and login functions shortly.

Account Controller

Here we will check how to register our user. Don’t forget that the login is provided out of the box and we will just have to modify our authentication provider in order to get the desired effects.

We create the following: java/server/account/controller/AccountController.java.

package server.account.controller;

import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.rules.SecurityRule;
import server.account.dto.Account;
import server.account.dto.RegisterDto;
import server.account.service.AccountService;

import javax.inject.Inject;


@Secured(SecurityRule.IS_ANONYMOUS)
@Controller
public class AccountController {

    @Inject
    AccountService accountService;

    @Post("/register")
    public Account register(@Body RegisterDto user) {
        return accountService.registerUser(user);
    }
}

Let’s see what we made, first we set @Secured(SecurityRule.IS_ANONYMOUS) annotation. This allows this controller be to be used without logging in. Remember we use @Secured(SecurityRule.IS_AUTHENTICATED) in secured controllers that require authentication.

Next, we inject our AccountService which will hold the logic to register the user.

We create @Post("/register") which configures a post method at /register path.

Account Service

Next let see what’s inside the account service that we reference.

java/server/account/service/AccountService.java

package server.account.service;

import com.org.mmo_server.repository.model.tables.pojos.Users;
import org.springframework.transaction.annotation.Transactional;
import server.account.dto.Account;
import server.account.dto.RegisterDto;
import server.account.repository.AccountRepository;
import server.security.BCryptPasswordEncoderService;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.time.LocalDateTime;

@Singleton
public class AccountService {

    @Inject
    AccountRepository accountRepository;

    @Inject
    BCryptPasswordEncoderService bCryptPasswordEncoderService;

    public Account fetchAccount(String username) {
        Users user = accountRepository.fetchByUsername(username);

        if (null == user) {
            return new Account();
        }
        return new Account(user.getUsername(), user.getEmail());
    }

    @Transactional
    public Account registerUser(RegisterDto registerDto) {
        try {
            String encodedPassword = bCryptPasswordEncoderService.encode(registerDto.getPassword());

            LocalDateTime now = LocalDateTime.now();
            Users user = new Users();
            user.setEmail(registerDto.getEmail());
            user.setUsername(registerDto.getUsername());
            // ensure password is encoded. Can be done on repo level instead.
            user.setPassword(encodedPassword);
            user.setEnabled(true);
            user.setCreatedAt(now);
            user.setUpdatedAt(now);
            user.setLastLoggedInAt(now);

            accountRepository.createUser(user);

            Account account = new Account();
            account.setEmail(user.getEmail());
            account.setUsername(user.getUsername());

            return account;
        } catch (Exception e) {
            // log exception in future
            return new Account();
        }

    }
}

In the account service, we will use accountRepository and BCryptPasswordEncoderService.

From the code above, we only care about registerUser function. The fetchAccount is from one of the previous posts.

The first important note is String encodedPassword = bCryptPasswordEncoderService.encode(registerDto.getPassword());

This shows that we plan to store only the encoded password, we don’t want to log or save the raw password in any form. This is reaffirmed in user.setPassword(encodedPassword);

We then try to save the user using the repository class accountRepository.createUser(user);

After which we build our response object, which is the account, we remove a lot of information that we don’t plan to send back.

Account account = new Account();
account.setEmail(user.getEmail());
account.setUsername(user.getUsername());

Note that this is optional, i.e. in our controller we can change the response to something else.

Second note is that we do try catch because we throw exception in repository on duplicate entries. This is fine and expected, we can change this in future to a more user friendly message and do useful logging.

Account Repository

Here we connect to postgres database and we want to make sure we use the encoded password to validate credentials.

java/server/account/repository/AccountRepository.java

package server.account.repository;

import com.org.mmo_server.repository.model.tables.daos.UserRolesDao;
import com.org.mmo_server.repository.model.tables.daos.UsersDao;
import com.org.mmo_server.repository.model.tables.pojos.UserRoles;
import com.org.mmo_server.repository.model.tables.pojos.Users;
import org.jooq.Configuration;
import org.jooq.DSLContext;
import server.account.dto.AccountRoles;
import server.security.BCryptPasswordEncoderService;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import java.util.stream.Collectors;

import static com.org.mmo_server.repository.model.tables.Users.USERS;

@Singleton
public class AccountRepository {
    // Use DSL context for higher performance results
    @Inject
    DSLContext dslContext;

    @Inject
    BCryptPasswordEncoderService bCryptPasswordEncoderService;

    UsersDao usersDao;
    UserRolesDao userRolesDao;

    AccountRepository(Configuration configuration) {
        this.usersDao = new UsersDao(configuration);
        this.userRolesDao = new UserRolesDao(configuration);
    }

    public Users fetchByUsername(String username) {
        return usersDao.fetchOneByUsername(username);
    }

    public boolean validCredentials(String username, String password) {

        Users user = dslContext.selectFrom(USERS)
                .where(USERS.USERNAME.equal(username))
                .fetchAnyInto(Users.class);
        if (null == user) {
            return false;
        }

        return bCryptPasswordEncoderService.matches(password, user.getPassword());
    }

    public List<String> getRolesForUser(String username) {
        return userRolesDao.fetchByUsername(username)
                .stream()
                .map(UserRoles::getRole)
                .collect(Collectors.toList());
    }

    public Users createUser(Users user) {
        usersDao.insert(user);
        UserRoles userRoles = new UserRoles();
        userRoles.setUsername(user.getUsername());
        userRoles.setRole(AccountRoles.ROLE_USER.role);
        userRolesDao.insert(userRoles);

        return user;
    }
}

Let’s first look at createUser function. We keep it simple by trying to insert the prepared user using usersDao. As we don’t plan to call this very often, we can use DAO as it’s a smaller implementation. This function will throw database exception if there’s duplicate records, these are guaranteed from our unique indexes that we added.

Note that we could have used the BCryptPasswordEncoderService in here and encode the password, we have done it on service layer so we don’t have to but that’s a strong contender.

Next, let’s explore validCredentials function.

First we find the user by username:

        Users user = dslContext.selectFrom(USERS)
                .where(USERS.USERNAME.equal(username))
                .fetchAnyInto(Users.class);

Next we check that user exists and if not return false (invalid credentials)

if (null == user) {
    return false;
}

Finally, we utilise the function we created earlier:

bCryptPasswordEncoderService.matches(password, user.getPassword())

And if matches, we return that the credentials are valid.

That’s it, let’s just recap the authentication provider that we implemented in previous post.

java/server/security/AuthenticationProviderUserPassword.java

    @Override
    public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest,
                                                          AuthenticationRequest<?, ?> authenticationRequest) {
        return Flowable.create(emitter -> {
            String username = (String) authenticationRequest.getIdentity();
            String pw = (String) authenticationRequest.getSecret();

            boolean validCredentials = accountRepository.validCredentials(username, pw);

            if (validCredentials) {
                emitter.onNext(new UserDetails(username, accountRepository.getRolesForUser(username)));
                emitter.onComplete();
            } else {
                emitter.onError(new AuthenticationException(new AuthenticationFailed()));
            }
        }, BackpressureStrategy.ERROR);
    }

The main part to highlight is this:

boolean validCredentials = accountRepository.validCredentials(username, pw);

Essentially, the flow of this function is the same as the responsibility for comparing the encoded password falls to repository layer.

This way, the login function will work as expected.

Testing

Feel free to check the test files included in the repository which demonstrates some of the fucntionality.

For this section, as usual we will use Postman to demonstrate this working as I usually find it useful to test it live.

Let’s begin with calling our register endpoint on POST: http://localhost:8081/register

Here we can see we register a user and the response is our Account dto, which does not include the password field.

What happens if we try send the request twice (i.e. duplicate?)

Not much, we respond with 200 OK and empty hash. We can change this to a 500 server error, but I plan to handle these requests and send a useful message back. Ultimately we can send the state of request and messages to user in a handled mode. This is lower priority than some of the other work currently though.

Next, we can try login request: POST http://localhost:8081/login

And this is it, this demonstrates that our authentication provider using our updated validCredentials method in our account repository is working as expected!

In the next chapter we will start looking at implementing account characters (i.e. creating characters menu) and. todo that we will integrate MongoDB.

Stay tuned and good luck with your developments!

1 Comment

Comments are closed