Security Tips and Tricks
Quarkus Security Dependency
io.quarkus:quarkus-security
module contains the core Quarkus Security classes.
In most cases, it does not have to be added directly to your project’s build file as it is already provided by all the security extensions. However, if you need to write your own custom security code (for example, register a Custom Jakarta REST SecurityContext) or use BouncyCastle libraries, then please make sure it is included:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
implementation("io.quarkus:quarkus-security")
HttpAuthenticationMechanism Customization
One can customize HttpAuthenticationMechanism
by registering a CDI implementation bean.
In the example below the custom authenticator delegates to JWTAuthMechanism
provided by quarkus-smallrye-jwt
:
@Alternative
@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);
@Inject
JWTAuthMechanism delegate;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
// do some custom action and delegate
return delegate.authenticate(context, identityProviderManager);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return delegate.getChallenge(context);
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return delegate.getCredentialTypes();
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport() {
return delegate.getCredentialTransport();
}
}
The HttpAuthenticationMechanism should transform incoming HTTP request with suitable authentication credentials
into an io.quarkus.security.identity.request.AuthenticationRequest instance and delegate the authentication to the io.quarkus.security.identity.IdentityProviderManager .
Leaving authentication to the io.quarkus.security.identity.IdentityProvider s gives you more options for credentials verifications,
as well as convenient way to perform blocking tasks.
Nevertheless, the io.quarkus.security.identity.IdentityProvider can be omitted and the HttpAuthenticationMechanism is free to authenticate request on its own in trivial use cases.
|
Dealing with more than one HttpAuthenticationMechanism
More than one HttpAuthenticationMechanism
can be combined, for example, the built-in Basic
or JWT
mechanism provided by quarkus-smallrye-jwt
has to be used to verify the service clients credentials passed as the HTTP Authorization
Basic
or Bearer
scheme values while the Authorization Code
mechanism provided by quarkus-oidc
has to be used to authenticate the users with Keycloak or other OpenID Connect providers.
In such cases the mechanisms are asked to verify the credentials in turn until a SecurityIdentity
is created. The mechanisms are sorted in the descending order using their priority. Basic
authentication mechanism has the highest priority of 2000
, followed by the Authorization Code
one with the priority of 1001
, with all other mechanisms provided by Quarkus having the priority of 1000
.
If no credentials are provided then the mechanism specific challenge is created, for example, 401
status is returned by either Basic
or JWT
mechanisms, URL redirecting the user to the OpenID Connect provider is returned by quarkus-oidc
, etc.
So if Basic
and Authorization Code
mechanisms are combined then 401
will be returned if no credentials are provided and if JWT
and Authorization Code
mechanisms are combined then a redirect URL will be returned.
In some cases such a default logic of selecting the challenge is exactly what is required by a given application, but sometimes it may not meet the requirements. In such cases (or indeed in other similar cases where you’d like to change the order in which the mechanisms are asked to handle the current authentication or challenge request), you can create a custom mechanism and choose which mechanism should create a challenge, for example:
@Alternative (1)
@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);
@Inject
JWTAuthMechanism jwt;
@Inject
OidcAuthenticationMechanism oidc;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return selectBetweenJwtAndOidc(context).authenticate(context, identityProviderManager);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return selectBetweenJwtAndOidcChallenge(context).getChallenge(context);
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
Set<Class<? extends AuthenticationRequest>> credentialTypes = new HashSet<>();
credentialTypes.addAll(jwt.getCredentialTypes());
credentialTypes.addAll(oidc.getCredentialTypes());
return credentialTypes;
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return selectBetweenJwtAndOidc(context).getCredentialTransport(context);
}
private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) {
....
}
private HttpAuthenticationMechanism selectBetweenJwtAndOidcChallenge(RoutingContext context) {
// for example, if no `Authorization` header is available and no `code` parameter is provided - use `jwt` to create a challenge
}
}
1 | Declaring the mechanism an alternative bean ensures this mechanism is used rather than OidcAuthenticationMechanism and JWTAuthMechanism . |
Security Identity Customization
Internally, the identity providers create and update an instance of the io.quarkus.security.identity.SecurityIdentity
class which holds the principal, roles, credentials which were used to authenticate the client (user) and other security attributes. An easy option to customize SecurityIdentity
is to register a custom SecurityIdentityAugmentor
. For example, the augmentor below adds an addition role:
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return Uni.createFrom().item(build(identity));
// Do 'return context.runBlocking(build(identity));'
// if a blocking call is required to customize the identity
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
if(identity.isAnonymous()) {
return () -> identity;
} else {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
// add custom role source here
builder.addRole("dummy");
return builder::build;
}
}
}
Here is another example showing how to use the client certificate available in the current mutual TLS (mTLS) authentication request to add more roles:
import java.security.cert.X509Certificate;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
import java.util.Set;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return Uni.createFrom().item(build(identity));
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
CertificateCredential certificate = identity.getCredential(CertificateCredential.class);
if (certificate != null) {
builder.addRoles(extractRoles(certificate.getCertificate()));
}
return builder::build;
}
private Set<String> extractRoles(X509Certificate certificate) {
String name = certificate.getSubjectX500Principal().getName();
switch (name) {
case "CN=client":
return Collections.singleton("user");
case "CN=guest-client":
return Collections.singleton("guest");
default:
return Collections.emptySet();
}
}
}
If more than one custom |
By default, the request context is not activated when augmenting the security identity, this means that if you want to use for example Hibernate
that mandates a request context, you will have a jakarta.enterprise.context.ContextNotActiveException
.
The solution is to activate the request context, the following example shows how to get the roles from an Hibernate with Panache UserRoleEntity
.
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Inject
Instance<SecurityIdentitySupplier> identitySupplierInstance;
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
if(identity.isAnonymous()) {
return Uni.createFrom().item(identity);
}
// Hibernate ORM is blocking
SecurityIdentitySupplier identitySupplier = identitySupplierInstance.get();
identitySupplier.setIdentity(identity);
return context.runBlocking(identitySupplier);
}
}
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.control.ActivateRequestContext;
import java.util.function.Supplier;
@Dependent
class SecurityIdentitySupplier implements Supplier<SecurityIdentity> {
private SecurityIdentity identity;
@Override
@ActivateRequestContext
public SecurityIdentity get() {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
String user = identity.getPrincipal().getName();
UserRoleEntity.<userRoleEntity>streamAll()
.filter(role -> user.equals(role.user))
.forEach(role -> builder.addRole(role.role));
return builder.build();
}
public void setIdentity(SecurityIdentity identity) {
this.identity = identity;
}
}
The CDI request context activation shown in the example above does not help you to access the RoutingContext
when the proactive authentication is enabled.
The following example illustrates how you can access the RoutingContext
from the SecurityIdentityAugmentor
:
package org.acme.security;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context,
Map<String, Object> attributes) {
RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
if (routingContext != null) {
// Augment SecurityIdentity using RoutingContext
} else {
return augment(identity, context); (1)
}
}
...
}
1 | The RoutingContext is not be available when the SecurityIdentity is augmented after HTTP request has completed. |
If you implemented a custom HttpAuthenticationMechanism , then you need to add the RoutingContext to the authentication
request attributes with the io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.setRoutingContextAttribute method call.
Otherwise, the RoutingContext will not be available during augmentation.
|
Custom Jakarta REST SecurityContext
If you use Jakarta REST ContainerRequestFilter
to set a custom Jakarta REST SecurityContext
then make sure ContainerRequestFilter
runs in the Jakarta REST pre-match phase by adding a @PreMatching
annotation to it for this custom security context to be linked with Quarkus SecurityIdentity
, for example:
import java.security.Principal;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;
@Provider
@PreMatching
public class SecurityOverrideFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String user = requestContext.getHeaders().getFirst("User");
String role = requestContext.getHeaders().getFirst("Role");
if (user != null && role != null) {
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return new Principal() {
@Override
public String getName() {
return user;
}
};
}
@Override
public boolean isUserInRole(String r) {
return role.equals(r);
}
@Override
public boolean isSecure() {
return false;
}
@Override
public String getAuthenticationScheme() {
return "basic";
}
});
}
}
}
Disabling Authorization
If you have a good reason to disable the authorization then you can register a custom AuthorizationController
:
@Alternative
@Priority(Interceptor.Priority.LIBRARY_AFTER)
@ApplicationScoped
public class DisabledAuthController extends AuthorizationController {
@ConfigProperty(name = "disable.authorization", defaultValue = "false")
boolean disableAuthorization;
@Override
public boolean isAuthorizationEnabled() {
return !disableAuthorization;
}
}
For manual testing Quarkus provides a convenient config property to disable authorization in dev mode. This property has the exact same effect as the custom AuthorizationController
shown above, but is only available in dev mode:
quarkus.security.auth.enabled-in-dev-mode=false
Please also see TestingSecurity Annotation section on how to disable the security checks using TestSecurity
annotation.
Registering Security Providers
Default providers
When running in native mode, the default behavior for GraalVM native executable generation is to only include the main "SUN" provider
unless you have enabled SSL, in which case all security providers are registered. If you are not using SSL, then you can selectively
register security providers by name using the quarkus.security.security-providers
property. The following example illustrates
configuration to register the "SunRsaSign" and "SunJCE" security providers:
quarkus.security.security-providers=SunRsaSign,SunJCE
BouncyCastle
If you need to register an org.bouncycastle.jce.provider.BouncyCastleProvider
JCE provider then please set a BC
provider name:
quarkus.security.security-providers=BC
and add the BouncyCastle provider dependency:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
implementation("org.bouncycastle:bcprov-jdk18on")
BouncyCastle JSSE
If you need to register an org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
JSSE provider and use it instead of the default SunJSSE provider then please set a BCJSSE
provider name:
quarkus.security.security-providers=BCJSSE
quarkus.http.ssl.client-auth=REQUIRED
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
and add the BouncyCastle TLS dependency:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
</dependency>
implementation("org.bouncycastle:bctls-jdk18on")
BouncyCastle FIPS
If you need to register an org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
JCE provider then please set a BCFIPS
provider name:
quarkus.security.security-providers=BCFIPS
and add the BouncyCastle FIPS provider dependency:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
</dependency>
implementation("org.bouncycastle:bc-fips")
|
BouncyCastle JSSE FIPS
If you need to register an org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
JSSE provider and use it in combination with org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
instead of the default SunJSSE provider then please set a BCFIPSJSSE
provider name:
quarkus.security.security-providers=BCFIPSJSSE
quarkus.http.ssl.client-auth=REQUIRED
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
quarkus.http.ssl.certificate.key-store-file-type=BCFKS
quarkus.http.ssl.certificate.key-store-provider=BCFIPS
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
quarkus.http.ssl.certificate.trust-store-file-type=BCFKS
quarkus.http.ssl.certificate.trust-store-provider=BCFIPS
and the BouncyCastle TLS dependency optimized for using the BouncyCastle FIPS provider:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-fips</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
</dependency>
implementation("org.bouncycastle:bctls-fips")
implementation("org.bouncycastle:bc-fips")
Note that the keystore and truststore type and provider are set to BCFKS
and BCFIPS
.
One can generate a keystore with this type and provider like this:
keytool -genkey -alias server -keyalg RSA -keystore server-keystore.jks -keysize 2048 -keypass password -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider -providerpath $PATH_TO_BC_FIPS_JAR -storetype BCFKS
|
SunPKCS11
SunPKCS11
provider provides a bridge to specific PKCS#11
implementations such as cryptographic smartcards and other Hardware Security Modules, Network Security Services in FIPS mode, etc.
Typically, in order to work with SunPKCS11
, one needs to install a PKCS#11
implementation, generate a configuration which usually refers to a shared library, token slot, etc and write the following Java code:
import java.security.Provider;
import java.security.Security;
String configuration = "pkcs11.cfg"
Provider sunPkcs11 = Security.getProvider("SunPKCS11");
Provider pkcsImplementation = sunPkcs11.configure(configuration);
// or prepare configuration in the code or read it from the file such as "pkcs11.cfg" and do
// sunPkcs11.configure("--" + configuration);
Security.addProvider(pkcsImplementation);
In Quarkus you can achieve the same at the configuration level only without having to modify the code, for example:
quarkus.security.security-providers=SunPKCS11
quarkus.security.security-provider-config.SunPKCS11=pkcs11.cfg
Note that while accessing the |
Reactive Security
If you are going to use security in a reactive environment, you will likely need SmallRye Context Propagation:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-context-propagation")
This will allow you to propagate the identity throughout the reactive callbacks. You also need to make sure you
are using an executor that is capable of propagating the identity (e.g. no CompletableFuture.supplyAsync
),
to make sure that Quarkus can propagate it. For more information see the
Context Propagation Guide.
Observe security events
Quarkus beans can use CDI observers to consume authentication and authorization security events. The observers can be either synchronous or asynchronous.
-
io.quarkus.security.spi.runtime.AuthenticationFailureEvent
-
io.quarkus.security.spi.runtime.AuthenticationSuccessEvent
-
io.quarkus.security.spi.runtime.AuthorizationFailureEvent
-
io.quarkus.security.spi.runtime.AuthorizationSuccessEvent
-
io.quarkus.oidc.SecurityEvent
-
io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent
For more information about security events specific to the Quarkus OpenID Connect extension, please see the Listening to important authentication events section of the OIDC code flow mechanism for protecting web applications guide. |
package org.acme.security;
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEvent;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;
import org.jboss.logging.Logger;
public class SecurityEventObserver {
private static final Logger LOG = Logger.getLogger(SecurityEventObserver.class.getName());
void observeAuthenticationSuccess(@ObservesAsync AuthenticationSuccessEvent event) { (1)
LOG.debugf("User '%s' has authenticated successfully", event.getSecurityIdentity().getPrincipal().getName());
}
void observeAuthenticationFailure(@ObservesAsync AuthenticationFailureEvent event) {
RoutingContext routingContext = (RoutingContext) event.getEventProperties().get(RoutingContext.class.getName());
LOG.debugf("Authentication failed, request path: '%s'", routingContext.request().path());
}
void observeAuthorizationSuccess(@ObservesAsync AuthorizationSuccessEvent event) {
String principalName = getPrincipalName(event);
if (principalName != null) {
LOG.debugf("User '%s' has been authorized successfully", principalName);
}
}
void observeAuthorizationFailure(@Observes AuthorizationFailureEvent event) {
LOG.debugf(event.getAuthorizationFailure(), "User '%s' authorization failed", event.getSecurityIdentity().getPrincipal().getName());
}
private static String getPrincipalName(SecurityEvent event) { (2)
if (event.getSecurityIdentity() != null) {
return event.getSecurityIdentity().getPrincipal().getName();
}
return null;
}
}
1 | This observer consumes all the AuthenticationSuccessEvent events asynchronously, which means that HTTP request processing will continue regardless on the event processing.
Depending on the application, that can be a lot of the AuthenticationSuccessEvent events.
For that reason, asynchronous processing can have positive effect on performance. |
2 | Common code for all supported security event types is possible because they all implement the io.quarkus.security.spi.runtime.SecurityEvent interface. |