Chapter 1. Getting started with Security by using Basic authentication and Jakarta Persistence
Chapter 1. Getting started with Security by using Basic authentication and Jakarta Persistence Get started with Quarkus Security by securing your Quarkus application endpoints with the built-in Quarkus Basic authentication and the Jakarta Persistence identity provider, enabling role-based access control. The Jakarta Persistence IdentityProvider verifies and converts a Basic authentication user name and password pair to a SecurityIdentity instance, which is used to authorize access requests, making your Quarkus application secure. For more information about Jakarta Persistence, see the Quarkus Security with Jakarta Persistence guide. This tutorial prepares you to implement more advanced security mechanisms in Quarkus, for example, how to use the OpenID Connect (OIDC) authentication mechanism. 1.1. Prerequisites To complete this guide, you need: Roughly 15 minutes An IDE JDK 17+ installed with JAVA_HOME configured appropriately Apache Maven 3.9.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) 1.2. Building your application This tutorial gives detailed steps for creating an application with endpoints that illustrate various authorization policies: Endpoint Description /api/public Accessible without authentication, this endpoint allows anonymous access. /api/admin Secured with role-based access control (RBAC), this endpoint is accessible only to users with the admin role. Access is controlled declaratively by using the @RolesAllowed annotation. /api/users/me Also secured by RBAC, this endpoint is accessible only to users with the user role. It returns the caller's username as a string. Tip To examine the completed example, download the archive or clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.8 You can find the solution in the security-jpa-quickstart directory . 1.3. Create and verify the Maven project For Quarkus Security to be able to map your security source to Jakarta Persistence entities, ensure that the Maven project in this tutorial includes the quarkus-security-jpa extension. Note Hibernate ORM with Panache is used to store your user identities, but you can also use Hibernate ORM with the quarkus-security-jpa extension. You must also add your preferred database connector library. The instructions in this example tutorial use a PostgreSQL database for the identity store. 1.3.1. Create the Maven project You can create a new Maven project with the Security Jakarta Persistence extension or add the extension to an existing Maven project. You can use either Hibernate ORM or Hibernate Reactive. To create a new Maven project with the Jakarta Persistence extension, complete one of the following steps: To create the Maven project with Hibernate ORM, use the following command: Using the Quarkus 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 and use the Quarkus CLI, see the Quarkus CLI guide. Using Maven: mvn io.quarkus.platform:quarkus-maven-plugin:3.8.5: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. For Windows users: If using cmd, (don't use backward slash \ and put everything on the same line) If using Powershell, wrap -D parameters in double quotes e.g. "-DprojectArtifactId=security-jpa-quickstart" To add the Jakarta Persistence extension to an existing Maven project, complete one of the following steps: To add the Security Jakarta Persistence extension to an existing Maven project with Hibernate ORM, run the following command from your project base directory: Using the Quarkus CLI: quarkus extension add security-jpa Using Maven: ./mvnw quarkus:add-extension -Dextensions='security-jpa' Using Gradle: ./gradlew addExtension --extensions='security-jpa' 1.3.2. Verify the quarkus-security-jpa dependency After you have run either of the preceding commands to create the Maven project, verify that the quarkus-security-jpa dependency was added to your project build XML file. To verify the quarkus-security-jpa extension, check for the following configuration: Using Maven: <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-security-jpa</artifactId> </dependency> Using Gradle: implementation("io.quarkus:quarkus-security-jpa") 1.4. Write the application Secure the API endpoint to determine who can access the application by using one of the following approaches: Implement the /api/public endpoint to allow all users access to the application. Add a regular Jakarta REST resource to your Java source code, as shown 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"; } } Implement an /api/admin endpoint that can only be accessed by users who have the admin role. 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"; } } Implement an /api/users/me endpoint that can only be accessed by users who have the user role. Use SecurityContext to get access to the currently authenticated Principal user and to return their username, all of which is retrieved 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(); } } 1.5. 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 is 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 quarkus-security-jpa extension only initializes if a single entity is annotated with @UserDefinition . 1 The @UserDefinition annotation must be present on a single entity, 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 Indicates the comma-separated list of roles added to the target principal representation attributes. 5 Allows us to add users while hashing passwords with the proper bcrypt hash. Note Don't forget to set up the Panache and PostgreSQL JDBC driver, please see Setting up and configuring Hibernate ORM with Panache for more information. 1.6. Configure the application Enable the built-in Quarkus Basic authentication mechanism 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 . Configure at least one data source in the application.properties file so the quarkus-security-jpa extension can access your database. For example: 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 To initialize the database with users and roles, implement the Startup class, as outlined in the following code snippet: 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 preceding example demonstrates how the application can be protected and identities provided by the specified database. Important In a production environment, do not store plain text passwords. As a result, the quarkus-security-jpa defaults to using bcrypt-hashed passwords. 1.7. Test your application by using Dev Services for PostgreSQL Complete the integration testing of your application in JVM and native modes by using Dev Services for PostgreSQL before you run your application in production mode. Start by adding the following dependencies to your test project: Using Maven: <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> Using Gradle: testImplementation("io.rest-assured:rest-assured") To run your application in dev mode: Using the Quarkus CLI: quarkus dev Using Maven: ./mvnw quarkus:dev Using Gradle: ./gradlew --console=plain quarkusDev The following properties configuration demonstrates how to enable PostgreSQL testing to run only in production ( prod ) mode. 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.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 When you start your application in dev mode, Dev Services for PostgreSQL launches a PostgreSQL dev mode container so that you can start developing your application. While developing your application, you can add and run tests individually 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 dev mode container. 1.7.1. Use curl or a browser to test your application Use the following example to start the PostgreSQL server: 1.7.2. Compile and run the application Compile and run your Quarkus application by using one of the following methods: JVM mode Compile the application: Using the Quarkus CLI: quarkus build Using Maven: ./mvnw install Using Gradle: ./gradlew build Run the application: java -jar target/quarkus-app/quarkus-run.jar Native mode Compile the application: Using the Quarkus CLI: quarkus build --native Using Maven: ./mvnw install -Dnative Using Gradle: ./gradlew build -Dquarkus.package.type=native Run the application: ./target/security-jpa-quickstart-1.0.0-SNAPSHOT-runner 1.7.3. Access and test the application security When your application is running, you can access its endpoints by using one of the following Curl commands. Connect to a protected endpoint anonymously: USD 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: USD 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 Connect to a protected endpoint as an authorized user: USD 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 You can also access the same endpoint URLs by using a browser. Note If you use a browser to connect to a protected resource anonymously, a Basic authentication form displays, prompting you to enter credentials. 1.7.4. Results When you provide the credentials of an authorized user, for example, admin:admin , the Jakarta Persistence security extension authenticates and loads the user's roles. 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 shown in the following example: USD 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 named user is authorized, and the security context contains the principal details, for example, the username. USD 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 1.8. What's You have successfully learned how to create and test a secure Quarkus application. This was achieved by integrating the built-in Basic authentication in Quarkus with the Jakarta Persistence identity provider. After completing this tutorial, you can explore more advanced security mechanisms in Quarkus. The following information shows you how to use OpenID Connect for secure single sign-on access to your Quarkus endpoints: OIDC Bearer token authentication OIDC code flow mechanism for protecting web applications 1.9. References Quarkus Security overview Quarkus Security architecture Other supported authentication mechanisms Identity providers OIDC Bearer token authentication OIDC code flow mechanism for protecting web applications Simplified Hibernate ORM with Panache Using Hibernate ORM and Jakarta Persistence
git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.8 quarkus create app org.acme:security-jpa-quickstart --extension='security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache' --no-code cd security-jpa-quickstart mvn io.quarkus.platform:quarkus-maven-plugin:3.8.5:create -DprojectGroupId=org.acme -DprojectArtifactId=security-jpa-quickstart -Dextensions='security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache' -DnoCode cd security-jpa-quickstart quarkus extension add security-jpa ./mvnw quarkus:add-extension -Dextensions='security-jpa' ./gradlew addExtension --extensions='security-jpa' <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-security-jpa</artifactId> </dependency> implementation("io.quarkus:quarkus-security-jpa") 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"; } } 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"; } } 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(); } } 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 is 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(); } } 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 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"); } } <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> testImplementation("io.rest-assured:rest-assured") quarkus dev ./mvnw quarkus:dev ./gradlew --console=plain quarkusDev %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 package org.acme.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")); } } 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 quarkus build ./mvnw install ./gradlew build java -jar target/quarkus-app/quarkus-run.jar quarkus build --native ./mvnw install -Dnative ./gradlew build -Dquarkus.package.type=native ./target/security-jpa-quickstart-1.0.0-SNAPSHOT-runner 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 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 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 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 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