Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Quarkus Security best practices for authentication, authorization, JWT/OIDC, RBAC, input validation, CSRF, secrets management, and dependency security.
.claude/skills/quarkus-security/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✗ | = Same ✗ | — | — |
| case-20 | ✗→✗ | = Same ✗ | — | — |
Best practices for securing Quarkus applications with authentication, authorization, and input validation.
java// 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):
propertiesmp.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}
java@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; } }
java@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") && !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); } }
java@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); } }
java// 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(); }
java@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 ) {}
java// 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));
java// 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(); } }
java@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); } }
properties# 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
properties# 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
java@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); } }
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.
java@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(); } }
java@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'"); } }
java@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(); } }
bash# Maven mvn org.owasp:dependency-check-maven:check # Gradle ./gradlew dependencyCheckAnalyze # Check Quarkus extensions quarkus extension list --installable
@RolesAllowed for declarative authorization| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.