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 bf65b2b commit ac29c5a
Show file tree
Hide file tree
Showing 23 changed files with 468 additions and 67 deletions.
62 changes: 60 additions & 2 deletions docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,9 @@ For example, you can combine the built-in Basic and the Quarkus `quarkus-oidc` B

The authentication process completes as soon as the first `SecurityIdentity` is produced by one of the authentication mechanisms.

[[inclusive-authentication]]
=== Inclusive Authentication

In some cases it can be required that all registered authentication mechanisms create their `SecurityIdentity`.
It can be required when the credentials such as tokens have to be passed over <<mutual-tls>>,
for example, when users are authenticating via `Virtual Private Network`, or when the current token has to be bound
Expand Down Expand Up @@ -662,7 +665,11 @@ 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.
====

=== Use HTTP Security Policy to enable path-based authentication
[[path-based-authentication]]
=== Path-based authentication

[[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 All @@ -682,7 +689,7 @@ quarkus.http.auth.permission.bearer.auth-mechanism=bearer

Ensure that the value of the `auth-mechanism` property matches the authentication scheme supported by `HttpAuthenticationMechanism`, for example, `basic`, `bearer`, or `form`.

=== Use annotations to enable path-based authentication for Jakarta REST endpoints
==== Use annotations to enable path-based authentication for Jakarta REST endpoints

It is possible to use annotations to select an authentication mechanism specific to each Jakarta REST endpoint.
This feature is only enabled when <<proactive-auth>> is disabled due to the fact that the annotations can only be used
Expand Down Expand Up @@ -762,6 +769,57 @@ Annotation-based analogy to the `quarkus.http.auth.permission.basic.auth-mechani

NOTE: For consistency with various Jakarta EE specifications, it is recommended to always repeat annotations instead of relying on annotation inheritance.

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

By default, Quarkus only supports <<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`.

Let's assume that we have 3 registered mechanisms, mTLS, Basic and OIDC and you only require Basic and mTLS authentications to succeed to permit access to the `hello` method.
In this case, enabling an inclusive authentication in a lax mode allows to check which mechanisms produced the identity as shown in the example below:

.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 the endpoint only if it is confirmed that both `mTLS` and `Basic` authentication mechanisms have authenticated the current request.

==== How to combine it with HTTP Security Policy

The easiest way to define roles that are allowed to access individual resources is the `@RolesAllowed` annotation.
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,9 @@
package io.quarkus.vertx.http.deployment;

import io.quarkus.builder.item.EmptyBuildItem;

/**
* Marker used by Build Steps that perform tasks which must 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 @@ -32,6 +32,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 @@ -321,6 +322,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,52 @@ 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>
* All produced security identities can be retrieved using the following utility method:
*
* <pre>
* {@code
* io.quarkus.vertx.http.runtime.security.HttpSecurityUtils#getSecurityIdentities(io.quarkus.security.identity.SecurityIdentity)
* }
* </pre>
*
* An injected `SecurityIdentity` represents an identity produced by the first inclusive authentication mechanism.
* When the `mTLS` authentication is required, the `mTLS` mechanism is always the first mechanism,
* because its priority is elevated when inclusive authentication
* <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`.
*/
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
Loading

0 comments on commit ac29c5a

Please sign in to comment.