This guide demonstartes how you can quickly create a secure Quarkus application by using Basic AuthenticationMechanism and JPA IdentityProvider to create SecurityIdentity which is authorized by the Role Based Access Control (RBAC) layer before the access to application is permitted.

Using Basic AuthenticationMechanism with JPA IdentityProvider is better than configuring the users and roles in application.properties therefore this guide recommends to combine Basic AuthenticationMechanism and JPA IdentityProvider. We will update this recommendation to use a reactive equivalent of the JPA IdentityProvider as soon as it is introduced.

This guide will conclude with recommending how to learn more about Quarkus Security, and in particular about its OpenId Connect Authentication Mechanism.

Prerequisites

To complete this guide, you need:

  • Roughly 15 minutes

  • An IDE

  • JDK 11+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.8.6

  • Optionally the Quarkus CLI if you want to use it

  • Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)

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-jpa-quickstart directory.

Creating the Maven Project

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

CLI
quarkus create app org.acme:security-jpa-quickstart \
    --extension=security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache \
    --no-code
cd security-jpa-quickstart

To create a Gradle project, add the --gradle or --gradle-kotlin-dsl option.

For more information about how to install the Quarkus CLI and use it, please refer to the Quarkus CLI guide.

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.12.0.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-jpa-quickstart \
    -Dextensions="security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache" \
    -DnoCode
cd security-jpa-quickstart

To create a Gradle project, add the -DbuildTool=gradle or -DbuildTool=gradle-kotlin-dsl option.

This command generates a Maven project, importing the security-jpa extension which allows you to map your security source to JPA entities.

Hibernate ORM with Panache is used to store your user identities but you can also use Hibernate ORM.

Don’t forget to add the database connector library of choice. Here we are using PostgreSQL as identity store.

If you already have your Quarkus project configured, you can add the security-jpa extension to your project by running the following command in your project base directory:

CLI
quarkus extension add 'security-jpa'
Maven
./mvnw quarkus:add-extension -Dextensions="security-jpa"
Gradle
./gradlew addExtension --extensions="security-jpa"

This will add the following to your build file:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-security-jpa</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-security-jpa")

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:

package org.acme.security.jpa;

import javax.annotation.security.PermitAll;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/public")
public class PublicResource {

    @GET
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String publicResource() {
        return "public";
   }
}

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:

package org.acme.security.jpa;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String adminResource() {
         return "admin";
    }
}

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.

package org.acme.security.jpa;

import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

@Path("/api/users")
public class UserResource {

    @GET
    @RolesAllowed("user")
    @Path("/me")
    public String me(@Context SecurityContext securityContext) {
        return securityContext.getUserPrincipal().getName();
    }
}

Defining our user entity

We can now describe how our security information is stored in our model by adding a few annotations to our User entity:

package org.acme.security.jpa;

import javax.persistence.Entity;
import javax.persistence.Table;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;

@Entity
@Table(name = "test_user")
@UserDefinition (1)
public class User extends PanacheEntity {
    @Username (2)
    public String username;
    @Password (3)
    public String password;
    @Roles (4)
    public String role;

    /**
     * Adds a new user in the database
     * @param username the username
     * @param password the unencrypted password (it will be encrypted with bcrypt)
     * @param role the comma-separated roles
     */
    public static void add(String username, String password, String role) { (5)
        User user = new User();
        user.username = username;
        user.password = BcryptUtil.bcryptHash(password);
        user.role = role;
        user.persist();
    }
}

The security-jpa extension is only initialized if there is a single entity annotated with @UserDefinition.

1 This annotation must be present on a single entity. It can be a regular Hibernate ORM entity or a Hibernate ORM with Panache entity as in this example.
2 This indicates the field used for the username.
3 This indicates the field used for the password. This defaults to using bcrypt hashed passwords, but you can also configure it for clear text passwords or custom passwords.
4 This indicates the comma-separated list of roles added to the target Principal representation attributes.
5 This method allows us to add users while hashing the password with the proper bcrypt hash.

Configuring the Application

First, Basic HTTPAuthenticationMechanism has to be enabled with quarkus.http.auth.basic=true. In fact, you do not even have to set quarkus.http.auth.basic=true to enable it in this demo as Basic HTTPAuthenticationMechanism is used as a fallback authentication mechanism when a secure access is required and no other authentication mechanisms are enabled.

Next, configure the datasource. The security-jpa extension requires at least one datasource to access to your database.

quarkus.http.auth.basic=true

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus
quarkus.datasource.password=quarkus
quarkus.datasource.jdbc.url=jdbc:postgresql:security_jpa

quarkus.hibernate-orm.database.generation=drop-and-create

In our context, we are using PostgreSQL as identity store. The database schema is created by Hibernate ORM automatically on startup (change this in production), and we initialize the database with users and roles in the Startup class:

package org.acme.security.jpa;

import javax.enterprise.event.Observes;
import javax.inject.Singleton;
import javax.transaction.Transactional;

import io.quarkus.runtime.StartupEvent;


@Singleton
public class Startup {
    @Transactional
    public void loadUsers(@Observes StartupEvent evt) {
        // reset and load all test users
        User.deleteAll();
        User.add("admin", "admin", "admin");
        User.add("user", "user", "user");
    }
}

The application is now protected and the identities are provided by our database.

We kindly remind you that you must not store clear-text passwords in production environments ;-). As a result, the security-jpa defaults to using bcrypt-hashed passwords.

Testing the Application

With Dev Services for PostgreSQL

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

Lets add the integration tests before running your application in production mode.

We recommend using Dev Services for PostgreSQL for the integration testing of your application in JVM and native modes. Dev Services for PostgreSQL will launch and configure a PostgreSQL test container if PostgreSQL configuration properties are enabled only in production (prod) mode:

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=quarkus
%prod.quarkus.datasource.password=quarkus
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql:elytron_security_jpa

quarkus.hibernate-orm.database.generation=drop-and-create

Note that adding a %prod. profile prefix the datasource properties will not make them visible to Dev Services for PostgreSQL but only to the application runnnig in production mode.

Next you can write the integration test:

package org.acme.elytron.security.jpa;

import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.given;
import static org.hamcrest.core.Is.is;

import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class JpaSecurityRealmTest {

    @Test
    void shouldAccessPublicWhenAnonymous() {
        get("/api/public")
                .then()
                .statusCode(HttpStatus.SC_OK);

    }

    @Test
    void shouldNotAccessAdminWhenAnonymous() {
        get("/api/admin")
                .then()
                .statusCode(HttpStatus.SC_UNAUTHORIZED);

    }

    @Test
    void shouldAccessAdminWhenAdminAuthenticated() {
        given()
                .auth().preemptive().basic("admin", "admin")
                .when()
                .get("/api/admin")
                .then()
                .statusCode(HttpStatus.SC_OK);

    }

    @Test
    void shouldNotAccessUserWhenAdminAuthenticated() {
        given()
                .auth().preemptive().basic("admin", "admin")
                .when()
                .get("/api/users/me")
                .then()
                .statusCode(HttpStatus.SC_FORBIDDEN);
    }

    @Test
    void shouldAccessUserAndGetIdentityWhenUserAuthenticated() {
        given()
                .auth().preemptive().basic("user", "user")
                .when()
                .get("/api/users/me")
                .then()
                .statusCode(HttpStatus.SC_OK)
                .body(is("user"));
    }
}

As you can see you do not have to launch the test container from the test code.

If you start your application in devmode then Dev Services for PostgreSQL will launch a PostgreSQL devmode container for a start for you to focus on the application development. While developing you can also start adding tests one by one and run them with the Continous Testing feature - Dev Services for PostgreSQL will support these tests with a separate PostgreSQL test container which will not conflict with the devmode container.

With Curl or Browser

First, start a PostgreSQL server:

docker run --rm=true --name security-getting-started -e POSTGRES_USER=quarkus \
           -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=elytron_security_jpa \
           -p 5432:5432 postgres:14.1

Next, compile and run the application in either JVM or Native mode:

JVM mode

Compile the application:

CLI
quarkus build
Maven
./mvnw clean package
Gradle
./gradlew build

Then run it:

java -jar target/quarkus-app/quarkus-run.jar

Native Mode

Compile the application:

CLI
quarkus build --native
Maven
./mvnw package -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

and run it:

./target/security-jpa-quickstart-runner

Now you can test it with curl or your favourite browser. We will use curl in this section but you can try to access the same endpoint URLs from the browser.

The very first thing to check is to ensure the anonymous access works.

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

public%

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

$ curl -i -X GET http://localhost:8080/api/admin
HTTP/1.1 401 Unauthorized
Content-Length: 14
Content-Type: text/html;charset=UTF-8
WWW-Authenticate: Basic

Not authorized%

Note, if you are using the browser then you should see the browser displaying a Basic Authentication challenge form.

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

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

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.

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

Forbidden%

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

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

user%

Security JPA Reference Guide

Now that you have run and tested the demo, please have a look at the more specific information about preparing your JPA identity store.

Supported model types

  • The @UserDefinition class must be a JPA entity (with Panache or not).

  • The @Username and @Password field types must be of type String.

  • The @Roles field must either be of type String or Collection<String> or alternately a Collection<X> where X is an entity class with one String field annotated with the @RolesValue annotation.

  • Each String role element type will be parsed as a comma-separated list of roles.

Storing roles in another entity

You can also store roles in another entity:

@UserDefinition
@Table(name = "test_user")
@Entity
public class User extends PanacheEntity {
    @Username
    public String name;

    @Password
    public String pass;

    @ManyToMany
    @Roles
    public List<Role> roles = new ArrayList<>();
}

@Entity
public class Role extends PanacheEntity {

    @ManyToMany(mappedBy = "roles")
    public List<ExternalRolesUserEntity> users;

    @RolesValue
    public String role;
}

Password storage and hashing

By default, we consider passwords to be stored hashed with bcrypt under the Modular Crypt Format (MCF).

When you need to create such a hashed password we provide the convenient String BcryptUtil.bcryptHash(String password) function, which defaults to creating a random salt and hashing in 10 iterations (though you can specify the iterations and salt too).

with MCF you don’t need dedicated columns to store the hashing algorithm, the iterations count or the salt because they’re all stored in the hashed value.

You also have the possibility to store password using different hashing algorithm @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class):

@UserDefinition
@Table(name = "test_user")
@Entity
public class CustomPasswordUserEntity {
    @Id
    @GeneratedValue
    public Long id;

    @Column(name = "username")
    @Username
    public String name;

    @Column(name = "password")
    @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class)
    public String pass;

    @Roles
    public String role;
}

public class CustomPasswordProvider implements PasswordProvider {
    @Override
    public Password getPassword(String pass) {
        byte[] digest = DatatypeConverter.parseHexBinary(pass);
        return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest);
    }
}

WARN: you can also store passwords in clear text with @Password(PasswordType.CLEAR) but we strongly recommend against it in production.

What is Next

You have learned how to create and test a secure Quarkus application by using Basic HTTPAuthenticationMechanism, JPA IdentityProvider.

Next we recommend you to see how OpenId Connect can be used to provide a secure, single sign on access to your Quarkus endpoints. Please follow Quarkus - Using OpenID Connect to Protect Service Applications using Bearer Token Authorization and Quarkus - Using OpenID Connect to Protect Web Applications using Authorization Code Flow guides.

For a complete reference to Quarkus Security please read a Quarkus Security document.

References