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 2, 2024
1 parent b870f32 commit 1f127d2
Show file tree
Hide file tree
Showing 52 changed files with 2,544 additions and 299 deletions.
81 changes: 81 additions & 0 deletions docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,87 @@ 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`.

=== Endpoint-specific authentication mechanisms

It is possible to select authentication mechanism specific for each Jakarta REST endpoint and Jakarta REST resource with annotation.
This feature is only enabled when <<proactive-auth>> is disabled, for authentication selection must happen 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
# combine HTTP permissions with the endpoint-specific authentication mechanism
quarkus.http.auth.policy.roles1.roles-allowed=admin
quarkus.http.auth.permission.roles1.paths=/oidc/http-perms-with-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.

[source,java]
----
import io.quarkus.oidc.BearerTokenAuthentication;
import io.quarkus.oidc.AuthorizationCodeFlow;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("oidc")
public class OidcResource1 {
@GET
@BearerTokenAuthentication <1> <2>
@Path("bearer")
public String bearerAuthMechanism() {
return "bearer";
}
@GET
@RolesAllowed("admin") <3>
@AuthorizationCodeFlow <4>
@Path("code-flow")
public String codeFlowAuthMechanism() {
return "code-flow";
}
@GET
@AuthorizationCodeFlow <5>
@Path("http-perms-with-code-flow")
public String httpPermsWithCodeFlow() {
return "unreachable";
}
}
----
<1> The REST endpoint `/oidc/bearer` can only ever be accessed by using the xref:security-oidc-bearer-token-authentication.adoc[Bearer token authentication].
<2> This endpoint requires authentication, because when no standard security annotation accompanies the `@BearerTokenAuthentication` annotation, the `@Authenticated` annotation is added by default.
<3> The `@AuthorizationCodeFlow` annotation can be combined with any other standard security annotation like `@RolesAllowed`, `@PermissionsAllowed` and others.
<4> The REST endpoint `/oidc/code-flow` can only ever be accessed by using the xref:security-oidc-code-flow-authentication.adoc[OIDC authorization code flow mechanism].
<5> The HTTP permission `roles1` secures the `/oidc/http-perms-with-code-flow` request path, therefore the `admin` role is required.

.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`

Check warning on line 550 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": 550, "column": 35}}}, "severity": "INFO"}
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`
|===

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.

[[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 1f127d2

Please sign in to comment.