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();
}
}
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;
}
}
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.