diff --git a/pom.xml b/pom.xml index 11a5ce6..1397411 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,10 @@ io.quarkus quarkus-resteasy-reactive-jackson + + io.quarkus + quarkus-security-webauthn + io.quarkus quarkus-junit5 diff --git a/src/main/java/com/ysoft/geecon/dto/User.java b/src/main/java/com/ysoft/geecon/dto/User.java index c50beb5..2177d13 100644 --- a/src/main/java/com/ysoft/geecon/dto/User.java +++ b/src/main/java/com/ysoft/geecon/dto/User.java @@ -1,7 +1,31 @@ package com.ysoft.geecon.dto; -public record User(String login, String password) { +import com.ysoft.geecon.webauthn.WebAuthnCredential; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public record User(String login, String password, List credentials) { public boolean validatePassword(String password) { return this.password != null && this.password.equals(password); } + + public User withAddedCredential(WebAuthnCredential webAuthnCredential) { + Optional existing = credentials.stream() + .filter(credential -> credential.credID.equals(webAuthnCredential.credID)) + .findAny(); + + List newCredentials; + if (existing.isPresent()) { + // TODO need to decide if immutable or not + existing.get().counter++; + newCredentials = credentials; + } else { + newCredentials = new ArrayList<>(credentials); + newCredentials.add(webAuthnCredential); + } + + return new User(login, password, newCredentials); + } } diff --git a/src/main/java/com/ysoft/geecon/repo/UsersRepo.java b/src/main/java/com/ysoft/geecon/repo/UsersRepo.java index 7dbda6d..0bdf099 100644 --- a/src/main/java/com/ysoft/geecon/repo/UsersRepo.java +++ b/src/main/java/com/ysoft/geecon/repo/UsersRepo.java @@ -4,6 +4,7 @@ import com.ysoft.geecon.dto.User; import jakarta.enterprise.context.ApplicationScoped; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -12,7 +13,7 @@ public class UsersRepo { private final Map users = new HashMap<>(); public UsersRepo() { - register(new User("bob", "Password1")); + register(new User("bob", "Password1", List.of())); } public Optional getUser(String username) { @@ -22,4 +23,11 @@ public class UsersRepo { public void register(User user) { users.put(user.login(), user); } + + public Optional findByCredID(String credID) { + // TODO + return users.values().stream() + .filter(u -> u.credentials().stream().anyMatch(c -> c.credID.equals(credID))) + .findAny(); + } } diff --git a/src/main/java/com/ysoft/geecon/webauthn/MyWebAuthnSetup.java b/src/main/java/com/ysoft/geecon/webauthn/MyWebAuthnSetup.java new file mode 100644 index 0000000..400fe7c --- /dev/null +++ b/src/main/java/com/ysoft/geecon/webauthn/MyWebAuthnSetup.java @@ -0,0 +1,65 @@ +package com.ysoft.geecon.webauthn; + +import com.ysoft.geecon.dto.User; +import com.ysoft.geecon.repo.UsersRepo; +import io.quarkus.security.webauthn.WebAuthnUserProvider; +import io.smallrye.mutiny.Uni; +import io.vertx.ext.auth.webauthn.AttestationCertificates; +import io.vertx.ext.auth.webauthn.Authenticator; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.List; + +@ApplicationScoped +public class MyWebAuthnSetup implements WebAuthnUserProvider { + @Inject + UsersRepo usersRepo; + + private static List toAuthenticators(List dbs) { + return dbs.stream().map(MyWebAuthnSetup::toAuthenticator).toList(); + } + + private static Authenticator toAuthenticator(WebAuthnCredential credential) { + Authenticator ret = new Authenticator(); + ret.setAaguid(credential.aaguid); + AttestationCertificates attestationCertificates = new AttestationCertificates(); + attestationCertificates.setAlg(credential.alg); + attestationCertificates.setX5c(credential.x5c); + ret.setAttestationCertificates(attestationCertificates); + ret.setCounter(credential.counter); + ret.setCredID(credential.credID); + ret.setFmt(credential.fmt); + ret.setPublicKey(credential.publicKey); + ret.setType(credential.type); + ret.setUserName(credential.userName); + return ret; + } + + @Override + public Uni> findWebAuthnCredentialsByUserName(String userName) { + return Uni.createFrom().item(usersRepo.getUser(userName) + .map((User dbs) -> toAuthenticators(dbs.credentials())) + .orElse(List.of()) + ); + } + + @Override + public Uni> findWebAuthnCredentialsByCredID(String credID) { + return Uni.createFrom().item(usersRepo.findByCredID(credID) + .map((User dbs) -> toAuthenticators(dbs.credentials())) + .orElse(List.of()) + ); + } + + @Override + public Uni updateOrStoreWebAuthnCredentials(Authenticator authenticator) { + WebAuthnCredential credential1 = new WebAuthnCredential(authenticator); + usersRepo.getUser(authenticator.getUserName()) + .ifPresentOrElse( + user -> usersRepo.register(user.withAddedCredential(credential1)), + () -> usersRepo.register(new User(authenticator.getUserName(), null, List.of(credential1))) + ); + return Uni.createFrom().nullItem(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ysoft/geecon/webauthn/WebAuthnCredential.java b/src/main/java/com/ysoft/geecon/webauthn/WebAuthnCredential.java new file mode 100644 index 0000000..200c5ab --- /dev/null +++ b/src/main/java/com/ysoft/geecon/webauthn/WebAuthnCredential.java @@ -0,0 +1,95 @@ +package com.ysoft.geecon.webauthn; + +import io.vertx.ext.auth.webauthn.Authenticator; +import io.vertx.ext.auth.webauthn.PublicKeyCredential; + +import java.util.ArrayList; +import java.util.List; + + +public class WebAuthnCredential { + + /** + * 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: + *
{@code
+     *   {
+     *     "alg": "string",
+     *     "x5c": [
+     *       "base64"
+     *     ]
+     *   }
+     * }
+ */ + /** + * The algorithm used for the public credential + */ + public PublicKeyCredential alg; + + /** + * The list of X509 certificates encoded as base64url. + */ + public List x5c = new ArrayList<>(); + + public String fmt; + + public WebAuthnCredential() { + } + + public WebAuthnCredential(Authenticator authenticator) { + 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) { + this.x5c.addAll(authenticator.getAttestationCertificates().getX5c()); + } +// this.user = user; +// user.webAuthnCredential = this; + } + +// public static Uni> findByUserName(String userName) { +// return list("userName", userName); +// } +// +// public static Uni> findByCredID(String credID) { +// return list("credID", credID); +// } +// +// public Uni fetch(T association) { +// return getSession().flatMap(session -> session.fetch(association)); +// } +} \ No newline at end of file diff --git a/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java b/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java index c4bcc00..0e5d2b9 100644 --- a/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java +++ b/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java @@ -37,7 +37,7 @@ public class AuthCodeGrantTest { @BeforeEach void beforeAll() { clientsRepo.register(CLIENT); - usersRepo.register(new User("bob", "password")); + usersRepo.register(new User("bob", "password", List.of())); } @Test diff --git a/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java b/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java index 2dcb77f..2e3014e 100644 --- a/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java +++ b/src/test/java/com/ysoft/geecon/DeviceAuthGrantTest.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; import java.net.URI; +import java.util.List; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -44,7 +45,7 @@ public class DeviceAuthGrantTest { @BeforeEach void beforeAll() { clientsRepo.register(CLIENT); - usersRepo.register(new User("bob", "password")); + usersRepo.register(new User("bob", "password", List.of())); }