Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce inclusive authentication mode, improve documentation of inclusive authentication and make its config runtime #46183

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 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,10 @@ 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

==== 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 +688,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 +768,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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, given the example you give about relying on the lax mode inclusive authentication to decide which mechanisms did the authentication makes this line a bit confusing... Futhermore, once we at some point just let users configure more than one mechanism per path, the inclusive strict mode can impact if it is and or or combination... IMHO it is worth dropping this line and for us keep tuning things going forward.

Copy link
Member Author

@michalvavrik michalvavrik Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is also present in the main branch already.

Futhermore, once we at some point just let users configure more than one mechanism per path, the inclusive strict mode can impact if it is and or or combination

I am not sure if I understand you, but inclusive authentication does not apply when for certain path user specifically selected one mechanism. I don't think it even makes sense. If I annotate endpoint with @BasicAuthentication I expect to use this mechanism and not all the registered mechanisms. I suspect I misunderstood your comment?

IMHO it is worth dropping this line and for us keep tuning things going forward.

I think it is extremely important information. You need to know that if you select specific path-matching authentication mechanism, inclusive authentication does not happen.

Copy link
Member

@sberyozkin sberyozkin Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalvavrik

but inclusive authentication does not apply when for certain path user specifically selected one mechanism

It is just that you added a doc section called Using inclusive authentication to enable path-based authentication but this line says the inclusive authentication is ignored for the path based authentication. So the messaging is conflicting,

How about saying something like This property is ignored for the enabled path based authentication which can currently support only a single authentication mechanism.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michalvavrik Never mind, that doc section clarifies that it is used in the lax mode, the messaging around inclusive authentication can be reworked later when more than one mechanism is supported for a specific path

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about saying something like This property is ignored for the enabled path based authentication which can currently support only a single authentication mechanism.

I see, yes that is a good point.

@michalvavrik Never mind, that doc section clarifies that it is used in the lax mode, the messaging around inclusive authentication can be reworked later when more than one mechanism is supported for a specific path

I put #46167 on my list, I don't think it is realistic someone else will implement it anytime soon, so I'll try to go back to it in few months and we can revise this docs.

*/
@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
Loading