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

Allow authentication mechanism selection for a REST endpoint with annotation #36504

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
85 changes: 84 additions & 1 deletion docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
* <<form-auth>>
* <<mutual-tls>>

[[basic-auth]]
=== Basic authentication

You can secure your Quarkus application endpoints with the built-in HTTP Basic authentication mechanism.
Expand Down Expand Up @@ -491,7 +492,7 @@
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.
====

=== Path-specific authentication mechanisms
=== 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 @@ -511,6 +512,88 @@

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

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
to select authentication mechanisms after a REST endpoint has been matched.
Here is how you can select an authentication mechanism per a REST endpoint basis:

[source,properties]
----
quarkus.http.auth.proactive=false
michalvavrik marked this conversation as resolved.
Show resolved Hide resolved
----

[source,java]
----
import io.quarkus.oidc.AuthorizationCodeFlow;
import io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("hello")
public class HelloResource {

@GET
@BasicAuthentication <1> <2>
@Path("basic")
public String basicAuthMechanism() {
return "basic";
}

@GET
@RolesAllowed("admin") <3>
@AuthorizationCodeFlow <4>
@Path("code-flow")
public String codeFlowAuthMechanism() {
return "code-flow";
}
}
----
<1> The REST endpoint `/hello/basic` can only ever be accessed by using the <<basic-auth>>.
<2> This endpoint requires authentication, because when no standard security annotation accompanies the `@BasicAuthentication` annotation, the `@Authenticated` annotation is added by default.

Check warning on line 555 in docs/src/main/asciidoc/security-authentication-mechanisms.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 555, "column": 108}}}, "severity": "INFO"}
<3> The `@AuthorizationCodeFlow` annotation can be combined with any other standard security annotation like `@RolesAllowed`, `@PermissionsAllowed` and others.
<4> The REST endpoint `/hello/code-flow` can only ever be accessed by using the xref:security-oidc-code-flow-authentication.adoc[OIDC authorization code flow mechanism].

.Supported authentication mechanism annotations
|===
^|Authentication mechanism^| Annotation

s|Basic authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.annotation.BasicAuthentication`
s|Form-based authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.annotation.FormAuthentication`
s|Mutual TLS authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.annotation.MTLSAuthentication`
s|WebAuthn authentication mechanism ^|`io.quarkus.security.webauthn.WebAuthn`
s|Bearer token authentication mechanism ^|`io.quarkus.oidc.BearerTokenAuthentication`
s|OIDC authorization code flow mechanism ^|`io.quarkus.oidc.AuthorizationCodeFlow`
s|SmallRye JWT authentication mechanism ^|`io.quarkus.smallrye.jwt.runtime.auth.BearerTokenAuthentication`

Check warning on line 569 in docs/src/main/asciidoc/security-authentication-mechanisms.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer. Raw Output: {"message": "[Quarkus.SentenceLength] Try to keep sentences to an average of 32 words or fewer.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 569, "column": 37}}}, "severity": "INFO"}
|===

TIP: Quarkus automatically secures endpoints annotated with the authentication mechanism annotation. When no standard security annotation is present on the REST endpoint and resource, the `io.quarkus.security.Authenticated` annotation is added for you.

It is also possible to use the `io.quarkus.vertx.http.runtime.security.annotation.HttpAuthenticationMechanism` annotation to select any authentication mechanism based on its scheme.
Annotation-based analogy to the `quarkus.http.auth.permission.basic.auth-mechanism=custom` configuration property is the `@HttpAuthenticationMechanism("custom")` annotation.

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

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

Check warning on line 579 in docs/src/main/asciidoc/security-authentication-mechanisms.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'How to combine it with HTTP Security Policy'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'How to combine it with HTTP Security Policy'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 579, "column": 6}}}, "severity": "INFO"}

The easiest way to define roles that are allowed to access individual resources is the `@RolesAllowed` annotation.
Nevertheless, it's also possible to use the HTTP Security Policy like in the example below:

[source,properties]
----
quarkus.http.auth.policy.roles1.roles-allowed=user
quarkus.http.auth.permission.roles1.paths=/hello/code-flow
quarkus.http.auth.permission.roles1.applies-to=JAXRS <1>
quarkus.http.auth.permission.roles1.policy=roles1
quarkus.http.auth.permission.roles1.methods=GET <2>
----
<1> Delay this policy's permission check after the endpoint-specific authentication mechanism has been selected.
<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.

[[proactive-auth]]
== Proactive authentication

Proactive authentication is enabled in Quarkus by default.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,22 @@

import static io.quarkus.arc.processor.BuiltinScope.APPLICATION;
import static io.quarkus.arc.processor.DotNames.DEFAULT;
import static io.quarkus.oidc.common.runtime.OidcConstants.BEARER_SCHEME;
import static io.quarkus.oidc.common.runtime.OidcConstants.CODE_FLOW_CODE;
import static io.quarkus.oidc.runtime.OidcUtils.DEFAULT_TENANT_ID;
import static io.quarkus.vertx.http.deployment.EagerSecurityInterceptorCandidateBuildItem.hasProperEndpointModifiers;
import static org.jboss.jandex.AnnotationTarget.Kind.CLASS;
import static org.jboss.jandex.AnnotationTarget.Kind.METHOD;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import jakarta.inject.Singleton;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
Expand All @@ -44,6 +39,8 @@
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.AuthorizationCodeFlow;
import io.quarkus.oidc.BearerTokenAuthentication;
import io.quarkus.oidc.IdToken;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantFeature;
Expand All @@ -67,14 +64,14 @@
import io.quarkus.oidc.runtime.providers.AzureAccessTokenCustomizer;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.vertx.core.deployment.CoreVertxBuildItem;
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorCandidateBuildItem;
import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBindingBuildItem;
import io.quarkus.vertx.http.deployment.HttpAuthMechanismAnnotationBuildItem;
import io.quarkus.vertx.http.deployment.SecurityInformationBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.smallrye.jwt.auth.cdi.ClaimValueProducer;
import io.smallrye.jwt.auth.cdi.CommonJwtProducer;
import io.smallrye.jwt.auth.cdi.JsonValueProducer;
import io.smallrye.jwt.auth.cdi.RawClaimTypeProducer;
import io.vertx.ext.web.RoutingContext;

@BuildSteps(onlyIf = OidcBuildStep.IsEnabled.class)
public class OidcBuildStep {
Expand Down Expand Up @@ -232,70 +229,17 @@ public SyntheticBeanBuildItem setup(

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
public void produceTenantResolverInterceptors(CombinedIndexBuildItem indexBuildItem,
Capabilities capabilities, OidcRecorder recorder,
BuildProducer<EagerSecurityInterceptorCandidateBuildItem> producer,
HttpBuildTimeConfig buildTimeConfig) {
public void registerTenantResolverInterceptor(Capabilities capabilities, OidcRecorder recorder,
HttpBuildTimeConfig buildTimeConfig,
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<EagerSecurityInterceptorBindingBuildItem> bindingProducer) {
if (!buildTimeConfig.auth.proactive
&& (capabilities.isPresent(Capability.RESTEASY_REACTIVE) || capabilities.isPresent(Capability.RESTEASY))) {
// provide method interceptor that will be run before security checks

// collect endpoint candidates
IndexView index = indexBuildItem.getIndex();
Map<MethodInfo, String> candidateToTenant = new HashMap<>();

for (AnnotationInstance annotation : index.getAnnotations(TENANT_NAME)) {

// validate tenant id
AnnotationTarget target = annotation.target();
if (annotation.value() == null || annotation.value().asString().isEmpty()) {
LOG.warnf("Annotation instance @Tenant placed on %s did not provide valid tenant", toTargetName(target));
continue;
}

// collect annotation instance methods
String tenant = annotation.value().asString();
if (target.kind() == METHOD) {
MethodInfo method = target.asMethod();
if (hasProperEndpointModifiers(method)) {
candidateToTenant.put(method, tenant);
} else {
LOG.warnf("Method %s is not valid endpoint, but is annotated with the '@Tenant' annotation",
toTargetName(target));
}
} else if (target.kind() == CLASS) {
// collect endpoint candidates; we only collect candidates, extensions like
// RESTEasy Reactive and others are still in control of endpoint selection and interceptors
// are going to be applied only on the actual endpoints
for (MethodInfo method : target.asClass().methods()) {
if (hasProperEndpointModifiers(method)) {
candidateToTenant.put(method, tenant);
}
}
}
}

// create 'interceptor' for each tenant that puts tenant id into routing context
if (!candidateToTenant.isEmpty()) {

Map<String, Consumer<RoutingContext>> tenantToInterceptor = candidateToTenant
.values()
.stream()
.distinct()
.map(tenant -> Map.entry(tenant, recorder.createTenantResolverInterceptor(tenant)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

candidateToTenant.forEach((method, tenant) -> {

// transform method info to description
String[] paramTypes = method.parameterTypes().stream().map(t -> t.name().toString()).toArray(String[]::new);
String className = method.declaringClass().name().toString();
String methodName = method.name();
var description = recorder.methodInfoToDescription(className, methodName, paramTypes);

producer.produce(new EagerSecurityInterceptorCandidateBuildItem(method, description,
tenantToInterceptor.get(tenant)));
});
var annotationInstances = combinedIndexBuildItem.getIndex().getAnnotations(TENANT_NAME);
if (!annotationInstances.isEmpty()) {
// register method interceptor that will be run before security checks
bindingProducer.produce(
new EagerSecurityInterceptorBindingBuildItem(recorder.tenantResolverInterceptorCreator(), TENANT_NAME));
}
}
}
Expand All @@ -322,6 +266,13 @@ void detectAccessTokenVerificationRequired(BeanRegistrationPhaseBuildItem beanRe
}
}

@BuildStep
List<HttpAuthMechanismAnnotationBuildItem> registerHttpAuthMechanismAnnotation() {
return List.of(
new HttpAuthMechanismAnnotationBuildItem(DotName.createSimple(AuthorizationCodeFlow.class), CODE_FLOW_CODE),
new HttpAuthMechanismAnnotationBuildItem(DotName.createSimple(BearerTokenAuthentication.class), BEARER_SCHEME));
}

private static boolean isInjected(BeanRegistrationPhaseBuildItem beanRegistrationPhaseBuildItem, DotName requiredType,
DotName withoutQualifier) {
for (InjectionPointInfo injectionPoint : beanRegistrationPhaseBuildItem.getInjectionPoints()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.oidc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import io.quarkus.oidc.runtime.CodeAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.annotation.HttpAuthenticationMechanism;

/**
* Selects {@link CodeAuthenticationMechanism}.
*
* @see HttpAuthenticationMechanism for more information
*/
@HttpAuthenticationMechanism("code")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface AuthorizationCodeFlow {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.oidc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import io.quarkus.vertx.http.runtime.security.annotation.HttpAuthenticationMechanism;

/**
* Selects {@link io.quarkus.oidc.runtime.BearerAuthenticationMechanism}.
*
* @see HttpAuthenticationMechanism for more information
*/
@HttpAuthenticationMechanism("Bearer")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface BearerTokenAuthentication {

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,13 @@
import io.quarkus.oidc.common.runtime.OidcCommonConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.TlsConfig;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.TokenAuthenticationRequest;
import io.quarkus.security.spi.runtime.BlockingSecurityExecutor;
import io.quarkus.security.spi.runtime.MethodDescription;
import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
Expand Down Expand Up @@ -107,10 +105,6 @@ public Uni<TenantConfigContext> apply(OidcTenantConfig config) {
};
}

public RuntimeValue<MethodDescription> methodInfoToDescription(String className, String methodName, String[] paramTypes) {
return new RuntimeValue<>(new MethodDescription(className, methodName, paramTypes));
}

private Uni<TenantConfigContext> createDynamicTenantContext(Vertx vertx,
OidcTenantConfig oidcConfig, TlsConfig tlsConfig, String tenantId) {

Expand Down Expand Up @@ -599,14 +593,19 @@ private static boolean fireOidcServerEvent(String authServerUrl, SecurityEvent.T
return false;
}

public Consumer<RoutingContext> createTenantResolverInterceptor(String tenantId) {
return new Consumer<RoutingContext>() {
public Function<String, Consumer<RoutingContext>> tenantResolverInterceptorCreator() {
return new Function<String, Consumer<RoutingContext>>() {
@Override
public void accept(RoutingContext routingContext) {
LOG.debugf("@Tenant annotation set a '%s' tenant id on the %s request path", tenantId,
routingContext.request().path());
routingContext.put(OidcUtils.TENANT_ID_SET_BY_ANNOTATION, tenantId);
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
public Consumer<RoutingContext> apply(String tenantId) {
return new Consumer<RoutingContext>() {
@Override
public void accept(RoutingContext routingContext) {
LOG.debugf("@Tenant annotation set a '%s' tenant id on the %s request path", tenantId,
routingContext.request().path());
routingContext.put(OidcUtils.TENANT_ID_SET_BY_ANNOTATION, tenantId);
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
}
};
}
};
}
Expand Down
Loading
Loading