Introduction to Quarkus Security
Quarkus provides a comprehensive security framework that integrates with standards like JWT, OAuth2, OpenID Connect, and MicroProfile JWT. It offers both traditional and reactive security approaches with minimal configuration.
Key Quarkus Security Features
- Built-in Authentication & Authorization
- JWT & OAuth2/OpenID Connect Support
- Role-Based Access Control (RBAC)
- MicroProfile JWT Integration
- Reactive Security
- Elytron Security
- Custom Security Extensions
Dependencies Setup
Maven Configuration
<properties> <quarkus.version>3.2.0.Final</quarkus.version> </properties> <dependencies> <!-- Quarkus Security --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-security</artifactId> </dependency> <!-- JWT Security --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc</artifactId> </dependency> <!-- MicroProfile JWT --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> <!-- Elytron Security --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-elytron-security</artifactId> </dependency> <!-- Reactive Routes Security --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-vertx-http</artifactId> </dependency> <!-- Database Security --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-security-jpa</artifactId> </dependency> <!-- Test Security --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security</artifactId> <scope>test</scope> </dependency> <!-- RESTEasy JSON-B --> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jsonb</artifactId> </dependency> </dependencies>
Basic Security Configuration
Application Properties
# Application Configuration quarkus.http.port=8080 # Basic Security quarkus.security.enabled=true # JWT Configuration quarkus.oidc.auth-server-url=https://your-oidc-provider.com quarkus.oidc.client-id=your-client-id quarkus.oidc.credentials.secret=your-client-secret # MP JWT Configuration mp.jwt.verify.publickey.location=publicKey.pem mp.jwt.verify.issuer=https://your-issuer.com # Security Roles quarkus.http.auth.permission.authenticated.paths=/* quarkus.http.auth.permission.authenticated.policy=authenticated # CORS Configuration quarkus.http.cors=true quarkus.http.cors.origins=*
JWT-Based Security Implementation
JWT Token Generator
package com.quarkus.security.jwt;
import io.smallrye.jwt.build.Jwt;
import org.eclipse.microprofile.jwt.Claims;
import java.time.Instant;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class JWTGenerator {
public static String generateUserToken(String username, Set<String> roles) {
return Jwt.issuer("https://quarkus-app.com")
.upn(username)
.subject(username)
.groups(roles)
.claim(Claims.birthdate.name(), "2000-01-01")
.expiresAt(Instant.now().plusSeconds(3600))
.sign();
}
public static String generateAdminToken() {
Set<String> roles = new HashSet<>(Arrays.asList("admin", "user"));
return generateUserToken("[email protected]", roles);
}
public static String generateUserToken() {
Set<String> roles = new HashSet<>(Arrays.asList("user"));
return generateUserToken("[email protected]", roles);
}
}
JWT Protected Resource
package com.quarkus.security.resource;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Set;
@Path("/secured")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SecuredResource {
@Inject
JsonWebToken jwt;
@Inject
@Claim(standard = Claims.upn)
String username;
@Inject
@Claim(standard = Claims.groups)
Set<String> groups;
@GET
@Path("/public")
public Response publicEndpoint() {
return Response.ok(
new Message("Public endpoint - no authentication required")
).build();
}
@GET
@Path("/user")
@RolesAllowed("user")
public Response userEndpoint() {
return Response.ok(
new Message("User endpoint - user role required. Current user: " + username)
).build();
}
@GET
@Path("/admin")
@RolesAllowed("admin")
public Response adminEndpoint() {
return Response.ok(
new Message("Admin endpoint - admin role required. Current user: " + username)
).build();
}
@GET
@Path("/profile")
@RolesAllowed({"user", "admin"})
public Response getUserProfile() {
UserProfile profile = new UserProfile(
username,
jwt.getSubject(),
groups,
jwt.getExpirationTime(),
jwt.getIssuer()
);
return Response.ok(profile).build();
}
@POST
@Path("/update")
@RolesAllowed("user")
public Response updateUserData(UpdateRequest request) {
// Business logic here
return Response.ok(new Message("Data updated for user: " + username)).build();
}
// DTO classes
public static class Message {
public String message;
public Message(String message) {
this.message = message;
}
}
public static class UserProfile {
public String username;
public String subject;
public Set<String> roles;
public long expiration;
public String issuer;
public UserProfile(String username, String subject, Set<String> roles,
long expiration, String issuer) {
this.username = username;
this.subject = subject;
this.roles = roles;
this.expiration = expiration;
this.issuer = issuer;
}
}
public static class UpdateRequest {
public String data;
}
}
Custom Authentication Mechanism
Custom Identity Provider
package com.quarkus.security.identity;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import java.util.Set;
@ApplicationScoped
public class CustomIdentityProvider implements IdentityProvider<CustomAuthenticationRequest> {
@Inject
UserService userService;
@Override
public Class<CustomAuthenticationRequest> getRequestType() {
return CustomAuthenticationRequest.class;
}
@Override
public Uni<SecurityIdentity> authenticate(CustomAuthenticationRequest request,
AuthenticationRequestContext context) {
return Uni.createFrom().item(() -> {
// Validate API key or custom token
User user = userService.validateToken(request.getToken());
if (user != null) {
return QuarkusSecurityIdentity.builder()
.setPrincipal(user::getUsername)
.addRoles(user.getRoles())
.addAttribute("user-id", user.getId())
.addCredential(request.getToken())
.build();
}
return null; // Authentication failed
});
}
}
// Custom Authentication Request
class CustomAuthenticationRequest implements io.quarkus.security.identity.request.AuthenticationRequest {
private final String token;
public CustomAuthenticationRequest(String token) {
this.token = token;
}
public String getToken() {
return token;
}
}
// User Service
@ApplicationScoped
class UserService {
public User validateToken(String token) {
// Implement token validation logic
if ("valid-api-key".equals(token)) {
return new User("1", "api-user", Set.of("user", "api-client"));
}
return null;
}
}
class User {
private final String id;
private final String username;
private final Set<String> roles;
public User(String id, String username, Set<String> roles) {
this.id = id;
this.username = username;
this.roles = roles;
}
public String getId() { return id; }
public String getUsername() { return username; }
public Set<String> getRoles() { return roles; }
}
Database-Backed Security
JPA Entity for Users
package com.quarkus.security.entity;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "users")
@NamedQuery(
name = "User.findByUsername",
query = "SELECT u FROM User u WHERE u.username = :username"
)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles;
@Column(nullable = false)
private boolean active = true;
// Constructors
public User() {}
public User(String username, String password, Set<String> roles) {
this.username = username;
this.password = password;
this.roles = roles;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public Set<String> getRoles() { return roles; }
public void setRoles(Set<String> roles) { this.roles = roles; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
JPA Identity Provider
package com.quarkus.security.jpa;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.util.Optional;
@ApplicationScoped
public class UserRepository implements PanacheRepository<User> {
@Inject
EntityManager entityManager;
public Optional<User> findByUsername(String username) {
return find("username", username).firstResultOptional();
}
@Transactional
public User createUser(String username, String password, Set<String> roles) {
User user = new User();
user.setUsername(username);
user.setPassword(BcryptUtil.bcryptHash(password));
user.setRoles(roles);
user.setActive(true);
persist(user);
return user;
}
public SecurityIdentity validateUser(String username, String password) {
Optional<User> userOpt = findByUsername(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user.isActive() && BcryptUtil.matches(password, user.getPassword())) {
return QuarkusSecurityIdentity.builder()
.setPrincipal(() -> user.getUsername())
.addRoles(user.getRoles())
.addAttribute("user-id", user.getId())
.build();
}
}
return null;
}
}
Reactive Security Implementation
Reactive JWT Authentication
package com.quarkus.security.reactive;
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.jwt.JsonWebToken;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/reactive")
public class ReactiveSecurityResource {
@Inject
SecurityIdentity securityIdentity;
@Inject
JsonWebToken jwt;
@GET
@Path("/user-info")
@Produces(MediaType.APPLICATION_JSON)
public Uni<UserInfo> getUserInfo() {
return Uni.createFrom().item(() -> {
String username = securityIdentity.getPrincipal().getName();
Set<String> roles = securityIdentity.getRoles();
return new UserInfo(username, roles, jwt.getExpirationTime());
});
}
@GET
@Path("/admin-task")
public Uni<String> adminTask() {
return Uni.createFrom().item(() -> {
if (!securityIdentity.hasRole("admin")) {
throw new javax.ws.rs.ForbiddenException("Admin role required");
}
return "Admin task executed successfully";
});
}
public static class UserInfo {
public String username;
public Set<String> roles;
public long tokenExpiry;
public UserInfo(String username, Set<String> roles, long tokenExpiry) {
this.username = username;
this.roles = roles;
this.tokenExpiry = tokenExpiry;
}
}
}
Reactive Route Security
package com.quarkus.security.reactive;
import io.quarkus.vertx.web.Route;
import io.quarkus.vertx.web.RouteBase;
import io.quarkus.vertx.web.RouteFilter;
import io.vertx.core.http.HttpMethod;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
@RouteBase(path = "/api/v1")
public class ReactiveRoutes {
@Route(path = "/public", methods = HttpMethod.GET)
public void publicRoute(RoutingContext rc) {
rc.response()
.putHeader("Content-Type", "application/json")
.end("{\"message\": \"Public route\"}");
}
@Route(path = "/secure", methods = HttpMethod.GET)
public void secureRoute(RoutingContext rc) {
// This will be secured by Quarkus security
rc.response()
.putHeader("Content-Type", "application/json")
.end("{\"message\": \"Secure route\"}");
}
@RouteFilter
public void addSecurityHeaders(RoutingContext rc) {
rc.response()
.putHeader("X-Security-Policy", "strict-origin-when-cross-origin")
.putHeader("X-Content-Type-Options", "nosniff");
rc.next();
}
}
OAuth2/OpenID Connect Integration
OIDC Configuration
# OIDC Configuration quarkus.oidc.auth-server-url=https://keycloak.example.com/auth/realms/quarkus quarkus.oidc.client-id=quarkus-app quarkus.oidc.credentials.secret=your-secret quarkus.oidc.application-type=service quarkus.oidc.authentication.redirect-path=/oidc-success quarkus.oidc.authentication.restore-path=/oidc-restore quarkus.oidc.authentication.scopes=openid,profile,email # Role Mapping quarkus.oidc.roles.role-claim-path=realm_access.roles quarkus.http.auth.permission.oidc.paths=/oidc/* quarkus.http.auth.permission.oidc.policy=authenticated
OIDC Protected Resource
package com.quarkus.security.oidc;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Set;
@Path("/oidc")
@RequestScoped
@Produces(MediaType.APPLICATION_JSON)
public class OIDCResource {
@Inject
@Claim(standard = Claims.email)
String email;
@Inject
@Claim(standard = Claims.preferred_username)
String username;
@Inject
@Claim(standard = Claims.groups)
Set<String> groups;
@GET
@Path("/profile")
@RolesAllowed("user")
public Response getOIDCProfile() {
OIDCProfile profile = new OIDCProfile(
username,
email,
groups
);
return Response.ok(profile).build();
}
@GET
@Path("/admin")
@RolesAllowed("admin")
public Response adminEndpoint() {
return Response.ok(new Message("OIDC Admin access granted for: " + email)).build();
}
public static class OIDCProfile {
public String username;
public String email;
public Set<String> roles;
public OIDCProfile(String username, String email, Set<String> roles) {
this.username = username;
this.email = email;
this.roles = roles;
}
}
public static class Message {
public String message;
public Message(String message) {
this.message = message;
}
}
}
Security Event Handling
Security Event Listeners
package com.quarkus.security.events;
import io.quarkus.security.spi.runtime.SecurityEvent;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import java.time.LocalDateTime;
@ApplicationScoped
public class SecurityEventListener {
public void onAuthenticationSuccess(@Observes SecurityEvent.AuthenticationSuccess event) {
System.out.printf("[SECURITY] User %s authenticated successfully at %s%n",
event.getSecurityIdentity().getPrincipal().getName(),
LocalDateTime.now());
// Log to audit system, send notifications, etc.
}
public void onAuthenticationFailure(@Observes SecurityEvent.AuthenticationFailed event) {
System.out.printf("[SECURITY] Authentication failed for user %s at %s. Reason: %s%n",
event.getAuthenticationRequest().getClass().getSimpleName(),
LocalDateTime.now(),
event.getThrowable().getMessage());
}
public void onAuthorizationSuccess(@Observes SecurityEvent.AuthorizationSuccess event) {
System.out.printf("[SECURITY] User %s authorized for resource at %s%n",
event.getSecurityIdentity().getPrincipal().getName(),
LocalDateTime.now());
}
public void onAuthorizationFailure(@Observes SecurityEvent.AuthorizationFailure event) {
System.out.printf("[SECURITY] Authorization failed for user %s at %s%n",
event.getSecurityIdentity().getPrincipal().getName(),
LocalDateTime.now());
}
}
Custom Security Constraints
Method-Level Security
package com.quarkus.security.constraints;
import javax.annotation.security.DenyAll;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/constraints")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class SecurityConstraintsResource {
@GET
@Path("/public")
@PermitAll
public Response publicMethod() {
return Response.ok("Public access allowed").build();
}
@GET
@Path("/user-only")
@RolesAllowed("user")
public Response userOnly() {
return Response.ok("User access allowed").build();
}
@GET
@Path("/admin-only")
@RolesAllowed("admin")
public Response adminOnly() {
return Response.ok("Admin access allowed").build();
}
@GET
@Path("/denied")
@DenyAll
public Response denied() {
return Response.ok("This should never be reached").build();
}
@GET
@Path("/multi-roles")
@RolesAllowed({"admin", "supervisor"})
public Response multiRoles() {
return Response.ok("Admin or supervisor access allowed").build();
}
}
Testing Security
Security Test Configuration
package com.quarkus.security.test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.jwt.Claim;
import io.quarkus.test.security.jwt.JwtSecurity;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
import static org.hamcrest.Matchers.*;
@QuarkusTest
public class SecurityTest {
@Test
@TestSecurity(user = "testuser", roles = "user")
public void testUserAccess() {
RestAssured.given()
.when().get("/secured/user")
.then()
.statusCode(200)
.body("message", containsString("testuser"));
}
@Test
@TestSecurity(user = "testadmin", roles = "admin")
public void testAdminAccess() {
RestAssured.given()
.when().get("/secured/admin")
.then()
.statusCode(200)
.body("message", containsString("testadmin"));
}
@Test
@TestSecurity(user = "testuser", roles = "user")
public void testUserAccessDeniedToAdminEndpoint() {
RestAssured.given()
.when().get("/secured/admin")
.then()
.statusCode(403);
}
@Test
@JwtSecurity(claims = {
@Claim(key = "upn", value = "jwtuser"),
@Claim(key = "groups", value = "user")
})
public void testJwtSecurity() {
RestAssured.given()
.when().get("/secured/user")
.then()
.statusCode(200)
.body("message", containsString("jwtuser"));
}
@Test
public void testUnauthenticatedAccess() {
RestAssured.given()
.when().get("/secured/user")
.then()
.statusCode(401);
}
}
Advanced Security Configuration
Custom Security Policy
package com.quarkus.security.policy;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
import javax.enterprise.context.ApplicationScoped;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletableFuture;
@ApplicationScoped
public class IPWhitelistPolicy implements HttpSecurityPolicy {
private final Set<String> allowedIPs = Set.of(
"192.168.1.0/24",
"10.0.0.1",
"127.0.0.1"
);
@Override
public CompletionStage<CheckResult> checkPermission(
io.vertx.ext.web.RoutingContext routingContext,
SecurityIdentity identity) {
String clientIP = routingContext.request().remoteAddress().host();
if (isIPAllowed(clientIP)) {
return CompletableFuture.completedFuture(CheckResult.PERMIT);
} else {
return CompletableFuture.completedFuture(CheckResult.DENY);
}
}
private boolean isIPAllowed(String ip) {
// Implement IP whitelist logic
return allowedIPs.stream().anyMatch(allowed -> matchesIP(ip, allowed));
}
private boolean matchesIP(String ip, String allowed) {
// Simple IP matching logic
return ip.equals(allowed) || allowed.endsWith("/24") &&
ip.startsWith(allowed.substring(0, allowed.length() - 4));
}
}
Rate Limiting Security
package com.quarkus.security.ratelimit;
import io.quarkus.redis.datasource.RedisDataSource;
import io.quarkus.redis.datasource.value.ValueCommands;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
@Provider
@ApplicationScoped
public class RateLimitFilter implements ContainerRequestFilter {
private final ValueCommands<String, Integer> redisCommands;
private static final int MAX_REQUESTS = 100;
private static final int TIME_WINDOW = 3600; // 1 hour
public RateLimitFilter(RedisDataSource redis) {
this.redisCommands = redis.value(Integer.class);
}
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String clientIP = requestContext.getHeaderString("X-Forwarded-For");
if (clientIP == null) {
clientIP = "unknown";
}
String key = "rate_limit:" + clientIP;
Integer currentCount = redisCommands.get(key);
if (currentCount == null) {
redisCommands.setex(key, TIME_WINDOW, 1);
} else if (currentCount >= MAX_REQUESTS) {
requestContext.abortWith(
Response.status(429)
.entity("Rate limit exceeded")
.build()
);
} else {
redisCommands.incr(key);
}
}
}
Production Security Configuration
Production Security Properties
# Production Security Settings quarkus.security.users.embedded.enabled=false # HTTPS Configuration quarkus.http.ssl-port=8443 quarkus.http.ssl.certificate.files=server.crt quarkus.http.ssl.certificate.key-files=server.key # Security Headers quarkus.http.cors=true quarkus.http.cors.origins=https://yourdomain.com quarkus.http.header.content-security-policy=default-src 'self' quarkus.http.header.strict-transport-security=max-age=31536000 # JWT Production Settings mp.jwt.verify.publickey.location=https://your-issuer.com/.well-known/jwks.json mp.jwt.verify.issuer=https://your-issuer.com # OIDC Production Settings quarkus.oidc.tls.verification=required quarkus.oidc.token.issuer=https://your-oidc-issuer.com # Logging Security Events quarkus.log.category."io.quarkus.security".level=INFO
Security Monitoring and Health Checks
Security Health Check
package com.quarkus.security.health;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;
import javax.enterprise.context.ApplicationScoped;
@Readiness
@ApplicationScoped
public class SecurityHealthCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
// Check security components status
boolean oidcAvailable = checkOIDCConnection();
boolean databaseAvailable = checkDatabaseConnection();
if (oidcAvailable && databaseAvailable) {
return HealthCheckResponse.up("security-components");
} else {
return HealthCheckResponse.down("security-components");
}
}
private boolean checkOIDCConnection() {
// Implement OIDC provider connectivity check
return true;
}
private boolean checkDatabaseConnection() {
// Implement database connectivity check
return true;
}
}
Conclusion
This comprehensive Quarkus security implementation provides:
- JWT-based authentication with MicroProfile JWT
- OAuth2/OpenID Connect integration
- Database-backed security with JPA
- Reactive security for non-blocking operations
- Custom security providers and policies
- Comprehensive testing strategies
- Production-ready configurations
Quarkus security offers a robust, standards-compliant security framework that integrates seamlessly with modern cloud-native applications. The reactive nature of Quarkus ensures that security operations don't block the event loop, maintaining high performance while providing enterprise-grade security features.