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.
This tutorial prepares you for implementing more advanced security mechanisms in Quarkus, for example, how to use the OpenID Connect (OIDC) authentication mechanism.
先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
安装JDK 11以上版本并正确配置了
JAVA_HOME
-
Apache Maven 3.8.6
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
What you will build
The steps in this tutorial guide you through building an application that provides the following endpoints:
Endpoint | 描述 |
---|---|
|
The |
|
The |
|
The |
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 |
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:
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:
quarkus extension add 'security-jpa'
./mvnw quarkus:add-extension -Dextensions='security-jpa'
./gradlew addExtension --extensions='security-jpa'
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 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 theadmin
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 theuser
role. We are also usingSecurityContext
to get access to the currently authenticatedPrincipal
, 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. 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 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 | 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 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 the application
-
Enable Quarkus built-in basic HTTP authentication by setting the
quarkus.http.auth.basic
property totrue
: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
. -
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
-
To initialize the database with users and roles, implement the
Startup
class, as outlined in the following code snippet:
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.
In a production environment, do not store plain text passwords. As a result, the |
3. Test your application
3.1. Use Dev Services for PostgreSQL to test your application
quarkus dev
./mvnw quarkus:dev
./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.
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.
If you start your application in dev mode, |
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.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%
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 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 typeString
. -
The
@Roles
field must either be of typeString
orCollection<String>
or alternately aCollection<X>
whereX
is an entity class with oneString
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.
|
What’s next
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: