Skip to content

Commit

Permalink
WebAuthn: support running on virtual thread
Browse files Browse the repository at this point in the history
Fixes #38352
  • Loading branch information
FroMage committed Jan 24, 2024
1 parent 41411fe commit c050fdb
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 4 deletions.
9 changes: 9 additions & 0 deletions docs/src/main/asciidoc/security-webauthn.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,15 @@ data access with your `WebAuthnUserProvider`.
You will have to add the `@Blocking` annotation on your `WebAuthnUserProvider` class in order to tell the
Quarkus WebAuthn endpoints to defer those calls to the worker pool.

== Virtual-Threads version

If you're using a blocking data access to the database, you can safely block on the `WebAuthnSecurity` methods,
with `.await().indefinitely()`, because nothing is async in the `register` and `login` methods, besides the
data access with your `WebAuthnUserProvider`.

You will have to add the `@RunOnVirtualThread` annotation on your `WebAuthnUserProvider` class in order to tell the
Quarkus WebAuthn endpoints to defer those calls to virtual threads.

== Testing WebAuthn

Testing WebAuthn can be complicated because normally you need a hardware token, which is why we've made the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import jakarta.inject.Inject;

import io.quarkus.runtime.BlockingOperationControl;
import io.quarkus.virtual.threads.VirtualThreadsRecorder;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.smallrye.mutiny.Uni;
import io.vertx.core.Future;
import io.vertx.ext.auth.webauthn.Authenticator;
Expand Down Expand Up @@ -39,18 +41,38 @@ else if (query.getCredID() != null)
}

@SuppressWarnings({ "rawtypes", "unchecked" })
private <T> Uni<T> runPotentiallyBlocking(Supplier<Uni<T>> supplier) {
private <T> Uni<T> runPotentiallyBlocking(Supplier<Uni<? extends T>> supplier) {
if (BlockingOperationControl.isBlockingAllowed()
|| !isBlocking(userProvider.getClass()))
return supplier.get();
|| isNonBlocking(userProvider.getClass())) {
return (Uni<T>) supplier.get();
}
if (isRunOnVirtualThread(userProvider.getClass())) {
return Uni.createFrom().deferred(supplier).runSubscriptionOn(VirtualThreadsRecorder.getCurrent());
}
// run it in a worker thread
return vertx.executeBlocking(Uni.createFrom().deferred((Supplier) supplier));
}

private boolean isBlocking(Class<?> klass) {
private boolean isNonBlocking(Class<?> klass) {
do {
if (klass.isAnnotationPresent(NonBlocking.class))
return true;
if (klass.isAnnotationPresent(Blocking.class))
return false;
if (klass.isAnnotationPresent(RunOnVirtualThread.class))
return false;
klass = klass.getSuperclass();
} while (klass != null);
// no information, assumed non-blocking
return true;
}

private boolean isRunOnVirtualThread(Class<?> klass) {
do {
if (klass.isAnnotationPresent(RunOnVirtualThread.class))
return true;
if (klass.isAnnotationPresent(Blocking.class))
return false;
if (klass.isAnnotationPresent(NonBlocking.class))
return false;
klass = klass.getSuperclass();
Expand Down
1 change: 1 addition & 0 deletions integration-tests/virtual-threads/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<module>quartz-virtual-threads</module>
<module>virtual-threads-disabled</module>
<module>reactive-routes-virtual-threads</module>
<module>security-webauthn-virtual-threads</module>
</modules>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>quarkus-virtual-threads-integration-tests-parent</artifactId>
<groupId>io.quarkus</groupId>
<version>999-SNAPSHOT</version>
</parent>

<artifactId>quarkus-integration-test-virtual-threads-security-webauthn</artifactId>
<name>Quarkus - Integration Tests - Virtual Threads - Security WebAuthn</name>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-webauthn</artifactId>
</dependency>
<!-- Use the "compile" scope because we need to include the VirtualThreadsAssertions in the app -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-vertx</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<!--We use it in compile scope -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-webauthn</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.junit5</groupId>
<artifactId>junit5-virtual-threads</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-webauthn-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.virtual.security.webauthn;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.test.vertx.VirtualThreadsAssertions;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;

@RunOnVirtualThread
@Path("/")
public class TestResource {
@Inject
SecurityIdentity identity;

@Authenticated
@Path("secure")
@GET
public String getUserName() {
VirtualThreadsAssertions.assertEverything();
return identity.getPrincipal().getName() + ": " + identity.getRoles();
}

@RolesAllowed("admin")
@Path("admin")
@GET
public String getAdmin() {
VirtualThreadsAssertions.assertEverything();
return "OK";
}

@RolesAllowed("cheese")
@Path("cheese")
@GET
public String getCheese() {
VirtualThreadsAssertions.assertEverything();
return "OK";
}

@Path("open")
@GET
public String hello() {
VirtualThreadsAssertions.assertEverything();
return "Hello";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkus.virtual.security.webauthn;

import java.util.List;

import io.quarkus.test.security.webauthn.WebAuthnTestUserProvider;
import io.quarkus.test.vertx.VirtualThreadsAssertions;
import io.smallrye.common.annotation.RunOnVirtualThread;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.auth.webauthn.Authenticator;
import jakarta.enterprise.context.ApplicationScoped;

/**
* This UserProvider stores and updates the credentials in the callback endpoint, but is blocking
*/
@ApplicationScoped
@RunOnVirtualThread
public class WebAuthnVirtualThreadTestUserProvider extends WebAuthnTestUserProvider {
@Override
public Uni<List<Authenticator>> findWebAuthnCredentialsByCredID(String credId) {
assertVirtualThread();
return super.findWebAuthnCredentialsByCredID(credId);
}

@Override
public Uni<List<Authenticator>> findWebAuthnCredentialsByUserName(String userId) {
assertVirtualThread();
return super.findWebAuthnCredentialsByUserName(userId);
}

@Override
public Uni<Void> updateOrStoreWebAuthnCredentials(Authenticator authenticator) {
assertVirtualThread();
return super.updateOrStoreWebAuthnCredentials(authenticator);
}

private void assertVirtualThread() {
// allow this being used in the tests
if (isTestThread())
return;
VirtualThreadsAssertions.assertEverything();
}

static boolean isTestThread() {
for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) {
if (stackTraceElement.getClassName().equals("io.quarkus.test.junit.QuarkusTestExtension"))
return true;
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.virtual.security.webauthn;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
class RunOnVirtualThreadIT extends RunOnVirtualThreadTest {

}
Loading

0 comments on commit c050fdb

Please sign in to comment.