The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Secure a Quarkus application with Basic authentication

Secure your Quarkus application endpoints by combining Quarkus built-in basic HTTP authentication with the JPA identity provider to enable role-based access control (RBAC). The JPA IdentityProvider creates a SecurityIdentity instance, which is used during user authentication to verify and authorize access requests making your Quarkus application secure.

Este tutorial prepara você para implementar mecanismos de segurança mais avançados no Quarkus, por exemplo, como usar o mecanismo de autenticação OpenID Connect (OIDC).

Pré-requisitos

Para concluir este guia, você precisa:

  • Cerca de 15 minutos

  • Um IDE

  • JDK 11+ instalado com 'JAVA_HOME' configurado adequadamente

  • Apache Maven 3.8.6

  • Opcionalmente, o Quarkus CLI se você quiser usá-lo

  • Opcionalmente, Mandrel ou GraalVM instalado e configurado apropriadamente se você quiser criar um executável nativo (ou Docker se você usar uma compilação de contêiner nativo)

O que você vai construir

The steps in this tutorial guide you through building an application that provides the following endpoints:

Endpoint Descrição

/api/public

O endpoint /api/public pode ser acessado anonimamente.

/api/admin

The /api/admin endpoint is protected with role-based access control (RBAC), and only users who have been granted the admin role can access it. At this endpoint, the @RolesAllowed annotation is used to declaratively enforce the access constraint.

/api/users/me

The /api/users/me endpoint is protected with RBAC and only users that have been granted the user role can access it. A JSON document with details about the user is returned as a response.

If you just want to examine the code, you can fast-forward to the completed example by using one of the following ways:

You can find the solution in the security-jpa-quickstart directory.

1. Create a Maven project

For Quarkus security to be able to map your security source to JPA entities, ensure that the Maven project that is used in this tutorial includes the security-jpa extension. You can either create a new Maven project with the security-jpa extension or you can add the extension to an existing Maven project.

  • To create the Maven project, use the following command:

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

Para criar um projeto Gradle, adicione a opção --gradle ou --gradle-kotlin-dsl.

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.16.12.Final:create \
    -DplatformVersion=2.16 \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-jpa-quickstart \
    -Dextensions='security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache' \
    -DnoCode
cd security-jpa-quickstart

Para criar um projeto Gradle, adicione a opção '-DbuildTool=gradle' ou '-DbuildTool=gradle-kotlin-dsl'.

Hibernate ORM with Panache is used to store your user identities but you can also use Hibernate ORM. You must also add your preferred database connector library. The instructions in this example tutorial use a PostgreSQL database for the identity store.

  • To add the security-jpa extension to an existing Maven project, run the following command from your project base directory:

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

1.1. Verification

When you run either command, the following XML is added 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")

2. Escreva a aplicação

  • Let’s start by implementing the /api/public endpoint to allow all users access to access the application. Add a regular JAX-RS resource to your Java source code, as outlined in the following code snippet:

    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 similar but instead you use a @RolesAllowed annotation to make sure that only users granted the admin role can access the endpoint. Add a JAX-RS resource with the following @RolesAllowed annotation:

    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, implement the /api/users/me endpoint. As you can see from the source code example below, we are trusting only users with the user role. We are also using SecurityContext to get access to the currently authenticated Principal, and we return the user name, all of which 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();
        }
    }

2.1. Defina a entidade do usuário

Agora, você pode descrever como deseja que as informações de segurança sejam armazenadas no modelo, adicionando anotações à entidade user, conforme descrito no seguinte trecho de código:

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 to 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 initializes only if there is a single entity annotated with @UserDefinition.

1 A anotação @UserDefinition deve estar presente em uma única entidade e pode ser uma entidade Hibernate ORM comum ou uma entidade Hibernate ORM com Panache.
2 Indicates the field used for the user name.
3 Indicates the field used for the password, which defaults to using bcrypt hashed passwords but you can also configure it for plain text 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.

2.2. Configure a aplicação

  1. Enable Quarkus built-in basic HTTP authentication by setting the quarkus.http.auth.basic property to true:

    quarkus.http.auth.basic=true`

    When secure access is required and no other authentication mechanisms are enabled, Quarkus built-in basic HTTP authentication is the fallback authentication mechanism. Therefore, in this tutorial, you do not need to set the property quarkus.http.auth.basic=true.

  2. Configure at least one data source so that the security-jpa extension can access 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
  3. Para inicializar o banco de dados com usuários e funções, implemente a classe Startup, conforme descrito no trecho de código a seguir:

In this tutorial, a PostgreSQL database is used for the identity store. Hibernate ORM automatically creates the database schema on startup (change this in production).

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 user identities are provided by the specified database.

Em um ambiente de produção, não armazene senhas em claro. Como resultado, o security-jpa tem como padrão o uso de senhas com hash bcrypt.

3. Test your application

3.1. Use Dev Services for PostgreSQL to test your application

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

Add the integration tests before you run your application in production mode.

Use Dev Services for PostgreSQL for the integration testing of your application in JVM and native modes.

A configuração de propriedades a seguir demonstra como você pode permitir que o teste do PostgreSQL seja executado somente no modo de produção ( prod ). Nesse cenário, o Dev Services para PostgreSQL inicia e configura um contêiner de teste PostgreSQL .

%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

Se você adicionar o prefixo de perfil %prod. , as propriedades da fonte de dados não estarão visíveis em Dev Services para PostgreSQL e só serão observadas por uma aplicação em execução no modo de produção.

Para escrever o teste de integração, use o exemplo de código a seguir:

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"));
    }
}

Como você pode ver neste exemplo de código, não é necessário iniciar o contêiner de teste a partir do código de teste.

If you start your application in dev mode, Dev Services for PostgreSQL launches a PostgreSQL devmode container so that you can start developing your application. While developing your application, you can also start to add tests one by one and run them by using the Continuous Testing feature. Dev Services for PostgreSQL supports testing while you develop by providing a separate PostgreSQL test container that does not conflict with the devmode container.

3.2. Use curl or a browser to test your application

Use o exemplo a seguir para iniciar o servidor PostgreSQL:

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

3.3. Compile e execute a aplicação

Compile e execute sua aplicação Quarkus usando um dos métodos a seguir:

3.3.1. Modo JVM

Compile a aplicação:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

Execute a aplicação:

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

3.3.2. Modo nativo

Compile a aplicação:

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

Execute a aplicação:

./target/security-jpa-quickstart-runner

3.4. Acesse e teste a segurança da aplicação

When your application is running, you can access your application by using one of the following curl commands. You can also access the same endpoint URLs by using a browser.

  • Conecte-se a um endpoint protegido de forma anônima:

$ 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%
  • Conecte-se a um endpoint protegido de forma anônima:

$ 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%

When you use a browser to anonymously connect to a protected resource, a basic authentication form displays prompting you to enter credentials.

  • Conecte-se a um endpoint protegido como um usuário autorizado:

$ 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%

3.5. Resultados

When you provide the credentials of an authorized user, for example, admin:admin, the JPA security extension authenticates and loads the roles of the user. The admin user is authorized to access the protected resources.

If a resource is protected with @RolesAllowed("user"), the user admin is not authorized to access the resource because it is not assigned to the "user" role, as outlined in the following shell example:

$ 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, the user name user is authorized and the security context contains the principal details, for example, the user name.

$ 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%

Quarkus Security JPA information

Now that you have successfully run and tested the security quick start project, you are ready to explore more security features of Quarkus Security and the 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

Use the following sample to store roles inside 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, passwords are stored and hashed by using bcrypt under the Modular Crypt Format (MCF).

When you need to create 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. You can also specify the number of iterations and the salt.

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

You can also store passwords by using a different hashing algorithm, for example, @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class), as outlined in the following code snippet:

@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);
    }
}
In a test environment, you can also store passwords in plain text by using @Password(PasswordType.CLEAR). For applications running in production, do not store passwords in plain text.

O que vem a seguir

Congratulations! You have learned how to create and test a secure Quarkus application by combining the Quarkus built-in basic HTTP authentication with the JPA identity provider.

After you have completed this tutorial, explore some of the more advanced security mechanisms in Quarkus. Use the following information to learn how you can securely use OpenID Connect to provide secure single sign-on access to your Quarkus endpoints: