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 10, 2025
1 parent a207423 commit 9d6fd56
Show file tree
Hide file tree
Showing 21 changed files with 459 additions and 68 deletions.
48 changes: 48 additions & 0 deletions docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,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

Currently Quarkus only supports out of the box path-based authentication for one authentication mechanism per path.
If you need to combine multiple authentication mechanisms, you can write your own HTTP Authentication mechanism 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 options is to enable inclusive authentication in the lax mode and write a custom HTTP Security policy or a permission checker that verifies all registered HTTP Authentication mechanisms created a `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 require that all registered HTTP authentication mechanisms must create the `SecurityIdentity`.
However in the lax mode, authentication succeeds if at least one registered HTTP authentication mechanism 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 @@ -48,8 +48,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.deployment.builditem.ApplicationIndexBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
Expand Down Expand Up @@ -276,13 +276,14 @@ void createHttpAuthenticationHandler(HttpSecurityRecorder recorder, Capabilities
}
}

@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,48 @@ public interface AuthRuntimeConfig {
* Form Auth config
*/
FormAuthRuntimeConfig form();

/**
* 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();

/**
* By default, inclusive authentication will result in a failure if all registered HTTP authentication mechanisms
* do not create an identity. If you require that all registered HTTP authentication mechanisms complete
* the authentication, but not all of them must create the identity, use the 'lax' mode.
*/
@WithDefault("strict")
InclusiveMode inclusiveMode();

enum InclusiveMode {
/**
* Authentication succeeds if at least one of registered HTTP authentication mechanisms create the identity.
*/
LAX,
/**
* Authentication succeeds if all of registered HTTP authentication mechanisms create the identity.
*/
STRICT
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

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;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticator;
import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.AuthenticationHandler;
import io.quarkus.vertx.http.runtime.security.ManagementInterfaceHttpAuthorizer;
import io.quarkus.vertx.http.runtime.security.ManagementPathMatchingHttpSecurityPolicy;
Expand All @@ -26,9 +28,9 @@ public Handler<RoutingContext> getAuthenticationHandler(RuntimeValue<Authenticat
}

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

public Handler<RoutingContext> permissionCheckHandler() {
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
@Singleton
public final class HttpAuthorizer extends AbstractHttpAuthorizer {

HttpAuthorizer(HttpAuthenticator httpAuthenticator, IdentityProviderManager identityProviderManager,
HttpAuthorizer(IdentityProviderManager identityProviderManager,
AuthorizationController controller, Instance<HttpSecurityPolicy> installedPolicies,
BlockingSecurityExecutor blockingExecutor, BeanManager beanManager,
Event<AuthorizationFailureEvent> authZFailureEvent, Event<AuthorizationSuccessEvent> authZSuccessEvent,
@ConfigProperty(name = "quarkus.security.events.enabled") boolean securityEventsEnabled) {
super(httpAuthenticator, identityProviderManager, controller, toList(installedPolicies), beanManager, blockingExecutor,
super(identityProviderManager, controller, toList(installedPolicies), beanManager, blockingExecutor,
authZFailureEvent, authZSuccessEvent, securityEventsEnabled);
}

Expand Down
Loading

0 comments on commit 9d6fd56

Please sign in to comment.