Chapter 2. Quarkus Security with Jakarta Persistence You can configure your application to use Jakarta Persistence to store users' identities. Quarkus provides a Jakarta Persistence identity provider, similar to the JDBC identity provider . Jakarta Persistence is suitable for use with the Basic and Form-based Quarkus Security mechanisms, which require username and password credentials. The Jakarta Persistence IdentityProvider creates a SecurityIdentity instance. During user authentication, this instance is used to verify and authorize access requests. For a practical example, see the Getting started with Security using Basic authentication and Jakarta Persistence tutorial. 2.1. Jakarta Persistence entity specification Quarkus security offers a Jakarta Persistence integration to collect usernames, passwords, and roles and store them into Jakarta Persistence database entities. The following Jakarta Persistence entity specification demonstrates how users' information needs to be stored in a Jakarta Persistence entity and correctly mapped so that Quarkus can retrieve this information from a database. The @UserDefinition annotation must be present on a Jakarta Persistence entity, regardless of whether simplified Hibernate ORM with Panache is used or not. The @Username and @Password field types are always String . The @Roles field must either be String , Collection<String> , or a Collection<X> , where X is an entity class with a single String field annotated as @RolesValue . Each String role element type is parsed as a comma-separated list of roles. The following example demonstrates storing security information by adding annotations to the user entity: 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 initializes only 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, quarkus-security-jpa uses bcrypt-hashed passwords, or you can configure plain text or custom passwords instead. 4 This indicates the comma-separated list of roles added to the target principal representation attributes. 5 This method lets you add users while hashing passwords with the proper bcrypt hash. 2.2. Jakarta Persistence entity as storage of roles Use the following example to store roles inside another Jakarta Persistence 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<User> users; @RolesValue public String role; } Note This example demonstrates storing and accessing roles. To update an existing user or create a new one, annotate public List<Role> roles with @Cascade(CascadeType.ALL) or choose a specific CascadeType . 2.3. Password storage and hashing When developing applications with Quarkus, you can decide how to manage password storage and hashing. You can keep the default password and hashing settings of Quarkus, or you can hash passwords manually. With the default option, passwords are stored and hashed with bcrypt under the Modular Crypt Format (MCF). While using MCF, the hashing algorithm, iteration count, and salt are stored as a part of the hashed value. As such, we do not need dedicated columns to keep them. Note In cryptography, a salt is a name for random data used as an additional input to a one-way function that hashes data, a password, or a passphrase. To represent passwords stored in the database that were hashed by different algorithms, create a class that implements org.wildfly.security.password.PasswordProvider as shown in the following example. The following snippet shows how to set a custom password provider that represents a password that was hashed with the SHA256 hashing algorithm. import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import io.quarkus.security.jpa.Password; import io.quarkus.security.jpa.PasswordType; import io.quarkus.security.jpa.Roles; import io.quarkus.security.jpa.UserDefinition; import io.quarkus.security.jpa.Username; @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; } import jakarta.xml.bind.DatatypeConverter; import org.wildfly.security.password.Password; import org.wildfly.security.password.interfaces.SimpleDigestPassword; import io.quarkus.security.jpa.PasswordProvider; public class CustomPasswordProvider implements PasswordProvider { @Override public Password getPassword(String passwordInDatabase) { byte[] digest = DatatypeConverter.parseHexBinary(passwordInDatabase); // Let the security runtime know that this passwordInDatabase is hashed by using the SHA256 hashing algorithm return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest); } } Tip To quickly create a hashed password, use String BcryptUtil.bcryptHash(String password) , which defaults to creating a random salt and hashing in ten iterations. This method also allows specifying the number of iterations and salt used. Warning For applications running in a production environment, do not store passwords as plain text. However, it is possible to store passwords as plain text with the @Password(PasswordType.CLEAR) annotation when operating in a test environment. Tip The Hibernate Multitenancy is supported, and you can store the user entity in a persistence unit with enabled multitenancy. However, if your io.quarkus.hibernate.orm.runtime.tenant.TenantResolver must access the io.vertx.ext.web.RoutingContext to resolve request details, you must disable proactive authentication. For more information about proactive authentication, see the Quarkus Proactive authentication guide. Configuration property fixed at build time - All other configuration properties are overridable at runtime Configuration property Type Default quarkus.security-jpa.persistence-unit-name Selects the Hibernate ORM persistence unit. Default persistence unit is used when no value is specified. Environment variable: QUARKUS_SECURITY_JPA_PERSISTENCE_UNIT_NAME string <default> 2.4. References Getting started with Security by using Basic authentication and Jakarta Persistence Identity providers Quarkus Security overview

Commands:
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(); } }
@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<User> users; @RolesValue public String role; }
import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import io.quarkus.security.jpa.Password; import io.quarkus.security.jpa.PasswordType; import io.quarkus.security.jpa.Roles; import io.quarkus.security.jpa.UserDefinition; import io.quarkus.security.jpa.Username; @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; }
import jakarta.xml.bind.DatatypeConverter; import org.wildfly.security.password.Password; import org.wildfly.security.password.interfaces.SimpleDigestPassword; import io.quarkus.security.jpa.PasswordProvider; public class CustomPasswordProvider implements PasswordProvider { @Override public Password getPassword(String passwordInDatabase) { byte[] digest = DatatypeConverter.parseHexBinary(passwordInDatabase); // Let the security runtime know that this passwordInDatabase is hashed by using the SHA256 hashing algorithm return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest); } }