Skip to content

Commit

Permalink
create security/webauthn module with reactive mysql client
Browse files Browse the repository at this point in the history
create quarkus webauthn code with reactive mysql client and compose yaml

do some tweaks and add init.sql

delete files not needed in the module for quarkus test suite

delete docker files not needed in the module for quarkus test suite

modify Readme

fix some annotation and create src/test

add security/webauthn module

add LoginResource class

remove init.sql and modify mysql.properties

create WebAuthnCredential method to associate with the User

add method to associate with WebAuthnCredential to the new User

add logOut tests

variable more descriptive

use vertx HttpClient for testing purposes and rename of xc5 variables

remove when from rest assured and add indentation

delete uncecessary dependencies

register a User with VirtualAuthenticator and selenium

add dependencies needed for selenium and also jupiter junit

add AbstractWebAuthnPlaywright example

add WebDriverWait wait variable

fix pom conflicts

close http client and vertx to prevent leaks

use MySqlService parameters withProperties

create basic tests on AbstractWebAuthnTest

remove classes not needed for now

add failed testRegisterSameUserShouldNotAllowed

change assertThat on testRegisterSameUserShouldNotAllowed

add AdminResource

add properties as static final and remove restAssured check from setUp

use upstream mysql80 image

call onSuccess after http.requests

add MyWebAuthnHardware class

tweak logic with all methods and parameters needed to register a user

add methods invokeRegistration and invokeCallback  needed in the user registration

with mysql.80.image tests works

add openshift tests

add logoutUser method

tweak to new user credential persist

add security/webauthn to README

refactor name method

remove UTF8 propery drom mysql.properties

add cbor library

add admin checks

use the getApp().getHost() instead of localhost

improve security/webauthn description in README

add some method order and another tweaks

drop jackson-cbor version

change deprecate getApp().getHost() method

rename class to OpenShiftWebAuthnIT to be included according maven pattern configuration

change expectedLog

change localhost rpDomain to app.getUri()

remove drop and create from mysql.properties

add properties quarkus.build.skip

fix regex header because failed on openshift

add TODO on OpenShift scenario with issue quarkus-qe#1500
  • Loading branch information
jcarranzan committed Nov 3, 2023
1 parent 3ef90ed commit d99a1b7
Show file tree
Hide file tree
Showing 19 changed files with 1,195 additions and 81 deletions.
184 changes: 103 additions & 81 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@
<module>security/keycloak-oidc-client-reactive-extended</module>
<module>security/vertx-jwt</module>
<module>security/oidc-client-mutual-tls</module>
<module>security/webauthn</module>
</modules>
</profile>
<profile>
Expand Down
49 changes: 49 additions & 0 deletions security/webauthn/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus.ts.qe</groupId>
<artifactId>parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../..</relativePath>
</parent>
<artifactId>security-webauthn</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>Quarkus QE TS: Security: WebAuth</name>
<properties>
<quarkus.build.skip>true</quarkus.build.skip>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-mysql-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security-webauthn</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-reactive-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus.qe</groupId>
<artifactId>quarkus-test-service-database</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-cbor</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.ts.security.webauthn.api;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String adminResource() {
return "admin";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package io.quarkus.ts.security.webauthn.api;

import jakarta.inject.Inject;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;

import org.jboss.resteasy.reactive.RestForm;

import io.quarkus.hibernate.reactive.panache.common.runtime.ReactiveTransactional;
import io.quarkus.security.webauthn.WebAuthnLoginResponse;
import io.quarkus.security.webauthn.WebAuthnRegisterResponse;
import io.quarkus.security.webauthn.WebAuthnSecurity;
import io.quarkus.ts.security.webauthn.model.User;
import io.quarkus.ts.security.webauthn.model.WebAuthnCredential;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.auth.webauthn.Authenticator;
import io.vertx.ext.web.RoutingContext;

@Path("")
public class LoginResource {

@Inject
WebAuthnSecurity webAuthnSecurity;

@Path("/login")
@POST
@ReactiveTransactional
public Uni<Response> login(@RestForm String userName,
@BeanParam WebAuthnLoginResponse webAuthnResponse,
RoutingContext ctx) {
// Input validation
if (userName == null || userName.isEmpty()
|| !webAuthnResponse.isSet()
|| !webAuthnResponse.isValid()) {
return Uni.createFrom().item(Response.status(Response.Status.BAD_REQUEST).build());
}

Uni<User> userUni = User.findByUserName(userName);
return userUni.flatMap(user -> {
if (user == null) {
// Invalid user
return Uni.createFrom().item(Response.status(Response.Status.BAD_REQUEST).build());
}
Uni<Authenticator> authenticator = this.webAuthnSecurity.login(webAuthnResponse, ctx);

return authenticator
// bump the auth counter
.invoke(auth -> user.webAuthnCredential.counter = auth.getCounter())
.map(auth -> {
// make a login cookie
this.webAuthnSecurity.rememberUser(auth.getUserName(), ctx);
return Response.ok().build();
})
// handle login failure
.onFailure().recoverWithItem(x -> {
// make a proper error response
return Response.status(Response.Status.BAD_REQUEST).build();
});

});
}

@Path("/register")
@POST
@ReactiveTransactional
public Uni<Response> register(@RestForm String userName,
@BeanParam WebAuthnRegisterResponse webAuthnResponse,
RoutingContext ctx) {
// Input validation
if (userName == null || userName.isEmpty()
|| !webAuthnResponse.isSet()
|| !webAuthnResponse.isValid()) {
return Uni.createFrom().item(Response.status(Response.Status.BAD_REQUEST).build());
}

Uni<User> userUni = User.findByUserName(userName);
return userUni.flatMap(user -> {
if (user != null) {
// Duplicate user
return Uni.createFrom().item(Response.status(Response.Status.BAD_REQUEST).build());
}
Uni<Authenticator> authenticator = this.webAuthnSecurity.register(webAuthnResponse, ctx);

return authenticator
// store the user
.flatMap(auth -> {
User newUser = new User();
newUser.userName = auth.getUserName();
WebAuthnCredential credential = new WebAuthnCredential(auth, newUser);
return credential.persist()
.flatMap(c -> newUser.<User> persist());

})
.map(newUser -> {
// make a login cookie
this.webAuthnSecurity.rememberUser(newUser.userName, ctx);
return Response.ok().build();
})
// handle login failure
.onFailure().recoverWithItem(x -> {
// make a proper error response
return Response.status(Response.Status.BAD_REQUEST).build();
});

});
}

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

import java.security.Principal;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/api/public")
public class PublicResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String publicResource() {
return "public";
}

@GET
@Path("/me")
@Produces(MediaType.TEXT_PLAIN)
public String me(@Context SecurityContext securityContext) {
Principal user = securityContext.getUserPrincipal();
return user != null ? user.getName() : "<not logged in>";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.ts.security.webauthn.api;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("/api/users")
public class UserResource {
@GET
@RolesAllowed("user")
@Path("/me")
public String me(@Context SecurityContext securityContext) {
return securityContext.getUserPrincipal().getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.ts.security.webauthn.model;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;

import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import io.smallrye.mutiny.Uni;

@Table(name = "user_table")
@Entity
public class User extends PanacheEntity {

@Column(unique = true)
public String userName;

@OneToOne(mappedBy = "user")
public WebAuthnCredential webAuthnCredential;

public static Uni<User> findByUserName(String userName) {
return find("userName", userName).firstResult();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.ts.security.webauthn.model;

import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;

import io.quarkus.hibernate.reactive.panache.PanacheEntity;

@Entity
public class WebAuthnCertificate extends PanacheEntity {
@ManyToOne
public WebAuthnCredential webAuthnCredential;

/**
* The list of X509 certificates encoded as base64url.
*/
public String base64X509Certificate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.quarkus.ts.security.webauthn.model;

import java.util.ArrayList;
import java.util.List;

import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

import io.quarkus.hibernate.reactive.panache.PanacheEntity;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.auth.webauthn.Authenticator;
import io.vertx.ext.auth.webauthn.PublicKeyCredential;

@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "userName", "credID" }))
@Entity
public class WebAuthnCredential extends PanacheEntity {
/**
* The username linked to this authenticator
*/
public String userName;

/**
* The type of key (must be "public-key")
*/
public String type = "public-key";

/**
* The non user identifiable id for the authenticator
*/
public String credID;

/**
* The public key associated with this authenticator
*/
public String publicKey;

/**
* The signature counter of the authenticator to prevent replay attacks
*/
public long counter;

public String aaguid;

/**
* The Authenticator attestation certificates object, a JSON like:
*
* <pre>{@code
* {
* "alg": "string",
* "x5c": [
* "base64"
* ]
* }
* }</pre>
*/
/**
* The algorithm used for the public credential
*/
public PublicKeyCredential alg;

/**
* The list of X509 certificates encoded as base64url.
*/
@OneToMany(mappedBy = "webAuthnCredential")
public List<WebAuthnCertificate> webAuthnx509Certificates = new ArrayList<>();

public String fmt;

// owning side
@OneToOne
public User user;

public WebAuthnCredential() {
}

public WebAuthnCredential(Authenticator authenticator, User user) {
aaguid = authenticator.getAaguid();
if (authenticator.getAttestationCertificates() != null)
alg = authenticator.getAttestationCertificates().getAlg();
counter = authenticator.getCounter();
credID = authenticator.getCredID();
fmt = authenticator.getFmt();
publicKey = authenticator.getPublicKey();
type = authenticator.getType();
userName = authenticator.getUserName();
if (authenticator.getAttestationCertificates() != null
&& authenticator.getAttestationCertificates().getX5c() != null) {
for (String x509VCertificate : authenticator.getAttestationCertificates().getX5c()) {
WebAuthnCertificate cert = new WebAuthnCertificate();
cert.base64X509Certificate = x509VCertificate;
cert.webAuthnCredential = this;
this.webAuthnx509Certificates.add(cert);
}
}
this.user = user;
user.webAuthnCredential = this;
}

public static Uni<WebAuthnCredential> createWebAuthnCredential(Authenticator authenticator, User user) {
WebAuthnCredential credential = new WebAuthnCredential(authenticator, user);
credential.persistAndFlush();
user.webAuthnCredential = credential;
user.persistAndFlush();
return Uni.createFrom().item(credential);
}

public static Uni<List<WebAuthnCredential>> findByUserName(String userName) {
return list("userName", userName);
}

public static Uni<List<WebAuthnCredential>> findByCredID(String credID) {
return list("credID", credID);
}

public <T> Uni<T> fetch(T association) {
return getSession().flatMap(session -> session.fetch(association));
}
}
Loading

0 comments on commit d99a1b7

Please sign in to comment.