Quarkus Form Authentication using JDBC
While experimenting with Quarkus and its amazing
Quinoa extension,
I wanted to add form authentication to my project.
Quarkus offers a BcryptUtil
utility class for hashing and verifying passwords. It also
provides two database authentication extensions. One for JPA and one using plain JDBC.
Since I wanted to use jOOQ for persistence, the JPA extension
would add unnecessary dependencies to my project so JDBC was the clear choice.
Unfortunately the JDBC extension doesn’t play along with the hashing utility which
expects passwords to be in the Modular Crypt Format (MCF). That’s why I decided to
do some research and implement the authentication code myself using the built-in hashing
utility.
Here’s how to get form authentication working using plain JDBC.
Let’s first get the config out of the way:
quarkus.http.auth.form.enabled=true
quarkus.http.auth.form.username-parameter=email
quarkus.http.auth.form.password-parameter=password
quarkus.http.auth.form.post-location=/login
# SPA related configuration:
# do not redirect, respond with HTTP 200 OK
quarkus.http.auth.form.landing-page=
# do not redirect, respond with HTTP 401 Unauthorized
quarkus.http.auth.form.login-page=
quarkus.http.auth.form.error-page=
# HttpOnly must be false if you want to log out on the client; it can be true if logging out from the server
quarkus.http.auth.form.http-only-cookie=true
quarkus.http.auth.session.encryption-key=06c22ca5-fb87-440e-a6ed-67da77da69f5
Now form authentication is enabled on the /login
endpoint using form inputs
named email
and password
.
Let’s set up our database with a user account next:
create table account (
id bigserial primary key,
email text unique,
password text,
role text
);
insert into
account (email, password, role)
values ('admin@example.com', '$2a$10$BaPFwpG14G3Tn.avCOnOPuAQH5CvhS9a4XkvspoJESjoWxNdv2ryi', 'admin');
The password in this case is ‘password’. Passwords can be created using the built-in BcryptUtil
class.
Now we need to provide two IdentityProvider
implementations for the form authentication
mechanism to be able to talk to the database. One is used for the initial login
whereas the second is used for refreshing the Cookie and populating the user roles.
@ApplicationScoped
public class LoginIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest> {
private final DataSource dataSource;
public LoginIdentityProvider(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Class<UsernamePasswordAuthenticationRequest> getRequestType() {
return UsernamePasswordAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest request,
AuthenticationRequestContext context) {
return context.runBlocking(() -> {
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("select password, role from account where email = ?");
) {
statement.setString(1, request.getUsername());
ResultSet resultSet = statement.executeQuery();
if (!resultSet.next()) {
throw new AuthenticationFailedException();
}
String password = resultSet.getString("password");
String role = resultSet.getString("role");
String plainTextPassword = new String(request.getPassword().getPassword());
if (!BcryptUtil.matches(plainTextPassword, password)) {
throw new AuthenticationFailedException();
}
return QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal(request.getUsername()))
.addCredential(request.getPassword())
.setAnonymous(false)
.addRole(role)
.build();
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
}
@Override
public int priority() {
return 1001;
}
}
The priority must be higher than 1000, so that it overrides any internal Quarkus
identity providers. We use AuthenticationRequestContext#runBlocking
because of the
blocking nature of JDBC.
@ApplicationScoped
public class AuthenticatedIdentityProvider implements IdentityProvider<TrustedAuthenticationRequest> {
private final DataSource dataSource;
public AuthenticatedIdentityProvider(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Class<TrustedAuthenticationRequest> getRequestType() {
return TrustedAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(TrustedAuthenticationRequest request,
AuthenticationRequestContext context) {
return context.runBlocking(() -> {
try (
Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("select role from account where email = ?");
) {
statement.setString(1, request.getPrincipal());
ResultSet resultSet = statement.executeQuery();
if (!resultSet.next()) {
throw new AuthenticationFailedException();
}
String role = resultSet.getString("role");
return QuarkusSecurityIdentity.builder()
.setPrincipal(new QuarkusPrincipal(request.getPrincipal()))
.setAnonymous(false)
.addRole(role)
.build();
} catch (SQLException e) {
throw new RuntimeException(e);
}
});
}
@Override
public int priority() {
return 1001;
}
}
That’s all we need! If you’re using jOOQ, you can also inject a DSLContext
to run the queries.