Skip to content

Commit

Permalink
Merge pull request #1741 from gtroitsk/security-jpa-coverage
Browse files Browse the repository at this point in the history
Add coverage for Quarkus Security JPA extension
  • Loading branch information
michalvavrik authored Apr 5, 2024
2 parents efd257f + 4f769bd commit 85a5030
Show file tree
Hide file tree
Showing 29 changed files with 720 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,11 @@ Bouncy castle providers:
Verifies form-based authentication.
Verifies that Basic Authentication is not used as a fallback option when it is explicitly disabled.

### `security/jpa`

Verifies Basic authentication with Jakarta Persistence identity provider, enabling role-based access control.
User password is hashed by one of algorithms: MD5, SHA-256, SHA-512.

### `security/jwt`

Verifies token-based authn and role-based authz.
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,7 @@
<module>security/bouncycastle-fips</module>
<module>security/form-authn</module>
<module>security/https</module>
<module>security/jpa</module>
<module>security/jwt</module>
<module>security/keycloak</module>
<module>security/keycloak-authz-classic</module>
Expand Down
7 changes: 7 additions & 0 deletions security/jpa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Security JPA test coverage
Use MariaDB, MySQL, OracleDB with MD5, SHA-256 and SHA-512 hashing algorithms for custom password providers.

## How to generate hashed passwords:
- MD5: `echo -n 'user-pass' | openssl md5`
- SHA256: `echo -n 'user-pass' | sha256sum`
- SHA512: `echo -n 'user-pass' | sha512sum`
53 changes: 53 additions & 0 deletions security/jpa/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus.ts.qe</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../..</relativePath>
</parent>
<artifactId>security-jpa</artifactId>
<packaging>jar</packaging>
<name>Quarkus QE TS: Security: Jakarta Persistence</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<!-- Bouncycastle is need for passwords encodings -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mariadb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mysql</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-oracle</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-service-database</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.ts.security.jpa;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.smallrye.mutiny.Uni;

@Path("/api/admin")
public class AdminResource {

@GET
@Produces(MediaType.TEXT_PLAIN)
public Uni<String> adminResource() {
return Uni.createFrom().item("admin");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.ts.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";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.ts.security.jpa;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

import io.smallrye.mutiny.Uni;

@Path("/api/users")
public class UserResource {

@GET
@RolesAllowed("user")
@Path("/me")
public Uni<String> me(@Context SecurityContext securityContext) {
return Uni.createFrom().item(securityContext.getUserPrincipal().getName());
}
}
10 changes: 10 additions & 0 deletions security/jpa/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
quarkus.http.auth.basic=true

quarkus.http.auth.policy.user-policy.roles-allowed=user
quarkus.http.auth.policy.admin-policy.roles-allowed=admin

quarkus.http.auth.permission.user.paths=/api/users/*
quarkus.http.auth.permission.user.policy=user-policy

quarkus.http.auth.permission.admin.paths=/api/admin/*
quarkus.http.auth.permission.admin.policy=admin-policy
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.quarkus.ts.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.restassured.http.ContentType;

public abstract class BaseJpaSecurityRealmIT {

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

@Test
void createUserThatShouldNotAccessAdmin() {
String requestBody = "{\"username\": \"newUser\", \"password\": \"user\", \"role\": \"user\"}";

given().contentType(ContentType.JSON).body(requestBody).post("/create/user").then()
.statusCode(HttpStatus.SC_CREATED);

given()
.auth().preemptive().basic("newUser", "user")
.when()
.get("/api/admin")
.then()
.statusCode(HttpStatus.SC_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.ts.security.jpa;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import jakarta.xml.bind.DatatypeConverter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Path("/create/user")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class CreateUserWithMD5PassResource {
@POST
@Transactional
public Response create(String jsonString) throws JsonProcessingException, NoSuchAlgorithmException {

ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(jsonString);

String username = jsonNode.get("username").asText();
String password = jsonNode.get("password").asText();
String role = jsonNode.get("role").asText();

MessageDigest digest = MessageDigest.getInstance("MD5");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
String md5Password = DatatypeConverter.printHexBinary(hash);

MD5UserEntity user = new MD5UserEntity(username, md5Password, role);
user.persist();
return Response.ok(user).status(201).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.ts.security.jpa;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;

import org.bouncycastle.util.encoders.Hex;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Path("/create/user")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class CreateUserWithSHA256PassResource {
@POST
@Transactional
public Response create(String jsonString) throws JsonProcessingException, NoSuchAlgorithmException {

ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(jsonString);

String username = jsonNode.get("username").asText();
String password = jsonNode.get("password").asText();
String role = jsonNode.get("role").asText();

MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
String sha256Password = new String(Hex.encode(hash));

SHA256UserEntity user = new SHA256UserEntity(username, sha256Password, role);
user.persist();
return Response.ok(user).status(201).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.ts.security.jpa;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;

import org.bouncycastle.util.encoders.Hex;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

@Path("/create/user")
@ApplicationScoped
@Produces("application/json")
@Consumes("application/json")
public class CreateUserWithSHA512PassResource {
@POST
@Transactional
public Response create(String jsonString) throws JsonProcessingException, NoSuchAlgorithmException {

ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(jsonString);

String username = jsonNode.get("username").asText();
String password = jsonNode.get("password").asText();
String role = jsonNode.get("role").asText();

MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
String sha512Password = new String(Hex.encode(hash));

SHA512UserEntity user = new SHA512UserEntity(username, sha512Password, role);
user.persist();
return Response.ok(user).status(201).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.ts.security.jpa;

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 MD5PasswordProvider implements PasswordProvider {
@Override
public Password getPassword(String passwordInDatabase) {
byte[] digest = DatatypeConverter.parseHexBinary(passwordInDatabase);
return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_MD5, digest);
}
}
Loading

0 comments on commit 85a5030

Please sign in to comment.