Skip to content

Commit

Permalink
Combining authentication mechanisms per endpoint using annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Oct 21, 2023
1 parent 2b26502 commit 078308b
Show file tree
Hide file tree
Showing 51 changed files with 2,535 additions and 234 deletions.
124 changes: 124 additions & 0 deletions docs/src/main/asciidoc/security-authentication-mechanisms.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,130 @@ 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
----

[source,java]
----
import io.quarkus.oidc.Bearer;
import io.quarkus.oidc.CodeFlow;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Path;
@Path("oidc")
public class OidcResource1 {
@Bearer <1> <2>
@Path("bearer")
public String bearerAuthMechanism() {
return "bearer";
}
@RolesAllowed <3>
@CodeFlow <4>
@Path("code-flow")
public String codeFlowAuthMechanism() {
return "code-flow";
}
}
----
<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 `@Bearer` annotation, the `@Authenticated` annotation is added by default.
<3> The `@CodeFlow` 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].

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

s|Basic authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.Basic`
s|Form-based authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.Form`
s|Mutual TLS authentication mechanism ^|`io.quarkus.vertx.http.runtime.security.MTLS`
s|WebAuthn authentication mechanism ^|`io.quarkus.security.webauthn.WebAuthn`
s|Bearer token authentication mechanism ^|`io.quarkus.oidc.Bearer`
s|OIDC authorization code flow mechanism ^|`io.quarkus.oidc.CodeFlow`
|===

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.HttpAuthMechanism` annotation to select any authentication mechanism based on its scheme.
Annotation-based analogy to the `quarkus.http.auth.permission.basic.auth-mechanism=basic` configuration property is the `@HttpAuthMechanism("basic")` annotation.

[NOTE]
====
For consistency with various Jakarta EE specifications, it is recommended to always repeat annotations instead of relying on annotation inheritance.
If you can't avoid relying on annotation inheritance, please remember following points and verify you test authorization:
* When used with the RESTEasy Classic, using the `io.quarkus.vertx.http.runtime.security.HttpAuthMechanism` annotation on interface and overridden methods is not supported.
* Standard security annotations placed on interfaces are ignored, therefore even the `@Authenticated` annotation added for you by default will be ignored.
* Things are simpler with RESTEasy Reactive, the selected authentication mechanism always come from the resource class, where the endpoint declaration is annotated with `jakarta.ws.rs.Path` as you can see in the example below:
.RESTEasy Reactive authentication mechanism selection inheritance example
[source,java]
----
@Form
public abstract class AbstractHelloResource {
@GET
@Path("/world") <1>
public String helloWorld() {
return "Hello World";
}
@MTLS
@GET
@Path("/city") <2>
public String helloCity() {
return "Hello City";
}
public String helloVillage() {
return ".";
}
}
@Authenticated
@Basic
@Path("/hello")
public class HelloResource extends AbstractHelloResource {
@Override
public String helloWorld() {
return super.helloWorld() + "!";
}
@Override
public String helloCity() {
return super.helloCity() + "?";
}
@Override
@GET
@Path("/village") <3>
public String helloVillage() {
return "Hello Village" + super.helloVillage();
}
}
----
<1> The REST endpoint `/hello/world` will require the Form-based authentication.
<2> The REST endpoint `/hello/city` will require the mutual TLS authentication.
<3> The REST endpoint `hello/village` will require the Basic authentication.
====


[[proactive-auth]]
== Proactive authentication

Proactive authentication is enabled in Quarkus by default.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
package io.quarkus.oidc.deployment;

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 static io.quarkus.oidc.common.runtime.OidcConstants.BEARER_SCHEME;
import static io.quarkus.oidc.common.runtime.OidcConstants.CODE_FLOW_CODE;

import java.util.HashMap;
import java.util.Map;
import java.util.List;
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.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 @@ -36,6 +28,8 @@
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.oidc.Bearer;
import io.quarkus.oidc.CodeFlow;
import io.quarkus.oidc.SecurityEvent;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TokenIntrospectionCache;
Expand All @@ -56,14 +50,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 @@ -158,80 +152,25 @@ public void findSecurityEventObservers(

@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(TENANT_NAME, recorder.tenantResolverInterceptorCreator()));
}
}
}

private static String toTargetName(AnnotationTarget target) {
if (target.kind() == CLASS) {
return target.asClass().name().toString();
} else {
return target.asMethod().declaringClass().name().toString() + "#" + target.asMethod().name();
}
@BuildStep
List<HttpAuthMechanismAnnotationBuildItem> registerHttpAuthMechanismAnnotation() {
return List.of(new HttpAuthMechanismAnnotationBuildItem(DotName.createSimple(CodeFlow.class), CODE_FLOW_CODE),
new HttpAuthMechanismAnnotationBuildItem(DotName.createSimple(Bearer.class), BEARER_SCHEME));
}

public static class IsEnabled implements BooleanSupplier {
Expand Down
20 changes: 20 additions & 0 deletions extensions/oidc/runtime/src/main/java/io/quarkus/oidc/Bearer.java
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.HttpAuthMechanism;

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

}
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.HttpAuthMechanism;

/**
* Selects {@link CodeAuthenticationMechanism}.
* Equivalent to '@HttpAuthMechanism("code")'.
*
* @see HttpAuthMechanism for more information
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface CodeFlow {

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@
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.spi.runtime.MethodDescription;
import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm;
import io.smallrye.jwt.util.KeyUtils;
import io.smallrye.mutiny.Uni;
Expand Down Expand Up @@ -87,10 +85,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 @@ -485,11 +479,16 @@ private static OidcConfigurationMetadata createLocalMetadata(OidcTenantConfig oi
oidcConfig.token.issuer.orElse(null));
}

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) {
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
public Consumer<RoutingContext> apply(String tenantId) {
return new Consumer<RoutingContext>() {
@Override
public void accept(RoutingContext routingContext) {
routingContext.put(OidcUtils.TENANT_ID_ATTRIBUTE, tenantId);
}
};
}
};
}
Expand Down
Loading

0 comments on commit 078308b

Please sign in to comment.