Quarkus - Using Security with a JDBC Realm

This guide demonstrates how your Quarkus application can use a database to store your user identities.

Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.5.3+

Architecture

In this example, we build a very simple microservice which offers three endpoints:

  • /api/public

  • /api/users/me

  • /api/admin

The /api/public endpoint can be accessed anonymously.The /api/admin endpoint is protected with RBAC (Role-Based Access Control) where only users granted with the admin role can access. At this endpoint, we use the @RolesAllowed annotation to declaratively enforce the access constraint.The /api/users/me endpoint is also protected with RBAC (Role-Based Access Control) where only users granted with the user role can access. As a response, it returns a JSON document with details about the user.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step.However, you can go right to the completed example.

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the security-jdbc-quickstart directory.

Creating the Maven Project

First, we need a new project. Create a new project with the following command:

  1. mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=security-jdbc-quickstart \
  4. -Dextensions="elytron-security-jdbc, jdbc-postgresql, resteasy"
  5. cd security-jdbc-quickstart
Don’t forget to add the database connector library of choice. Here we are using PostgreSQL as identity store.

This command generates a Maven project, importing the elytron-security-jdbc extensionwhich is an wildfly-elytron-realm-jdbc adapter for Quarkus applications.

Writing the application

Let’s start by implementing the /api/public endpoint. As you can see from the source code below, it is just a regular JAX-RS resource:

  1. package org.acme.elytron.security.jdbc;
  2. import javax.annotation.security.PermitAll;
  3. import javax.ws.rs.GET;
  4. import javax.ws.rs.Path;
  5. import javax.ws.rs.Produces;
  6. import javax.ws.rs.core.MediaType;
  7. @Path("/api/public")
  8. public class PublicResource {
  9. @GET
  10. @PermitAll
  11. @Produces(MediaType.TEXT_PLAIN)
  12. public String publicResource() {
  13. return "public";
  14. }
  15. }

The source code for the /api/admin endpoint is also very simple. The main difference here is that we are using a @RolesAllowed annotation to make sure that only users granted with the admin role can access the endpoint:

  1. package org.acme.elytron.security.jdbc;
  2. import javax.annotation.security.RolesAllowed;
  3. import javax.ws.rs.GET;
  4. import javax.ws.rs.Path;
  5. import javax.ws.rs.Produces;
  6. import javax.ws.rs.core.MediaType;
  7. @Path("/api/admin")
  8. public class AdminResource {
  9. @GET
  10. @RolesAllowed("admin")
  11. @Produces(MediaType.TEXT_PLAIN)
  12. public String adminResource() {
  13. return "admin";
  14. }
  15. }

Finally, let’s consider the /api/users/me endpoint. As you can see from the source code below, we are trusting only users with the user role.We are using SecurityContext to get access to the current authenticated Principal and we return the user’s name. This information is loaded from the database.

  1. package org.acme.elytron.security.jdbc;
  2. import javax.annotation.security.RolesAllowed;
  3. import javax.inject.Inject;
  4. import javax.ws.rs.GET;
  5. import javax.ws.rs.Path;
  6. import javax.ws.rs.Produces;
  7. import javax.ws.rs.core.Context;
  8. import javax.ws.rs.core.MediaType;
  9. import javax.ws.rs.core.SecurityContext;
  10. @Path("/api/users")
  11. public class UserResource {
  12. @GET
  13. @RolesAllowed("user")
  14. @Path("/me")
  15. @Produces(MediaType.APPLICATION_JSON)
  16. public String me(@Context SecurityContext securityContext) {
  17. return securityContext.getUserPrincipal().getName();
  18. }
  19. }

Configuring the Application

The elytron-security-jdbc extension requires at least one datasource to access to your database.

  1. quarkus.datasource.url=jdbc:postgresql:elytron-security-jdbc
  2. quarkus.datasource.driver=org.postgresql.Driver
  3. quarkus.datasource.username=quarkus
  4. quarkus.datasource.password=quarkus

In our context, we are using PostgreSQL as identity store and we init the database with users and roles.

  1. CREATE TABLE test_user (
  2. id INT,
  3. username VARCHAR(255),
  4. password VARCHAR(255),
  5. role VARCHAR(255)
  6. );
  7. INSERT INTO test_user (id, username, password, role) VALUES (1, 'admin', 'admin', 'admin');
  8. INSERT INTO test_user (id, username, password, role) VALUES (2, 'user','user', 'user');
It is probably useless but we kindly remind you that you must not store clear-text passwords in production environment ;-).The elytron-security-jdbc offers a built-in bcrypt password mapper.

We can now configure the Elytron JDBC Realm.

  1. quarkus.security.jdbc.enabled=true
  2. quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM test_user u WHERE u.username=? (1)
  3. quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true (2)
  4. quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1
  5. quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 (3)
  6. quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups

The elytron-security-jdbc extension requires at least one principal query to autenticate the user and its identity.

1We define a parameterized SQL statement (with exactly 1 parameter) which should return the user’s password plus any additional information you want to load.
2We configure the password mapper with the position of the password field in the SELECT fields and other information like salt, hash encoding, etc.
3We use attribute-mappings to bind the SELECT projection fields (ie. u.role here) to the target Principal representation attributes.
In the principal-query configuration all the index properties start at 1 (rather than 0).

Testing the Application

The application is now protected and the identities are provided by our database.The very first thing to check is to ensure the anonymous access works.

  1. $ curl -i -X GET http://localhost:8080/api/public
  2. HTTP/1.1 200 OK
  3. Content-Length: 6
  4. Content-Type: text/plain;charset=UTF-8
  5. public%

Now, let’s try a to hit a protected resource anonymously.

  1. $ curl -i -X GET http://localhost:8080/api/admin
  2. HTTP/1.1 401 Unauthorized
  3. Content-Length: 14
  4. Content-Type: text/html;charset=UTF-8
  5. Not authorized%

So far so good, now let’s try with an allowed user.

  1. $ curl -i -X GET -u admin:admin http://localhost:8080/api/admin
  2. HTTP/1.1 200 OK
  3. Content-Length: 5
  4. Content-Type: text/plain;charset=UTF-8
  5. admin%

By providing the admin:admin credentials, the extension authenticated the user and loaded their roles.The admin user is authorized to access to the protected resources.

The user admin should be forbidden to access a resource protected with @RolesAllowed("user") because it doesn’t have this role.

  1. $ curl -i -X GET -u admin:admin http://localhost:8080/api/users/me
  2. HTTP/1.1 403 Forbidden
  3. Content-Length: 34
  4. Content-Type: text/html;charset=UTF-8
  5. Forbidden%

Finally, using the user user works and the security context contains the principal details (username for instance).

  1. curl -i -X GET -u user:user http://localhost:8080/api/users/me
  2. HTTP/1.1 200 OK
  3. Content-Length: 4
  4. Content-Type: text/plain;charset=UTF-8
  5. user%

Advanced Configuration

This guide only covered an easy use case, the extension offers multiple datasources, multiple principal queries configuration as well as a bcrypt password mapper.

  1. quarkus.datasource.url=jdbc:postgresql:multiple-data-sources-users
  2. quarkus.datasource.driver=org.postgresql.Driver
  3. quarkus.datasource.username=quarkus
  4. quarkus.datasource.password=quarkus
  5. quarkus.datasource.url=jdbc:postgresql:multiple-data-sources-permissions
  6. quarkus.datasource.driver=org.postgresql.Driver
  7. quarkus.datasource.username=quarkus
  8. quarkus.datasource.password=quarkus
  9. quarkus.security.jdbc.enabled=true
  10. quarkus.security.jdbc.principal-query.sql=SELECT u.password FROM test_user u WHERE u.username=?
  11. quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true
  12. quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1
  13. quarkus.security.jdbc.principal-query.roles.sql=SELECT r.role_name FROM test_role r, test_user_role ur WHERE ur.username=? AND ur.role_id = r.id
  14. quarkus.security.jdbc.principal-query.roles.datasource=permissions
  15. quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.index=1
  16. quarkus.security.jdbc.principal-query.roles.attribute-mappings.0.to=groups

Configuration Reference

Configuration property fixed at build time - ️ Configuration property overridable at runtime

Configuration propertyTypeDefault
quarkus.security.jdbc.realm-nameThe realm namestringQuarkus
quarkus.security.jdbc.enabledIf the properties store is enabled.booleanfalse
quarkus.security.jdbc.principal-query.sqlThe sql query to find the passwordstringrequired
quarkus.security.jdbc.principal-query.datasourceThe data source to usestring
quarkus.security.jdbc.principal-query.clear-password-mapper.enabledIf the clear-password-mapper is enabled.booleanfalse
quarkus.security.jdbc.principal-query.clear-password-mapper.password-indexThe index (1 based numbering) of the column containing the clear passwordint1
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.enabledIf the bcrypt-password-mapper is enabled.booleanfalse
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.password-indexThe index (1 based numbering) of the column containing the password hashint0
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.hash-encodingA string referencing the password hash encoding ("BASE64" or "HEX")base64, hexBASE64
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-indexThe index (1 based numbering) of the column containing the Bcrypt saltint0
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.salt-encodingA string referencing the salt encoding ("BASE64" or "HEX")base64, hexBASE64
quarkus.security.jdbc.principal-query.bcrypt-password-mapper.iteration-count-indexThe index (1 based numbering) of the column containing the Bcrypt iteration countint0
quarkus.security.jdbc.principal-query.attribute-mappings."attribute-mappings".indexThe index (1 based numbering) of column to mapint0
quarkus.security.jdbc.principal-query.attribute-mappings."attribute-mappings".toThe target attribute namestringrequired
quarkus.security.jdbc.principal-query."named-principal-queries".sqlThe sql query to find the passwordstringrequired
quarkus.security.jdbc.principal-query."named-principal-queries".datasourceThe data source to usestring
quarkus.security.jdbc.principal-query."named-principal-queries".attribute-mappings."attribute-mappings".indexThe index (1 based numbering) of column to mapint0
quarkus.security.jdbc.principal-query."named-principal-queries".attribute-mappings."attribute-mappings".toThe target attribute namestringrequired
quarkus.security.jdbc.principal-query."named-principal-queries".clear-password-mapper.enabledIf the clear-password-mapper is enabled.booleanfalse
quarkus.security.jdbc.principal-query."named-principal-queries".clear-password-mapper.password-indexThe index (1 based numbering) of the column containing the clear passwordint1
quarkus.security.jdbc.principal-query."named-principal-queries".bcrypt-password-mapper.enabledIf the bcrypt-password-mapper is enabled.booleanfalse
quarkus.security.jdbc.principal-query."named-principal-queries".bcrypt-password-mapper.password-indexThe index (1 based numbering) of the column containing the password hashint0
quarkus.security.jdbc.principal-query."named-principal-queries".bcrypt-password-mapper.hash-encodingA string referencing the password hash encoding ("BASE64" or "HEX")base64, hexBASE64
quarkus.security.jdbc.principal-query."named-principal-queries".bcrypt-password-mapper.salt-indexThe index (1 based numbering) of the column containing the Bcrypt saltint0
quarkus.security.jdbc.principal-query."named-principal-queries".bcrypt-password-mapper.salt-encodingA string referencing the salt encoding ("BASE64" or "HEX")base64, hexBASE64
quarkus.security.jdbc.principal-query."named-principal-queries".bcrypt-password-mapper.iteration-count-indexThe index (1 based numbering) of the column containing the Bcrypt iteration countint0

Future Work

  • Propose more password mappers.

  • Provide an opinionated configuration.