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:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-security</artifactId>
</dependency>
build.gradle
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 SecurityIdentityAugmentor is registered then they will be considered equal candidates and invoked in random order. You can enforce the order by implementing a default SecurityIdentityAugmentor#priority method. Augmentors with higher priorities will be invoked first.

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:

Example Security Providers Configuration
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:

Example Security Providers BouncyCastle Configuration
quarkus.security.security-providers=BC

and add the BouncyCastle provider dependency:

pom.xml
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
</dependency>
build.gradle
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:

Example Security Providers BouncyCastle JSSE Configuration
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:

pom.xml
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bctls-jdk18on</artifactId>
</dependency>
build.gradle
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:

Example Security Providers BouncyCastle FIPS Configuration
quarkus.security.security-providers=BCFIPS

and add the BouncyCastle FIPS provider dependency:

pom.xml
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bc-fips</artifactId>
</dependency>
build.gradle
implementation("org.bouncycastle:bc-fips")

BCFIPS provider option is supported in native image but the algorithm self-tests which rely on java.security.SecureRandom to verify the generated keys have been removed for these tests to pass. The following classes have been affected: - org.bouncycastle.crypto.general.DSA - org.bouncycastle.crypto.general.DSTU4145 - org.bouncycastle.crypto.general.ECGOST3410 - org.bouncycastle.crypto.general.GOST3410 - org.bouncycastle.crypto.fips.FipsDSA - org.bouncycastle.crypto.fips.FipsEC - org.bouncycastle.crypto.fips.FipsRSA

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:

Example Security Providers BouncyCastle FIPS JSSE Configuration
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:

pom.xml
<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bctls-fips</artifactId>
</dependency>

<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bc-fips</artifactId>
</dependency>
build.gradle
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

BCFIPSJSSE provider option is currently not supported in native image.

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 SunPKCS11 bridge provider is supported in native image, configuring SunPKCS11 is currently not supported in native image at the Quarkus level.

Reactive Security

If you are going to use security in a reactive environment, you will likely need SmallRye Context Propagation:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
build.gradle
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.

List of supported security events
  • 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

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.

Related content