Skip to content

Latest commit

 

History

History
530 lines (411 loc) · 18.1 KB

security-basic-authentication-tutorial.adoc

File metadata and controls

530 lines (411 loc) · 18.1 KB

Secure a Quarkus application with Basic authentication and Jakarta Persistence

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

For more information about Jakarta Persistence, see the Quarkus Security with Jakarta Persistence section.

This tutorial prepares you for implementing more advanced security mechanisms in Quarkus, for example, how to use the OpenID Connect (OIDC) authentication mechanism.

What you will build

To demonstrate different authorization policies, the steps in this tutorial guide you through building an application that provides the following endpoints:

Endpoint Description

/api/public

The /api/public endpoint can be accessed anonymously.

/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 enforces the access constraint declaratively.

/api/users/me

The /api/users/me endpoint is protected by RBAC. Only users that have the user role can access the endpoint. This endpoint returns the caller’s username as a string.

Tip

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

  • Download the {quickstarts-archive-url}[archive]

  • Clone the Git repository: git clone {quickstarts-clone-url}

You can find the solution in the security-jpa-quickstart {quickstarts-tree-url}/security-jpa-quickstart[directory].

1. Create a Maven project

For Quarkus Security to be able to map your security source to Jakarta Persistence 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:

Note

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:

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

1.2. Using Hibernate Reactive

If you plan to use Hibernate Reactive and non-blocking database drivers, you will need to use the security-jpa-reactive extension instead. The security-jpa-reactive extension works with the same annotations and only difference in configuration is a datasource URL. Reactive datasource has URL set with the quarkus.datasource.reactive.url configuration property, instead of the quarkus.datasource.jdbc.url configuration property used by JDBC datasource.

%prod.quarkus.datasource.reactive.url=vertx-reactive:postgresql://localhost:5431/security_jpa

Please refer to the Hibernate Reactive guide for more information. On top of that, Hibernate Reactive with Panache uses its own reactive classes as io.quarkus.hibernate.reactive.panache.PanacheEntity. You can learn more about it in the Hibernate Reactive with Panache guide.

2. Write the application

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

    package org.acme.security.jpa;
    
    import jakarta.annotation.security.PermitAll;
    import jakarta.ws.rs.GET;
    import jakarta.ws.rs.Path;
    import jakarta.ws.rs.Produces;
    import jakarta.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 ensure that only users granted the admin role can access the endpoint. Add a Jakarta REST resource with the following @RolesAllowed annotation:

    package org.acme.security.jpa;
    
    import jakarta.annotation.security.RolesAllowed;
    import jakarta.ws.rs.GET;
    import jakarta.ws.rs.Path;
    import jakarta.ws.rs.Produces;
    import jakarta.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 username, all of which is loaded from the database.

    package org.acme.security.jpa;
    
    import jakarta.annotation.security.RolesAllowed;
    import jakarta.inject.Inject;
    import jakarta.ws.rs.GET;
    import jakarta.ws.rs.Path;
    import jakarta.ws.rs.core.Context;
    import jakarta.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. Define the user entity

You can now describe how you want security information to be stored in the model by adding annotations to the user entity, as outlined in the following code snippet:

package org.acme.security.jpa;

import jakarta.persistence.Entity;
import jakarta.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. The @UserDefinition annotation must be present on a single entity and can be either a regular Hibernate ORM entity or a Hibernate ORM with Panache entity.

  2. Indicates the field used for the username.

  3. Indicates the field used for the password. By default, it uses bcrypt-hashed passwords. You can configure it to use 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 the application

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

    quarkus.http.auth.basic=true`

    Note

    When secure access is required and no other authentication mechanisms are enabled, the built-in Basic authentication of Quarkus is the fallback authentication mechanism. Therefore, in this tutorial, you do not need to set the property quarkus.http.auth.basic to 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. To initialize the database with users and roles, implement the Startup class, as outlined in the following code snippet:

Note

In this tutorial, a PostgreSQL database is used for the identity store. Hibernate ORM automatically creates the database schema on startup. This approach is suitable for development but needs to be revised for production.

package org.acme.security.jpa;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Singleton;
import jakarta.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.

Note

In a production environment, do not store plain text passwords. As a result, the security-jpa defaults to using bcrypt-hashed passwords.

3. Test your application

3.1. Use Dev Services for PostgreSQL to test your application

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.

The following properties configuration demonstrates how you can enable PostgreSQL testing to run in production (prod) mode only. In this scenario, Dev Services for PostgreSQL launches and configures a PostgreSQL test container.

%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

If you add the %prod. profile prefix, data source properties are not visible to Dev Services for PostgreSQL and are only observed by an application running in production mode.

To write the integration test, use the following code sample:

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 in this code sample, you do not need to start the test container from the test code.

Note

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 the following example to start the 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

3.3. Compile and run the application

Compile and run your Quarkus application by using one of the following methods:

3.3.1. JVM mode

Compile the application: {includes}/devtools/build.adoc Run the application:

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

3.3.2. Native mode

Compile the application:

Run the application:

./target/security-jpa-quickstart-runner

3.4. Access and test the application security

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.

  • Connect to a protected endpoint anonymously:

$ 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
  • Connect to a protected endpoint 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

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

  • Connect to a protected endpoint as an authorized 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

3.5. Results

When you provide the credentials of an authorized user, for example, admin:admin, the Jakarta Persistence 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

What’s next

Congratulations! You have learned how to create and test a secure Quarkus application by combining the built-in Basic authentication in Quarkus with the Jakarta Persistence 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: