Search code examples
javamicronaut

Unable to set PasswordEncoder in Micronaut


I am very new to the Micronaut framework and decided to try it on a project, that I am working on.

I have a problem where I am trying to register a user. I have followed this guide https://guides.micronaut.io/latest/micronaut-database-authentication-provider-gradle-groovy.html but extracted the logic from the AuthenticationProvider to an Authentication Service so I ended up with this:

@Singleton
public class UserAuthProvider implements AuthenticationProvider {
    private final AuthService authService;

    public UserAuthProvider(AuthService authService) {
        this.authService = authService;
    }

    @Override
    public Publisher<AuthenticationResponse> authenticate(HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) {
        return Flux.create(emitter -> {
            var username = authenticationRequest.getIdentity().toString();
            var password = authenticationRequest.getSecret().toString();

            var user = this.authService.getValidatedUser(username, password);
            if (user != null) {
                emitter.next(AuthenticationResponse.success(username, List.of(user.roleId().toString())));
                emitter.complete();
            } else  {
                emitter.error(AuthenticationResponse.exception());
            }
        }, FluxSink.OverflowStrategy.ERROR);
    }
}

Here is the code in the AuthService:

@Singleton
public class AuthService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final RoleRepository roleRepository;

    public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, RoleRepository roleRepository) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.roleRepository = roleRepository;
    }

    public void register(RegistrationRequest registrationRequest) {
        var user = this.userRepository.findByEmailEmail(registrationRequest.email());
        if (user.isPresent()) {
            throw new EntityAlreadyExistsException(String.format(EMAIL_ALREADY_EXISTS,registrationRequest.email()));
        }

        var role = this.roleRepository.findByName(RoleEnum.ROLE_PARENT)
                .orElseThrow(() -> new EntityNotFoundException(ENTITY_NOT_FOUND));

        var newUser = new ApplicationUser(
                new Email(registrationRequest.email()),
                this.passwordEncoder.encode(registrationRequest.password()),
                registrationRequest.firstName(),
                registrationRequest.middleName(),
                registrationRequest.lastName(),
                new MobilePhone(registrationRequest.phoneNumber()),
                role
        );

        var patient = new Parent();
        newUser.setParent(patient);

        this.userRepository.save(newUser);
    }

    public UserDto getValidatedUser(String username, String password) {
        if (this.validateCredentials(username, password)) {
            return this.findByEmail(username);
        }

        return null;
    }

    private boolean validateCredentials(String username, String password) {
        var user = this.userRepository.findByEmailEmail(username)
                .orElseThrow(() -> new EntityNotFoundException(ENTITY_NOT_FOUND));

        return this.passwordEncoder.matches(password, user.getPassword());
    }

    public UserDto findByEmail(String email) {
        return this.userRepository.findByEmailEmail(email)
                .map(u -> new UserDto(
                        u.getId(),
                        u.getFirstName(),
                        u.getLastName(),
                        u.getEmail().getEmail(),
                        u.getRole().getId()))
                .orElseThrow(() -> new EntityNotFoundException(ENTITY_NOT_FOUND));
    }
}

Every time I try to create a POST request to register a user I get this error:

11:51:21.631 [default-nioEventLoopGroup-1-2] WARN i.n.c.AbstractChannelHandlerContext - An exception 'io.micronaut.context.exceptions.BeanInstantiationException: Error instantiating bean of type [server.application.services.auth.AuthService]

Message: org/apache/commons/logging/LogFactory Path Taken: new SecurityFilter(Collection securityRules,Collection authenticationFetchers,SecurityConfiguration securityConfiguration) --> new SecurityFilter(Collection securityRules,[Collection authenticationFetchers],SecurityConfiguration securityConfiguration) --> new BasicAuthAuthenticationFetcher([Authenticator authenticator]) --> new Authenticator([Collection authenticationProviders],SecurityConfiguration securityConfiguration) --> new UserAuthProvider([AuthService authService]) --> new AuthService(UserRepository userRepository,[PasswordEncoder passwordEncoder],RoleRepository roleRepository)' [enable DEBUG level for full stacktrace] was thrown by a user handler's exceptionCaught() method while handling the following exception: io.micronaut.context.exceptions.BeanInstantiationException: Error instantiating bean of type [server.application.services.auth.AuthService]

Message: org/apache/commons/logging/LogFactory Path Taken: new SecurityFilter(Collection securityRules,Collection authenticationFetchers,SecurityConfiguration securityConfiguration) --> new SecurityFilter(Collection securityRules,[Collection authenticationFetchers],SecurityConfiguration securityConfiguration) --> new BasicAuthAuthenticationFetcher([Authenticator authenticator]) --> new Authenticator([Collection authenticationProviders],SecurityConfiguration securityConfiguration) --> new UserAuthProvider([AuthService authService]) --> new AuthService(UserRepository userRepository,[PasswordEncoder passwordEncoder],RoleRepository roleRepository)

Not to paste the whore stacktrace, but here is the end of it, that makes me think that there is some kind of error instanciating the PasswordEncoder:

Caused by: java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory

at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.<init>(BCryptPasswordEncoder.java:40)
at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.<init>(BCryptPasswordEncoder.java:79)
at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.<init>(BCryptPasswordEncoder.java:56)
at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.<init>(BCryptPasswordEncoder.java:49)
at server.infrastructure.config.encoding.AuthPasswordEncoderService.<init>(AuthPasswordEncoderService.java:9)
at server.infrastructure.config.encoding.$AuthPasswordEncoderService$Definition.build(Unknown Source)
at io.micronaut.context.DefaultBeanContext.resolveByBeanFactory(DefaultBeanContext.java:2331)
... 162 common frames omitted

Caused by: java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory Caused by: java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory

This is how the AuthPasswordEncoderService looks like:

package server.infrastructure.config.encoding;

import jakarta.inject.Singleton;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Singleton
public class AuthPasswordEncoderService implements PasswordEncoder {
    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    public String encode(CharSequence rawPassword) {
        return this.passwordEncoder.encode(rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return this.passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

Solution

  • As stacktrace says

    java.lang.ClassNotFoundException: org.apache.commons.logging.LogFactory

    You are missing class LogFactory that is required for BCryptPasswordEncoder class. Spring security uses this old dependency for logging purposes, and Spring projects are using spring-jcl to fix it. You need to add spring-jcl to you build.gradle dependencies section,

    implementation 'org.springframework:spring-jcl:5.3.25'
    

    Spring JCL (Java Class Library) is a utility library that provides common functionality to Spring-related projects. It includes classes for logging, resource loading, and other common tasks that are used across multiple Spring modules.