securityquarkus

Problem logging into an application with Quarkus Renarde


I am migrating an application made with Quarkus and using Renarde for the UI. I am struggling with some problems regarding the login form. The form was working fine with the old version of Renarde I was using (3.0.1) but with the new one (3.0.19), when I submit the form, I just get a blank page. I tried to make a break point in my code but it doesn't even reach the method. The security is done with Database. I am puzzled and I don't really know what to do. Can someone help? For this I have created 2 classes

@Provider
@Priority(1)
public class AuthenticationFailedExceptionHandler implements ExceptionMapper<AuthenticationFailedException>  {
    @Override
    public Response toResponse(AuthenticationFailedException exception) {
        var removeCookie = new NewCookie.Builder("Authorization")
                .value(null)
                .path("/")
                .domain(null)
                .comment(null)
                .maxAge(0)
                .secure(false)
                .httpOnly(true)
                .build();
        var uri = getURI(Security::login);
        return temporaryRedirect(uri).cookie(removeCookie).build();
    }
}

and

@ApplicationScoped
public class UserSecuritySetup implements RenardeUserProvider, RenardeOidcHandler {
    @Inject RenardeSecurity security;

    @Override
    public RenardeUser findUser(final String tenantId, final String username) {
        return User.findByUsername(username);
    }
    @Override
    public void oidcSuccess(String tenantId, String authId) {
    }
    @Override
    public void loginWithOidcSession(String tenantId, String authId) {
    }
}

and my User implements RenardUser

@Entity
@Table(name = "user")
public class User extends PanacheEntity implements RenardeUser {
    @Column(name = "code", nullable = false) public String code;
    @Column(name = "firstname", nullable = false) public String firstname;
    @Column(name = "lastname", nullable = false) public String lastname;
    @Column(name = "created_at") public Instant createdAt;
    @ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "role_id", nullable = false) public Role role;
    @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "status_id", nullable = false) public Status status;
    @JsonbTransient @OneToOne(mappedBy = "user") public Credentials credentials;
    @OneToOne(mappedBy = "user") public UserPicture picture;

    public static User findByCode(String code) {
        return User.find("code", code).firstResult();
    }

    public static User findByUsername(String username) {
        return User.find("credentials.username", username).firstResult();
    }

    @Override
    public Set<String> roles() {
        Set<String> roles = new HashSet<>();
        roles.add(role.code);
        return roles;
    }

    @Override
    public String userId() {
        return credentials.username;
    }

    @Override
    public boolean registered() {
        return true;
    }
}

And this is what the class used to log into the application looks like

import com.dwitech.surveille.ui.control.util.Helper;
import com.dwitech.surveille.ui.entity.db.User;
import io.micrometer.core.instrument.MeterRegistry;
import io.quarkiverse.renarde.security.ControllerWithUser;
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import io.smallrye.common.annotation.Blocking;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.RestForm;
import org.mindrot.jbcrypt.BCrypt;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import static com.dwitech.surveille.ui.control.util.CurrentMethod.name;
import static io.quarkiverse.renarde.router.Router.getURI;
import static io.quarkus.elytron.security.common.BcryptUtil.matches;
import static org.jboss.logging.Logger.getLogger;

@Path("/security")
@Blocking
public class Security extends ControllerWithUser<User> {
    private static final Logger LOGGER = getLogger(Security.class);
    private final MeterRegistry registry;
    @ConfigProperty(name = "quarkus.application.name") String applicationName;
    @ConfigProperty(name = "quarkus.profile") String profile;
    @Inject Helper helper;
    @Inject Audit audit;
    @Inject DashboardHelper dashboardHelper;

    Security(MeterRegistry registry) {
        this.registry = registry;
    }

    public TemplateInstance display_login() {
        return sign_in();
    }

    public TemplateInstance display_change_password(User user) {
        return settings()
                .data("picture", helper.getPictureAsBase64(user))
                ;
    }

    @POST
    @Path("login")
    public Response login(@RestForm String username, @RestForm String password) {
        var action = name();
        var user = User.findByUsername(username);
        if (user == null || !matches(password, user.credentials.password) || !user.registered()) {
            System.out.println("username --> Invalid username/password");
            validation.addError("username", "Invalid username/password");
            flash.flash("message", "Invalid user: "+username);
            prepareForErrorRedirect();
            audit.writeAuditLog("LGN", username, action, username, "Invalid username/password");
            display_login();
        }

        var cookie = security.makeUserCookie(user);
        String userRole = user.role.code;
        System.out.println("userRole --> " + userRole);
        audit.writeAuditLog("LGN",  username, action, user.credentials.username, "logged in");
        var template = dashboardHelper.getDashboard(user);
        return Response.ok(template
                .data("source", action)
                .data("picture", helper.getPictureAsBase64(user))
                .data("user", user)
        ).cookie(cookie).build();
    }

    @CheckedTemplate(requireTypeSafeExpressions = false)
    public static class Templates {
        public static native TemplateInstance sign_in();
    }
}

This is kind of crucial for me. 2 applications rely on it and have the same behaviour.

Regards and thanks. D.


Solution

  • Could this be due to CSRF protection? Do you include the CSRF token when invoking the login action?