Skip to content

Commit

Permalink
Support annotation-based auth mechanism selection
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Apr 4, 2024
1 parent df43e7d commit 1373ed0
Show file tree
Hide file tree
Showing 58 changed files with 2,608 additions and 302 deletions.
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 @@ Quarkus Security provides the following built-in authentication support:
* <<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 @@ For example, you can combine the built-in Basic and the Quarkus `quarkus-oidc` B
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 @@ 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

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 <<openid-connect-authentication>> mechanism per a REST endpoint basis:

[source,properties]
----
quarkus.http.auth.proactive=false
----

[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

0 comments on commit 1373ed0

Please sign in to comment.