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.
Could this be due to CSRF protection? Do you include the CSRF token when invoking the login
action?