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:
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!
Pingback: 7. Java Micronaut with MongoDB – Character controller