Skip to main content

Registration Backend

First, let's start with saving the users.

Repository

https://github.com/Mehdi-HAFID/registration

Architecture

Start up a MySQL database. We use version 8. schema called identity_hub.

Entities

We have two entities that we will save.

User: A simple entity with email and password fields. The password will hold the hash of the password. The enabled field is related to Spring Security.

src/main/java/nidam/registration/entities/User.java
@Entity
@Table(name = "USERS")
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
@Email
private String email;

private String password;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "USER_AUTHORITIES", joinColumns = @JoinColumn(name = "USER_ID"),
inverseJoinColumns = @JoinColumn(name = "AUTHORITY_ID"))
private List<Authority> authorities = new ArrayList<>();

private boolean enabled;
}

Authority: lists all authorities for the user. At startup, we persisted all possible authorities. When a user is created, the authorities it has are persisted in a join table.

@Entity
@Table
public class Authority {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String name;
}

Each entity has its own JpaRepository.

Password Encoder

Nidam supports these hashing algorithms: scrypt, bcrypt, argon2, pbkdf2.

application.properties
custom.password.encoders={'scrypt', 'bcrypt', 'argon2', 'pbkdf2'}

The first algorithm in the list is selected as the _defaultç. So in this case, passwords are encoded with SCrypt. example: {scrypt}$100801$2umc2uH6XR0Cq9Odmfl5WA==$9Dgl0ZRoCisqnx5C80STGUZH2gmJe8/gHHuL9pTTmzg=

Support for hashes without an ID is supported with the use of this property: custom.password.idless.encoder="scrypt" for example this hash: $100801$2umc2uH6XR0Cq9Odmfl5WA==$9Dgl0ZRoCisqnx5C80STGUZH2gmJe8/gHHuL9pTTmzg= is missing the id \{ID\}. so Nidam will use the algorithm specified in custom.password.idless.encoder which is SCrypt by default.

If you want to make changes to change the default algorithm to BCrypt you simply change the order so that bcrypt is the first in the list.

The same goes for idless hashes you simply change the value to the desired algorithm, for example: custom.password.idless.encoder="bcrypt"

nidam/registration/config/ProjectConfig.java

@Bean
public PasswordEncoder passwordEncoder(@Value("#{${custom.password.encoders}}") List<String> encoders,
@Value("#{${custom.password.idless.encoder}}") String idlessEncoderName) {

Map<String, PasswordEncoder> encodersMapping = new HashMap<>();

if(encoders.contains("scrypt")){ encodersMapping.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());}
if(encoders.contains("bcrypt")){ encodersMapping.put("bcrypt", new BCryptPasswordEncoder());}
if(encoders.contains("argon2")){ encodersMapping.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());}
if(encoders.contains("pbkdf2")){ encodersMapping.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());}

// first in list used to encode
DelegatingPasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(encoders.get(0), encodersMapping);
// use this encoder if {id} does not exist
passwordEncoder.setDefaultPasswordEncoderForMatches(getEncoderForIdlessHash(idlessEncoderName));
return passwordEncoder;
}

Authorities

Nidam assumes that the created user has full access. By default, when the user registers, we attach all the authorities listed in the property custom.authorities={'manage-users','manage-projects'}. List all authorities of your application in this property.

For user management, meaning creating users with only a subset of the authorities by a logged-in user, you will have to implement that yourself.

info

When you read the source code, you will find that registration supports Roles in addition to Authorities. But roles support is not fully implemented just yet in other microservices. So just ignore Roles support for now. When the feature is implemented the documentation will be updated to reflect that.

UserService

The save() method receives the DTO with email and password properties and does the following:

  1. Check if an account with the same email already exists.
  2. Convert the DTO to the entity object.
  3. Validate and encode the password.
  4. Set authorities.
  5. Return the created entity as a DTO.
nidam/registration/services/UserService.java
public UserRegisteredDto save(UserRegistrationDto userDto){
if(userRepository.findUserByEmail(userDto.getEmail()).isPresent()){
throw new AlreadyExistException(format("Email %s already used!", userDto.getEmail()));
}
User user = registrationMapper.toEntity(userDto);

setPassword(user);

setAuthorities(user);

user.setEnabled(true);

user = userRepository.save(user);

UserRegisteredDto dto = registeredMapper.toDto(user);
return dto;
}

Rest Endpoint

There are two endpoints for registering the user. One without ReCaptcha support and one with. The next section will explain all ReCaptcha related code.

nidam/registration/controllers/UserRegistration.java
	@PostMapping("/register")
public ResponseEntity<UserRegisteredDto> register(@RequestBody UserRegistrationDto userDto){
UserRegisteredDto entity = userService.save(userDto);
return ResponseEntity.status(HttpStatus.CREATED).body(entity);
}

This endpoint can be used with utilities like Postman in development. In production, the ReCaptcha endpoint is used.

Google ReCaptcha support

Nidam is meant to be used in production, so support for Google ReCaptcha is implemented using version 3.

The front is meant to send the ReCaptcha key alongside email and password values.

nidam/registration/services/dto/UserRegistrationCaptchaDto.java
public class UserRegistrationCaptchaDto {
private String email;
private String password;
private String recaptchaKey;
}

Which the RecaptchaService validates before continuing the same process described above if the check is successful.

The Endpoint

nidam/registration/controllers/UserRegistration.java
	@PostMapping("/registerCaptcha")
public ResponseEntity<UserRegisteredDto> register(@RequestBody UserRegistrationCaptchaDto userDto){}

The save() method in UserService is overloaded. The method first checks with Google Recaptcha backend if the request is valid.

nidam/registration/services/UserService.java
	public UserRegisteredDto save(UserRegistrationCaptchaDto userDto){
boolean result = recaptchaService.validateCaptcha(userDto.getRecaptchaKey());
if(!result){
throw new ReCaptchaException("Captcha Error");
}
return save(userRegistrationCaptchaMapper.toEntity(userDto));
}
nidam/registration/services/RecaptchaService.java
@Service
public class RecaptchaService {
@Value("#{${custom.recaptcha.secret}}")
private String recaptchaSecret;

private ReCaptchaProxy reCaptchaProxy;

public boolean validateCaptcha(String key){
MultiValueMap<String, String> requestMap = new LinkedMultiValueMap<>();
requestMap.add("secret", recaptchaSecret);
requestMap.add("response", key);

CaptchaResponse captchaResponse = reCaptchaProxy.validateReCaptcha(requestMap);
if(captchaResponse == null || captchaResponse.getSuccess() == null){
return false;
}
return captchaResponse.getSuccess();
}

}

validateCaptcha() method uses Spring OpenFeign to send requests to Google ReCaptcha backend. RestTemplate has been put in maintenance mode starting with Spring 5.

note

custom.recaptcha.secret is the private key provided to you by ReCaptcha. You can use your own or use the one that comes with this backend for development. The frontend on the next page uses the public key of this secret key, so if you use your own make sure to update the frontend key also.

nidam/registration/proxy/ReCaptchaProxy.java
@FeignClient(name = "recaptcha", url = "https://www.google.com/recaptcha/api/siteverify")
public interface ReCaptchaProxy {

@PostMapping
public CaptchaResponse validateReCaptcha(@RequestBody MultiValueMap<String, String> requestMap);
}

In the RecaptchaService.validateCaptcha() method, you implement your custom recaptcha V3 code. Nidam simply relies on the boolean in the response. So CaptchaResponse is simply a boolean holder.

If the ReCaptcha check succeeds, then the previous non-recaptcha method is called UserService.save(UserRegistrationDto userDto).

Rest of the Configuration

Finally, we have to specify SecurityFilterChain Bean.

nidam/registration/config/ProjectConfig.java
@Configuration
@EnableFeignClients(basePackages = "nidam.registration.proxy") // #1
public class ProjectConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests.dispatcherTypeMatchers(DispatcherType.ERROR).permitAll(). // #2
requestMatchers(HttpMethod.POST,"/register", "/registerCaptcha").permitAll(). // #3
anyRequest().denyAll()
);
http.csrf((csrf) -> csrf.disable()); // #4
http.httpBasic(httpSecurityHttpBasicConfigurer -> httpSecurityHttpBasicConfigurer.disable()); // #5
http.cors(c -> {
CorsConfigurationSource source = request -> { // #6
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:4001", "http://127.0.0.1:4001",
"http://localhost:7080", "http://127.0.0.1:7080"));
config.setAllowedMethods(List.of("*"));
config.setAllowedHeaders(List.of("*"));
return config;
};
c.configurationSource(source);
});
return http.build();
}
}
  1. We enable OpenFeign for the proxy package.
  2. Spring has a weird behavior where when the code throws an exception, it converts it to Access Denied. See Stackoverflow & docs
  3. We permit access to the two endpoints with the POST method and deny everything else.
  4. Disable CSRF since we're using different origins.
  5. Disable Spring Boot autoconfiguration of basic authentication
  6. CORS configuration: the front is hosted at port 4001 -explained on the next page-. Allows all methods and headers (for CORS preflight requests)
    tip

    The finished application will be accessed through a reverse proxy at a different port 7080. But for now, we will simply access the front through its port. Don't let this bother you for now.

First Time Run

The first time you run the registration backend you will have to uncomment two portions of code so that database tables are generated.

application.properties
spring.sql.init.mode=always
spring.jpa.generate-ddl=true

And

nidam/registration/RegistrationApplication.java
@Value("#{${custom.authorities}}")
private List<String> authorities;

@Bean
ApplicationRunner configureRepository(AuthorityRepository authorityRepository) {
return args -> {
for (String authority : authorities) {
Authority auth = new Authority(authority);
authorityRepository.save(auth);
}
};
}

Run the application using: mvn spring-boot:run. Check that the tables are created in the schema.

Create The First Account

Using Postman or a similar tool, we will create our first account using the non-recaptcha endpoint. First User

Result:

{
"id": 27,
"email": "mehdi@nidam.com",
"authorities": [
"manage-users",
"manage-projects"
]
}

Other Info

You're supposed to download the code and read through it since everyone will customize it differently. You want to take a look at pom.xml and application.properties first.

MongoDB Support

MongoDB support is planned for future versions.

This concludes the registration backend.