quarkus-security

Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill quarkus-security
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Quarkus Security Review

Best practices for securing Quarkus applications with authentication, authorization, and input validation.

When to Activate

  • Adding authentication (JWT, OIDC, Basic Auth)
  • Implementing authorization with @RolesAllowed or SecurityIdentity
  • Validating user input (Bean Validation, custom validators)
  • Configuring CORS or security headers
  • Managing secrets (Vault, environment variables, config sources)
  • Adding rate limiting or brute-force protection
  • Scanning dependencies for CVEs
  • Working with MicroProfile JWT or SmallRye JWT

Authentication

JWT Authentication

// Resource protected with JWT

@Path("/api/protected")

@Authenticated

public class ProtectedResource {

  @Inject

  JsonWebToken jwt;

  @Inject

  SecurityIdentity securityIdentity;

  @GET

  public Response getData() {

    String username = jwt.getName();

    Set<String> roles = jwt.getGroups();

    return Response.ok(Map.of(

        "username", username,

        "roles", roles,

        "principal", securityIdentity.getPrincipal().getName()

    )).build();

  }

}

Configuration (application.properties):

mp.jwt.verify.publickey.location=publicKey.pem

mp.jwt.verify.issuer=https://auth.example.com

# OIDC

quarkus.oidc.auth-server-url=https://auth.example.com/realms/myrealm

quarkus.oidc.client-id=backend-service

quarkus.oidc.credentials.secret=${OIDC_SECRET}

Custom Authentication Filter

@Provider

@Priority(Priorities.AUTHENTICATION)

public class CustomAuthFilter implements ContainerRequestFilter {

  @Inject

  SecurityIdentity identity;

  @Override

  public void filter(ContainerRequestContext requestContext) {

    String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);

    // Reject immediately if header is absent or malformed

    if (authHeader == null || !authHeader.startsWith("Bearer ")) {

      requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());

      return;

    }

    String token = authHeader.substring(7);

    if (!validateToken(token)) {

      requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());

    }

  }

  private boolean validateToken(String token) {

    // Token validation logic

    return true;

  }

}

Authorization

Role-Based Access Control

@Path("/api/admin")

@RolesAllowed("ADMIN")

public class AdminResource {

  @GET

  @Path("/users")

  public List<UserDto> listUsers() {

    return userService.findAll();

  }

  @DELETE

  @Path("/users/{id}")

  @RolesAllowed({"ADMIN", "SUPER_ADMIN"})

  public Response deleteUser(@PathParam("id") Long id) {

    userService.delete(id);

    return Response.noContent().build();

  }

}

@Path("/api/users")

public class UserResource {

  @Inject

  SecurityIdentity securityIdentity;

  @GET

  @Path("/{id}")

  @RolesAllowed("USER")

  public Response getUser(@PathParam("id") Long id) {

    // Check ownership

    if (!securityIdentity.hasRole("ADMIN") &#x26;&#x26;

        !isOwner(id, securityIdentity.getPrincipal().getName())) {

      return Response.status(Response.Status.FORBIDDEN).build();

    }

    return Response.ok(userService.findById(id)).build();

  }

  private boolean isOwner(Long userId, String username) {

    return userService.isOwner(userId, username);

  }

}

Programmatic Security

@ApplicationScoped

public class SecurityService {

  @Inject

  SecurityIdentity securityIdentity;

  public boolean canAccessResource(Long resourceId) {

    if (securityIdentity.isAnonymous()) {

      return false;

    }

    if (securityIdentity.hasRole("ADMIN")) {

      return true;

    }

    String userId = securityIdentity.getPrincipal().getName();

    return resourceRepository.isOwner(resourceId, userId);

  }

}

Input Validation

Bean Validation

// BAD: No validation

@POST

public Response createUser(UserDto dto) {

  return Response.ok(userService.create(dto)).build();

}

// GOOD: Validated DTO

public record CreateUserDto(

    @NotBlank @Size(max = 100) String name,

    @NotBlank @Email String email,

    @NotNull @Min(18) @Max(150) Integer age,

    @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$") String phone

) {}

@POST

@Path("/users")

public Response createUser(@Valid CreateUserDto dto) {

  User user = userService.create(dto);

  return Response.status(Response.Status.CREATED).entity(user).build();

}

Custom Validators

@Target({ElementType.FIELD, ElementType.PARAMETER})

@Retention(RetentionPolicy.RUNTIME)

@Constraint(validatedBy = UsernameValidator.class)

public @interface ValidUsername {

  String message() default "Invalid username format";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};

}

public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {

  @Override

  public boolean isValid(String value, ConstraintValidatorContext context) {

    if (value == null) return false;

    return value.matches("^[a-zA-Z0-9_-]{3,20}$");

  }

}

// Usage

public record CreateUserDto(

    @ValidUsername String username,

    @NotBlank @Email String email

) {}

SQL Injection Prevention

Panache Active Record (Safe by Default)

// GOOD: Parameterized queries with Panache

List<User> users = User.list("email = ?1 and active = ?2", email, true);

Optional<User> user = User.find("username", username).firstResultOptional();

// GOOD: Named parameters

List<User> users = User.list("email = :email and age > :minAge",

    Parameters.with("email", email).and("minAge", 18));

Native Queries (Use Parameters)

// BAD: String concatenation

@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)

// GOOD: Parameterized native query

@Entity

public class User extends PanacheEntity {

  public static List<User> findByEmailNative(String email) {

    return getEntityManager()

        .createNativeQuery("SELECT * FROM users WHERE email = :email", User.class)

        .setParameter("email", email)

        .getResultList();

  }

}

Password Hashing

@ApplicationScoped

public class PasswordService {

  public String hash(String plainPassword) {

    return BcryptUtil.bcryptHash(plainPassword);

  }

  public boolean verify(String plainPassword, String hashedPassword) {

    return BcryptUtil.matches(plainPassword, hashedPassword);

  }

}

// In service

@ApplicationScoped

public class UserService {

  @Inject

  PasswordService passwordService;

  @Transactional

  public User register(CreateUserDto dto) {

    String hashedPassword = passwordService.hash(dto.password());

    User user = new User();

    user.email = dto.email();

    user.password = hashedPassword;

    user.persist();

    return user;

  }

  public boolean authenticate(String email, String password) {

    return User.find("email", email)

        .firstResultOptional()

        .map(u -> passwordService.verify(password, u.password))

        .orElse(false);

  }

}

CORS Configuration

# application.properties

quarkus.http.cors=true

quarkus.http.cors.origins=https://app.example.com,https://admin.example.com

quarkus.http.cors.methods=GET,POST,PUT,DELETE

quarkus.http.cors.headers=accept,authorization,content-type,x-requested-with

quarkus.http.cors.exposed-headers=Content-Disposition

quarkus.http.cors.access-control-max-age=24H

quarkus.http.cors.access-control-allow-credentials=true

Secrets Management

# application.properties - NO SECRETS HERE

# Use environment variables

quarkus.datasource.username=${DB_USER}

quarkus.datasource.password=${DB_PASSWORD}

quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}

# Or use Vault

quarkus.vault.url=https://vault.example.com

quarkus.vault.authentication.kubernetes.role=my-role

HashiCorp Vault Integration

@ApplicationScoped

public class SecretService {

  @ConfigProperty(name = "api-key")

  String apiKey; // Fetched from Vault

  public String getSecret(String key) {

    return ConfigProvider.getConfig().getValue(key, String.class);

  }

}

Rate Limiting

Security Note: Never use X-Forwarded-For directly — clients can spoof it.

Use the actual remote address from the servlet request, or an authenticated

identity (API key, JWT subject) when available.

@ApplicationScoped

public class RateLimitFilter implements ContainerRequestFilter {

  private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();

  @Inject

  HttpServletRequest servletRequest;

  @Override

  public void filter(ContainerRequestContext requestContext) {

    String clientId = getClientIdentifier();

    RateLimiter limiter = limiters.computeIfAbsent(clientId,

        k -> RateLimiter.create(100.0)); // 100 requests per second

    if (!limiter.tryAcquire()) {

      requestContext.abortWith(

          Response.status(429)

              .entity(Map.of("error", "Too many requests"))

              .build()

      );

    }

  }

  private String getClientIdentifier() {

    // Use the container-provided remote address (not X-Forwarded-For).

    // If behind a trusted proxy, configure quarkus.http.proxy.proxy-address-forwarding=true

    // so getRemoteAddr() returns the real client IP.

    return servletRequest.getRemoteAddr();

  }

}

Security Headers

@Provider

public class SecurityHeadersFilter implements ContainerResponseFilter {

  @Override

  public void filter(ContainerRequestContext request, ContainerResponseContext response) {

    MultivaluedMap<String, Object> headers = response.getHeaders();

    // Prevent clickjacking

    headers.putSingle("X-Frame-Options", "DENY");

    // XSS protection

    headers.putSingle("X-Content-Type-Options", "nosniff");

    headers.putSingle("X-XSS-Protection", "1; mode=block");

    // HSTS

    headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");

    // CSP — avoid 'unsafe-inline' for script-src as it negates XSS protection;

    // use nonces or hashes instead. 'unsafe-inline' for style-src is acceptable

    // when CSS frameworks require it, but prefer nonces where possible.

    headers.putSingle("Content-Security-Policy",

        "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");

  }

}

Audit Logging

@ApplicationScoped

public class AuditService {

  private static final Logger LOG = Logger.getLogger(AuditService.class);

  @Inject

  SecurityIdentity securityIdentity;

  public void logAccess(String resource, String action) {

    String user = securityIdentity.isAnonymous()

        ? "anonymous"

        : securityIdentity.getPrincipal().getName();

    LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",

        user, action, resource, Instant.now());

  }

}

// Usage in resource

@Path("/api/sensitive")

public class SensitiveResource {

  @Inject

  AuditService auditService;

  @GET

  @RolesAllowed("ADMIN")

  public Response getData() {

    auditService.logAccess("sensitive-data", "READ");

    return Response.ok(data).build();

  }

}

Dependency Security Scanning

# Maven

mvn org.owasp:dependency-check-maven:check

# Gradle

./gradlew dependencyCheckAnalyze

# Check Quarkus extensions

quarkus extension list --installable

Best Practices

  • Always use HTTPS in production
  • Enable JWT or OIDC for stateless authentication
  • Use @RolesAllowed for declarative authorization
  • Validate all input with Bean Validation
  • Hash passwords with BCrypt (never plaintext)
  • Store secrets in Vault or environment variables
  • Use parameterized queries to prevent SQL injection
  • Add security headers to all responses
  • Implement rate limiting for public endpoints
  • Audit sensitive operations
  • Keep dependencies updated and scan for CVEs
  • Use SecurityIdentity for programmatic checks
  • Set appropriate CORS policies
  • Test authentication and authorization paths
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card