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 16, 2023
1 parent ec4074a commit 44a9bff
Show file tree
Hide file tree
Showing 50 changed files with 2,398 additions and 235 deletions.
118 changes: 118 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,124 @@ 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

If you are using either the RESTEasy Reactive extension or the RESTEasy Classic extension, you can select authentication mechanism specific for each REST endpoint and/or REST resource.

Check warning on line 366 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 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 366, "column": 11}}}, "severity": "INFO"}

Check warning on line 366 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.TermsWarnings] Consider using 'a or b' or 'a, b, or both' rather than 'and/or' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'a or b' or 'a, b, or both' rather than 'and/or' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 366, "column": 164}}}, "severity": "WARNING"}
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 io.quarkus.security.Authenticated;
import jakarta.ws.rs.Path;
@Authenticated
@Path("oidc")
public class OidcResource1 {
@Bearer <1>
@Path("bearer")
public String bearerAuthMechanism() {
return "bearer";
}
@CodeFlow <2>
@Path("code-flow")
public String codeFlowAuthMechanism() {
return "code-flow";
}
}
----
<1> The REST endpoint `/oidc/bearer` can only ever be accessed using the xref:security-oidc-bearer-token-authentication.adoc[Bearer token authentication].

Check warning on line 400 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 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 400, "column": 63}}}, "severity": "INFO"}
<2> The REST endpoint `/oidc/code-flow` can only ever be accessed using the xref:security-oidc-code-flow-authentication.adoc[OIDC authorization code flow mechanism].

Check warning on line 401 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 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 401, "column": 66}}}, "severity": "INFO"}

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

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 keep in mind following points and make sure you test authorization:

Check warning on line 421 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.TermsWarnings] Consider using 'remember' rather than 'keep in mind' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'remember' rather than 'keep in mind' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 421, "column": 62}}}, "severity": "WARNING"}

Check warning on line 421 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.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'verify' rather than 'make sure' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 421, "column": 96}}}, "severity": "WARNING"}
* When used with the RESTEasy Classic, using the `io.quarkus.vertx.http.runtime.security.HttpAuthMechanism` annotation on interface and overridden methods is not supported.
* 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 Form-based authentication.
<2> The REST endpoint `/hello/city` will require Mutual TLS authentication.

Check warning on line 476 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 'Mutual TLS authentication'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Mutual TLS authentication'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 476, "column": 50}}}, "severity": "INFO"}

Check warning on line 476 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.CaseSensitiveTerms] Use 'mutual TLS' rather than 'Mutual TLS'. Raw Output: {"message": "[Quarkus.CaseSensitiveTerms] Use 'mutual TLS' rather than 'Mutual TLS'.", "location": {"path": "docs/src/main/asciidoc/security-authentication-mechanisms.adoc", "range": {"start": {"line": 476, "column": 50}}}, "severity": "INFO"}
<3> The REST endpoint `hello/village` will require 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
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ static Optional<AnnotationInstance> searchPathAnnotationOnInterfaces(CombinedInd
* @param resultAcc accumulator for tail-recursion
* @return Collection of all interfaces und their parents. Never null.
*/
private static Collection<ClassInfo> getAllClassInterfaces(
static Collection<ClassInfo> getAllClassInterfaces(
CombinedIndexBuildItem index,
Collection<ClassInfo> classInfos,
List<ClassInfo> resultAcc) {
Expand Down
Loading

0 comments on commit 44a9bff

Please sign in to comment.