Skip to content

Commit

Permalink
feat(security): Improve inclusive auth with docs and modes
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Feb 11, 2025
1 parent cd02eae commit b16f0d5
Show file tree
Hide file tree
Showing 21 changed files with 463 additions and 60 deletions.
49 changes: 49 additions & 0 deletions docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ public class InclusiveAuthExampleBean {
You cannot combine the Quarkus `quarkus-oidc` Bearer token and `smallrye-jwt` authentication mechanisms because both mechanisms attempt to verify the token extracted from the HTTP Bearer token authentication scheme.
====

[[http-security-policy-path-based-auth]]
=== Use HTTP Security Policy to enable path-based authentication

The following configuration example demonstrates how you can enforce a single selectable authentication mechanism for a given request path:
Expand Down Expand Up @@ -779,6 +780,54 @@ quarkus.http.auth.permission.roles1.methods=GET <2>
<2> Make the `roles1` permission match only the endpoint annotated with the `@AuthorizationCodeFlow` annotation.
Unannotated endpoints must avoid the delay caused by the `applies-to=JAXRS` option.

=== Use inclusive authentication to enable path-based authentication

By default, Quarkus only supports <<http-security-policy-path-based-auth,path-based authentication>> for one authentication mechanism per path.
If more than one authentication mechanism must be used for the path-based authentication, you can write a custom `HttpAuthenticationMechanism` as documented in the xref:security-customization.adoc#dealing-with-more-than-one-http-auth-mechanisms[Dealing with more than one HttpAuthenticationMechanism] section of the Security Tips and Tricks guide.
Another option is to enable inclusive authentication in the lax mode and write a custom `HttpSecurityPolicy` or `PermissionChecker` that verifies that all registered HTTP authentication mechanisms created their mechanism-specific `SecurityIdentity`.

.Enable inclusive authentication in the lax mode
[source,properties]
----
quarkus.http.auth.inclusive-mode=lax <1>
quarkus.http.auth.inclusive=true
----
<1> By default, inclusive authentication requires that all registered HTTP authentication mechanisms must create the `SecurityIdentity`.
However, in the lax mode, the authentication succeeds if at least one registered `HttpAuthenticationMechanism` created the `SecurityIdentity`.

.Example of the HTTP Authentication mechanisms check
[source,java]
----
import io.quarkus.security.PermissionChecker;
import io.quarkus.security.PermissionsAllowed;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import java.util.Map;
@Path("/hello")
public class HelloResource {
@PermissionsAllowed("mtls-basic")
@GET
public String hello() {
return "Hello world";
}
@PermissionChecker("mtls-basic")
boolean isMtlsAndBasicAuthentication(SecurityIdentity identity) {
Map<String, SecurityIdentity> identities = HttpSecurityUtils.getSecurityIdentities(identity);
if (identities != null) {
return identities.containsKey("basic") && identities.containsKey("x509"); <1>
}
return false;
}
}
----
<1> Permit access to HTTP requests authenticated with both mTLS and basic authentication mechanisms.

[[proactive-auth]]
== Proactive authentication

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@
import io.quarkus.tls.TlsRegistryBuildItem;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem;
import io.quarkus.vertx.http.deployment.PreRouterFinalizationBuildItem;
import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem;
import io.quarkus.vertx.http.runtime.VertxHttpBuildTimeConfig;
import io.smallrye.jwt.auth.cdi.ClaimValueProducer;
Expand Down Expand Up @@ -316,9 +316,7 @@ void detectIfUserInfoRequired(OidcRecorder recorder, BeanRegistrationPhaseBuildI
recorder.setUserInfoInjectionPointDetected(detectUserInfoRequired(beanRegistration));
}

// this ensures we initialize OIDC before HTTP router is finalized
// because we need TenantConfigBean in the BackChannelLogoutHandler
@Produce(FilterBuildItem.class)
@Produce(PreRouterFinalizationBuildItem.class)
@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@Consume(BeanContainerBuildItem.class)
@Consume(SyntheticBeansRuntimeInitBuildItem.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.deployment.Capabilities;
Expand All @@ -50,9 +51,11 @@
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationIndexBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.builditem.SystemPropertyBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.DescriptorUtils;
Expand Down Expand Up @@ -276,13 +279,16 @@ void createHttpAuthenticationHandler(HttpSecurityRecorder recorder, Capabilities
}
}

@Consume(RuntimeConfigSetupCompleteBuildItem.class)
@Consume(SyntheticBeansRuntimeInitBuildItem.class) // identity providers in HTTP authenticator may depend on synthetic beans
@Produce(PreRouterFinalizationBuildItem.class)
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
@Consume(BeanContainerBuildItem.class)
void initializeAuthenticationHandler(Optional<HttpAuthenticationHandlerBuildItem> authenticationHandler,
HttpSecurityRecorder recorder, VertxHttpConfig httpConfig) {
HttpSecurityRecorder recorder, VertxHttpConfig httpConfig, BeanContainerBuildItem beanContainerBuildItem) {
if (authenticationHandler.isPresent()) {
recorder.initializeHttpAuthenticatorHandler(authenticationHandler.get().handler, httpConfig);
recorder.initializeHttpAuthenticatorHandler(authenticationHandler.get().handler, httpConfig,
beanContainerBuildItem.getValue());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Produce;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.vertx.http.deployment.HttpSecurityProcessor.IsApplicationBasicAuthRequired;
Expand Down Expand Up @@ -81,13 +81,14 @@ void createManagementAuthMechHandler(
}
}

@Produce(PreRouterFinalizationBuildItem.class)
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
@Consume(BeanContainerBuildItem.class)
void initializeAuthMechanismHandler(Optional<ManagementAuthenticationHandlerBuildItem> managementAuthenticationHandler,
ManagementSecurityRecorder recorder, ManagementConfig managementConfig) {
ManagementSecurityRecorder recorder, ManagementConfig managementConfig, BeanContainerBuildItem containerBuildItem) {
if (managementAuthenticationHandler.isPresent()) {
recorder.initializeAuthenticationHandler(managementAuthenticationHandler.get().handler, managementConfig);
recorder.initializeAuthenticationHandler(managementAuthenticationHandler.get().handler, managementConfig,
containerBuildItem.getValue());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.quarkus.vertx.http.deployment;

import io.quarkus.builder.item.EmptyBuildItem;

/**
* Marker used by Build Steps that consume runtime configuration to ensure that they run before the HTTP router
* has been finalized.
*/
public final class PreRouterFinalizationBuildItem extends EmptyBuildItem {
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.ExecutionTime;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.ApplicationStartBuildItem;
Expand Down Expand Up @@ -339,6 +340,7 @@ void createDevUILog(BuildProducer<FooterLogBuildItem> footerLogProducer,
vertxDevUILogBuildItem.produce(new VertxDevUILogBuildItem(publisher));
}

@Consume(PreRouterFinalizationBuildItem.class)
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
ServiceStartBuildItem finalizeRouter(Optional<LoggingDecorateBuildItem> decorateBuildItem,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,29 +31,4 @@ public interface AuthConfig {
*/
@WithDefault("true")
boolean proactive();

/**
* Require that all registered HTTP authentication mechanisms must complete the authentication.
* <p>
* Typically, this property has to be true when the credentials are carried over mTLS, when both mTLS and another
* authentication, for example, OIDC bearer token authentication, must succeed.
* In such cases, `SecurityIdentity` created by the first mechanism, mTLS, can be injected, identities created
* by other mechanisms will be available on `SecurityIdentity`.
* The mTLS mechanism is always the first mechanism, because its priority is elevated when inclusive authentication
* is enabled.
* The identities can be retrieved using utility method as in the example below:
*
* <pre>
* {@code
* io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getSecurityIdentities(securityIdentity)
* }
* </pre>
* <p>
* This property is false by default which means that the authentication process is complete as soon as the first
* `SecurityIdentity` is created.
* <p>
* This property will be ignored if the path specific authentication is enabled.
*/
@WithDefault("false")
boolean inclusive();
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,53 @@ public interface AuthRuntimeConfig {
* Form Auth config
*/
FormAuthRuntimeConfig form();

/**
* Require that all registered HTTP authentication mechanisms must attempt to verify the request credentials.
* <p>
* By default, when the {@link #inclusiveMode} is strict, every registered authentication mechanism must produce
* SecurityIdentity, otherwise, a number of mechanisms which produce the identity may be less than a total
* number of registered mechanisms.
* <p>
* The identities can be retrieved using utility method as in the example below:
*
* <pre>
* {@code
* io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getSecurityIdentities(securityIdentity)
* }
* </pre>
*
* When mutual TLS (mTLS) is enabled, the `securityIdentity` instance in the example above is created by
* the mTLS HTTP authentication mechanism, because this mechanism runs first.
* <p>
* This property is false by default which means that the authentication process is complete as soon as the first
* `SecurityIdentity` is created.
* <p>
* This property will be ignored if the path specific authentication is enabled.
*/
@WithDefault("false")
boolean inclusive();

/**
* Inclusive authentication mode.
*/
@WithDefault("strict")
InclusiveMode inclusiveMode();

enum InclusiveMode {
/**
* Authentication succeeds if at least one of the registered HTTP authentication mechanisms creates the identity.
*/
LAX,
/**
* Authentication succeeds if all the registered HTTP authentication mechanisms create the identity.
* Typically, inclusive authentication should be in the strict mode when the credentials are carried over mTLS,
* when both mTLS and another authentication, for example, OIDC bearer token authentication, must succeed.
* In such cases, `SecurityIdentity` created by the first mechanism, mTLS, can be injected, identities created
* by other mechanisms will be available on `SecurityIdentity`.
* The mTLS mechanism is always the first mechanism, because its priority is elevated when inclusive authentication
* is enabled.
*/
STRICT
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import jakarta.enterprise.inject.spi.CDI;

import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.vertx.http.runtime.security.BasicAuthenticationMechanism;
Expand All @@ -26,8 +27,8 @@ public Handler<RoutingContext> getAuthenticationHandler(RuntimeValue<Authenticat
}

public void initializeAuthenticationHandler(RuntimeValue<AuthenticationHandler> handler,
ManagementConfig managementConfig) {
handler.getValue().init(ManagementPathMatchingHttpSecurityPolicy.class,
ManagementConfig managementConfig, BeanContainer beanContainer) {
handler.getValue().init(beanContainer.beanInstance(ManagementPathMatchingHttpSecurityPolicy.class),
RolesMapping.of(managementConfig.auth().rolesMapping()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,16 @@ abstract class AbstractHttpAuthorizer {

private static final Logger log = Logger.getLogger(AbstractHttpAuthorizer.class);

private final HttpAuthenticator httpAuthenticator;
private final IdentityProviderManager identityProviderManager;
private final AuthorizationController controller;
private final List<HttpSecurityPolicy> policies;
private final SecurityEventHelper<AuthorizationSuccessEvent, AuthorizationFailureEvent> securityEventHelper;
private final HttpSecurityPolicy.AuthorizationRequestContext context;

AbstractHttpAuthorizer(HttpAuthenticator httpAuthenticator, IdentityProviderManager identityProviderManager,
AbstractHttpAuthorizer(IdentityProviderManager identityProviderManager,
AuthorizationController controller, List<HttpSecurityPolicy> policies, BeanManager beanManager,
BlockingSecurityExecutor blockingExecutor, Event<AuthorizationFailureEvent> authZFailureEvent,
Event<AuthorizationSuccessEvent> authZSuccessEvent, boolean securityEventsEnabled) {
this.httpAuthenticator = httpAuthenticator;
this.identityProviderManager = identityProviderManager;
this.controller = controller;
this.policies = policies;
Expand Down Expand Up @@ -135,6 +133,7 @@ public void accept(Throwable throwable) {
private void doDeny(SecurityIdentity identity, RoutingContext routingContext, HttpSecurityPolicy policy) {
//if we were denied we send a challenge if we are not authenticated, otherwise we send a 403
if (identity.isAnonymous()) {
HttpAuthenticator httpAuthenticator = routingContext.get(HttpAuthenticator.class.getName());
httpAuthenticator.sendChallenge(routingContext).subscribe().withSubscriber(new UniSubscriber<Boolean>() {
@Override
public void onSubscribe(UniSubscription subscription) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.AuthRuntimeConfig;
import io.quarkus.vertx.http.runtime.VertxHttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.VertxHttpConfig;
import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication;
Expand Down Expand Up @@ -86,19 +87,20 @@ public final class HttpAuthenticator {
private final HttpAuthenticationMechanism[] mechanisms;
private final SecurityEventHelper<AuthenticationSuccessEvent, AuthenticationFailureEvent> securityEventHelper;
private final boolean inclusiveAuth;
private final boolean strictInclusiveMode;

HttpAuthenticator(IdentityProviderManager identityProviderManager,
Event<AuthenticationFailureEvent> authFailureEvent,
Event<AuthenticationSuccessEvent> authSuccessEvent,
BeanManager beanManager,
VertxHttpBuildTimeConfig httpBuildTimeConfig,
Instance<HttpAuthenticationMechanism> httpAuthenticationMechanism,
BeanManager beanManager, VertxHttpBuildTimeConfig httpBuildTimeConfig,
VertxHttpConfig httpConfig, Instance<HttpAuthenticationMechanism> httpAuthenticationMechanism,
Instance<IdentityProvider<?>> providers,
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) {
this.securityEventHelper = new SecurityEventHelper<>(authSuccessEvent, authFailureEvent, AUTHENTICATION_SUCCESS,
AUTHENTICATION_FAILURE, beanManager, securityEventsEnabled);
this.identityProviderManager = identityProviderManager;
this.inclusiveAuth = httpBuildTimeConfig.auth().inclusive();
this.inclusiveAuth = httpConfig.auth().inclusive();
this.strictInclusiveMode = httpConfig.auth().inclusiveMode() == AuthRuntimeConfig.InclusiveMode.STRICT;
List<HttpAuthenticationMechanism> mechanisms = new ArrayList<>();
for (HttpAuthenticationMechanism mechanism : httpAuthenticationMechanism) {
if (mechanism.getCredentialTypes().isEmpty()) {
Expand Down Expand Up @@ -215,6 +217,30 @@ public Uni<SecurityIdentity> apply(HttpAuthenticationMechanism mech) {
});
}

if (inclusiveAuth && strictInclusiveMode && pathSpecificMechanism == null) {
// inclusive authentication in the strict mode requires that all registered mechanisms created identity
// if at least one of them created it (AKA: if identity is not null, null results in anonymous identity)
// inclusive authentication is not applied when path-specific mechanism has been selected (because there
// user said use 'xyz' mechanism, not all the mechanisms)
result = result.onItem().ifNotNull()
.transformToUni(new Function<SecurityIdentity, Uni<? extends SecurityIdentity>>() {
@Override
public Uni<? extends SecurityIdentity> apply(SecurityIdentity identity) {
Map<String, SecurityIdentity> identities = HttpSecurityUtils.getSecurityIdentities(routingContext);
if (identities == null || identities.size() != mechanisms.length) {
return Uni.createFrom().failure(new AuthenticationFailedException(
"""
There is '%d' HTTP authentication mechanisms, however only '%d' authentication mechanisms
created identity: %s
"""
.formatted(identities == null ? 0 : identities.size(), mechanisms.length,
identities == null ? "" : identities.keySet())));
}
return Uni.createFrom().item(identity);
}
});
}

if (routingContext.get(ROLES_MAPPING_KEY) != null) {
result = result.onItem().ifNotNull().transform(routingContext.get(ROLES_MAPPING_KEY));
}
Expand Down
Loading

0 comments on commit b16f0d5

Please sign in to comment.