From b96e8dd1cdf00857aaa2ac7ca1ef2e2b608506d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Thu, 28 Jul 2022 18:55:29 +0300 Subject: [PATCH] WebAuthn: Add user id to PublicKeyCredentialsCreateOptions, Authenticator and WebAuthnCredentials (#580, #581) --- .../auth/webauthn/AuthenticatorConverter.java | 7 ++ .../WebAuthnCredentialsConverter.java | 8 ++ .../ext/auth/webauthn/Authenticator.java | 14 +++ .../io/vertx/ext/auth/webauthn/WebAuthn.java | 21 +++- .../auth/webauthn/WebAuthnCredentials.java | 10 ++ .../ext/auth/webauthn/impl/WebAuthnImpl.java | 26 ++++- .../vertx/ext/auth/webauthn/DummyStore.java | 17 +++- .../webauthn/NavigatorCredentialsCreate.java | 95 ++++++++++++++++++- .../vertx/ext/auth/webauthn/WebAuthNTest.java | 84 ++++++++++++++-- 9 files changed, 262 insertions(+), 20 deletions(-) diff --git a/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/AuthenticatorConverter.java b/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/AuthenticatorConverter.java index 886b3ca43..f164d4d0d 100644 --- a/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/AuthenticatorConverter.java +++ b/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/AuthenticatorConverter.java @@ -60,6 +60,10 @@ public static void fromJson(Iterable> json, obj.setUserName((String)member.getValue()); } break; + case "userId": + if (member.getValue() instanceof String) { + obj.setUserId((String)member.getValue()); + } } } } @@ -91,5 +95,8 @@ public static void toJson(Authenticator obj, java.util.Map json) if (obj.getUserName() != null) { json.put("userName", obj.getUserName()); } + if (obj.getUserId() != null) { + json.put("userId", obj.getUserId()); + } } } diff --git a/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/WebAuthnCredentialsConverter.java b/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/WebAuthnCredentialsConverter.java index e096aff9d..ab38736e6 100644 --- a/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/WebAuthnCredentialsConverter.java +++ b/vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/WebAuthnCredentialsConverter.java @@ -45,6 +45,11 @@ public static void fromJson(Iterable> json, obj.setWebauthn(((JsonObject)member.getValue()).copy()); } break; + case "userId": + if (member.getValue() instanceof String) { + obj.setUserId((String)member.getValue()); + } + break; } } } @@ -69,5 +74,8 @@ public static void toJson(WebAuthnCredentials obj, java.util.Map if (obj.getWebauthn() != null) { json.put("webauthn", obj.getWebauthn()); } + if (obj.getUserId() != null) { + json.put("userId", obj.getUserId()); + } } } diff --git a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/Authenticator.java b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/Authenticator.java index 251b89459..c8b9e4e29 100644 --- a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/Authenticator.java +++ b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/Authenticator.java @@ -73,6 +73,11 @@ public class Authenticator { private AttestationCertificates attestationCertificates; private String fmt; + /** + * The base64 url encoded user handle associated with this authenticator. + */ + private String userId; + public Authenticator() {} public Authenticator(JsonObject json) { AuthenticatorConverter.fromJson(json, this); @@ -168,4 +173,13 @@ public Authenticator setAaguid(String aaguid) { public String getAaguid() { return aaguid; } + + public String getUserId() { + return userId; + } + + public Authenticator setUserId(String userId) { + this.userId = userId; + return this; + } } diff --git a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthn.java b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthn.java index fd3df21bf..4774d2136 100644 --- a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthn.java +++ b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthn.java @@ -20,6 +20,7 @@ import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.*; import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; import io.vertx.ext.auth.authentication.AuthenticationProvider; import io.vertx.ext.auth.webauthn.impl.WebAuthnImpl; @@ -59,7 +60,24 @@ static WebAuthn create(Vertx vertx, WebAuthnOptions options) { * Gets a challenge and any other parameters for the {@code navigator.credentials.create()} call. * * The object being returned is described here https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions - * @param user - the user object with name and optionally displayName and icon + * + * The caller should extract the generated challenge and store it, so it can be fetched later for the + * {@link #authenticate(JsonObject)} call. The challenge could for example be stored in a session and later + * pulled from there. + * + * The user object should contain base64 url encoded id (the user handle), name and, optionally, displayName and icon. + * See the above link for more documentation on the content of the different fields. The user handle should be base64 + * url encoded. You can use java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(byte[]) + * to encode any user id bytes to base64 url format. + * + * For backwards compatibility, if user id is not defined, a random UUID will be generated instead. This has some + * drawbacks, as it might cause user to register the same authenticator multiple times. + * + * Will use the configured {@link #authenticatorFetcher(Function)} to fetch any existing authenticators + * by the user id or name. Any authenticators found will be added as excludedCredentials, so the application + * knows not to register those again. + * + * @param user - the user object with id, name and optionally displayName and icon * @param handler server encoded make credentials request * @return fluent self */ @@ -106,6 +124,7 @@ default WebAuthn getCredentialsOptions(@Nullable String name, Handlerexclusively, while performing the lookup: *
    + *
  • {@link Authenticator#getUserId()}
  • *
  • {@link Authenticator#getUserName()}
  • *
  • {@link Authenticator#getCredID()} ()}
  • *
diff --git a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthnCredentials.java b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthnCredentials.java index 325cdf07a..b67381d00 100644 --- a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthnCredentials.java +++ b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthnCredentials.java @@ -26,6 +26,7 @@ public class WebAuthnCredentials implements Credentials { private String challenge; private JsonObject webauthn; private String username; + private String userId; private String origin; private String domain; @@ -80,6 +81,15 @@ public WebAuthnCredentials setDomain(String domain) { return this; } + public String getUserId() { + return userId; + } + + public WebAuthnCredentials setUserId(String userId) { + this.userId = userId; + return this; + } + @Override public void checkValid(V arg) throws CredentialValidationException { if (challenge == null || challenge.length() == 0) { diff --git a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/impl/WebAuthnImpl.java b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/impl/WebAuthnImpl.java index 61662b5c4..3e9c4682a 100644 --- a/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/impl/WebAuthnImpl.java +++ b/vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/impl/WebAuthnImpl.java @@ -159,9 +159,20 @@ public WebAuthn authenticatorUpdater(Function> updat @Override public Future createCredentialsOptions(JsonObject user) { + String userId; + + if (user.getString("id") != null) { + userId = user.getString("id"); + } else if (user.getString("rawId") != null) { + // For backwards compatibility, allow using rawId in place of id. Should be removed in future. + userId = user.getString("rawId"); + } else { + // For backwards compatibility, if both id and rawId is missing, use a random base64url encoded UUID + userId = uUIDtoBase64Url(UUID.randomUUID()); + } return fetcher - .apply(new Authenticator().setUserName(user.getString("name"))) + .apply(new Authenticator().setUserId(userId).setUserName(user.getString("name"))) .map(authenticators -> { // empty structure with all required fields JsonObject json = new JsonObject() @@ -177,10 +188,11 @@ public Future createCredentialsOptions(JsonObject user) { putOpt(json.getJsonObject("rp"), "icon", options.getRelyingParty().getIcon()); // put non null values for User - putOpt(json.getJsonObject("user"), "id", uUIDtoBase64Url(UUID.randomUUID())); + putOpt(json.getJsonObject("user"), "id", userId); putOpt(json.getJsonObject("user"), "name", user.getString("name")); putOpt(json.getJsonObject("user"), "displayName", user.getString("displayName")); putOpt(json.getJsonObject("user"), "icon", user.getString("icon")); + // put the public key credentials parameters for (PublicKeyCredential pubKeyCredParam : options.getPubKeyCredParams()) { addOpt( @@ -294,6 +306,7 @@ public void authenticate(Credentials credentials, Handler> han WebAuthnCredentials authInfo = (WebAuthnCredentials) credentials; // check authInfo.checkValid(null); + // The basic data supplied with any kind of validation is: // { // "rawId": "base64url", @@ -339,6 +352,7 @@ public void authenticate(Credentials credentials, Handler> han } // optional data + if (clientData.containsKey("tokenBinding")) { JsonObject tokenBinding = clientData.getJsonObject("tokenBinding"); if (tokenBinding == null) { @@ -358,6 +372,7 @@ public void authenticate(Credentials credentials, Handler> han } } + final String userId = authInfo.getUserId(); final String username = authInfo.getUsername(); // Step #4 @@ -379,6 +394,7 @@ public void authenticate(Credentials credentials, Handler> han final Authenticator authrInfo = verifyWebAuthNCreate(authInfo, clientDataJSON); // by default the store can upsert if a credential is missing, the user has been verified so it is valid // the store however might disallow this operation + authrInfo.setUserId(userId); authrInfo.setUserName(username); // the create challenge is complete we can finally safe this @@ -393,6 +409,7 @@ public void authenticate(Credentials credentials, Handler> han return; case "webauthn.get": Authenticator query = new Authenticator(); + if (options.getRequireResidentKey()) { // username are not provided (RK) we now need to lookup by id query.setCredID(webauthn.getString("id")); @@ -402,9 +419,14 @@ public void authenticate(Credentials credentials, Handler> han handler.handle(Future.failedFuture("username can't be null!")); return; } + query.setUserName(username); } + if (userId != null) { + query.setUserId(userId); + } + fetcher.apply(query) .onFailure(err -> handler.handle(Future.failedFuture(err))) .onSuccess(authenticators -> { diff --git a/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/DummyStore.java b/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/DummyStore.java index a2ed3e9ce..9bc725820 100644 --- a/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/DummyStore.java +++ b/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/DummyStore.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class DummyStore { @@ -20,17 +21,25 @@ public void clear() { } public Future> fetch(Authenticator query) { + if (query.getUserName() == null && query.getCredID() == null && query.getUserId() == null) { + return Future.failedFuture(new IllegalArgumentException("Bad authenticator query! All conditions were null")); + } + return Future.succeededFuture( database.stream() .filter(entry -> { + boolean matches = true; if (query.getUserName() != null) { - return query.getUserName().equals(entry.getUserName()); + matches = query.getUserName().equals(entry.getUserName()); } if (query.getCredID() != null) { - return query.getCredID().equals(entry.getCredID()); + matches = matches || query.getCredID().equals(entry.getCredID()); } - // This is a bad query! both username and credID are null - return false; + if (query.getUserId() != null) { + matches = matches || query.getUserId().equals(entry.getUserId()); + } + + return matches; }) .collect(Collectors.toList()) ); diff --git a/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/NavigatorCredentialsCreate.java b/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/NavigatorCredentialsCreate.java index 32e3d2b86..dca2fe955 100644 --- a/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/NavigatorCredentialsCreate.java +++ b/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/NavigatorCredentialsCreate.java @@ -1,6 +1,8 @@ package io.vertx.ext.auth.webauthn; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.impl.Codec; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.RunTestOnContext; @@ -10,7 +12,13 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.assertNotNull; +import javax.naming.AuthenticationException; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.*; @RunWith(VertxUnitRunner.class) public class NavigatorCredentialsCreate { @@ -36,10 +44,19 @@ public void testRequestRegister(TestContext should) { .authenticatorFetcher(database::fetch) .authenticatorUpdater(database::store); + final String userId = Codec.base64UrlEncode(UUID.randomUUID().toString().getBytes()); + + // Authenticator to test excludedCredentials + database.add( + new Authenticator() + .setUserId(userId) + .setType("public-key") + .setCredID("-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw") + ); + // Dummy user JsonObject user = new JsonObject() - // id is expected to be a base64url string - .put("id", "000000000000000000000000") + .put("id", userId) .put("name", "john.doe@email.com") .put("displayName", "John Doe") .put("icon", "https://pics.example.com/00/p/aBjjjpqPb.png"); @@ -56,7 +73,77 @@ public void testRequestRegister(TestContext should) { assertNotNull(challengeResponse.getJsonArray("pubKeyCredParams")); // ensure that challenge and user.id are base64url encoded assertNotNull(challengeResponse.getBinary("challenge")); - assertNotNull(challengeResponse.getJsonObject("user").getBinary("id")); + + final JsonObject challengeResponseUser = challengeResponse.getJsonObject("user"); + assertNotNull(challengeResponseUser); + assertEquals(userId, challengeResponseUser.getString("id")); + assertEquals(user.getString("name"), challengeResponseUser.getString("name")); + assertEquals(user.getString("displayName"), challengeResponseUser.getString("displayName")); + assertEquals(user.getString("icon"), challengeResponseUser.getString("icon")); + + final JsonArray excludeCredentials = challengeResponse.getJsonArray("excludeCredentials"); + assertEquals(1, excludeCredentials.size()); + + final JsonObject excludeCredential = excludeCredentials.getJsonObject(0); + assertEquals("public-key", excludeCredential.getString("type")); + assertEquals("-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw", excludeCredential.getString("id")); + assertEquals(new JsonArray(Arrays.asList("usb", "nfc", "ble", "internal")), excludeCredential.getJsonArray("transports")); + + test.complete(); + }); + } + + @Test + public void testRequestRegisterWithRawId(TestContext should) { + final Async test = should.async(); + + WebAuthn webAuthN = WebAuthn.create( + rule.vertx(), + new WebAuthnOptions().setRelyingParty(new RelyingParty().setName("ACME Corporation")) + .setAttestation(Attestation.of("direct"))) + .authenticatorFetcher(database::fetch) + .authenticatorUpdater(database::store); + + final String userId = Codec.base64UrlEncode(UUID.randomUUID().toString().getBytes()); + + // Dummy user + JsonObject user = new JsonObject() + .put("rawId", userId) + .put("displayName", "John Doe"); + + webAuthN + .createCredentialsOptions(user) + .onFailure(should::fail) + .onSuccess(challengeResponse -> { + final JsonObject challengeResponseUser = challengeResponse.getJsonObject("user"); + assertNotNull(challengeResponseUser); + assertEquals("rawId should have been used as-is", user.getString("rawId"), challengeResponseUser.getString("id")); + test.complete(); + }); + } + + @Test + public void testRequestRegisterWithNoId(TestContext should) { + final Async test = should.async(); + + WebAuthn webAuthN = WebAuthn.create( + rule.vertx(), + new WebAuthnOptions().setRelyingParty(new RelyingParty().setName("ACME Corporation")) + .setAttestation(Attestation.of("direct"))) + .authenticatorFetcher(database::fetch) + .authenticatorUpdater(database::store); + + // Dummy user + JsonObject user = new JsonObject() + .put("displayName", "John Doe"); + + webAuthN + .createCredentialsOptions(user) + .onFailure(should::fail) + .onSuccess(challengeResponse -> { + final JsonObject challengeResponseUser = challengeResponse.getJsonObject("user"); + assertNotNull(challengeResponseUser); + assertNotNull("random id should have been generated", challengeResponseUser.getBinary("id")); test.complete(); }); } diff --git a/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/WebAuthNTest.java b/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/WebAuthNTest.java index c606d6414..794a29540 100644 --- a/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/WebAuthNTest.java +++ b/vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/WebAuthNTest.java @@ -1,6 +1,8 @@ package io.vertx.ext.auth.webauthn; import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.impl.Codec; import io.vertx.ext.auth.impl.cose.CWK; import io.vertx.ext.auth.impl.jose.JWK; import io.vertx.ext.auth.impl.jose.JWS; @@ -18,7 +20,8 @@ import java.io.IOException; import java.security.cert.CertificateException; import java.util.Base64; -import java.util.Map; +import java.util.List; +import java.util.UUID; @RunWith(VertxUnitRunner.class) public class WebAuthNTest { @@ -43,27 +46,61 @@ public void testFIDORegister(TestContext should) { .authenticatorFetcher(database::fetch) .authenticatorUpdater(database::store); - database.add( - new Authenticator() - .setCredID("vp6cvoSgvTWSyFpnmdpm1dwiuREvsm-Kqw0Jt0Y0PQfjHsEhKE82KompUXqEt5yQIQl9ZKj6L1-700LGaVUMoQ") - .setPublicKey("pQECAyYgASFYINE091XO4J5juKbQMyeu9X2oZYFvAq6oIgp_3z1hXhG3Ilgg_cCyBgk8U8Zm3umoX2ELm6Is1k-2PLtwWmCkmul07cQ") - .setCounter(0) - ); - final JsonObject webauthn = new JsonObject("{\"getClientExtensionResults\":{},\"rawId\":\"vp6cvoSgvTWSyFpnmdpm1dwiuREvsm-Kqw0Jt0Y0PQfjHsEhKE82KompUXqEt5yQIQl9ZKj6L1-700LGaVUMoQ\",\"response\":{\"attestationObject\":\"o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAOOPecQ34VN0QW-cmj-Sft9aCahqgTlFQzbQH1LpEgrTAiBWW6KoqlKbLMtGd1Y_VcQML8eugYZcrmSSCS0of2T-M2N4NWOBWQIyMIICLjCCARigAwIBAgIECmML_zALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCkxJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDE3NDI2MzI5NTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKQjZF26iyPtbNnl5IuTKs_fRWTHVzHxz1IHRRBrSbqWD60PCqUJPe4zkIRFqBa4NnzdhVcS80nlZuY3ANQm0J-jJjAkMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4yMAsGCSqGSIb3DQEBCwOCAQEAZTmwMqHPxEjSB64Umwq2tGDKplAcEzrwmg6kgS8KPkJKXKSu9T1H6XBM9-LAE9cN48oUirFFmDIlTbZRXU2Vm2qO9OdrSVFY-qdbF9oti8CKAmPHuJZSW6ii7qNE59dHKUaP4lDYpnhRDqttWSUalh2LPDJQUpO9bsJPkgNZAhBUQMYZXL_MQZLRYkX-ld7llTNOX5u7n_4Y5EMr-lqOyVVC9lQ6JP6xoa9q6Zp9-Y9ZmLCecrrcuH6-pLDgAzPcc8qxhC2OR1B0ZSpI9RBgcT0KqnVE0tq1KEDeokPqF3MgmDRkJ--_a2pV0wAYfPC3tC57BtBdH_UXEB8xZVFhtGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQL6enL6EoL01kshaZ5naZtXcIrkRL7JviqsNCbdGND0H4x7BIShPNiqJqVF6hLeckCEJfWSo-i9fu9NCxmlVDKGlAQIDJiABIVgg0TT3Vc7gnmO4ptAzJ671fahlgW8CrqgiCn_fPWFeEbciWCD9wLIGCTxTxmbe6ahfYQuboizWT7Y8u3BaYKSa6XTtxA\",\"clientDataJSON\":\"eyJjaGFsbGVuZ2UiOiJQZXlodVVYaVQzeG55V1pqZWNaU1NxaFVTdUttYmZPV0dGREN0OGZDUXYwIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9\"},\"id\":\"vp6cvoSgvTWSyFpnmdpm1dwiuREvsm-Kqw0Jt0Y0PQfjHsEhKE82KompUXqEt5yQIQl9ZKj6L1-700LGaVUMoQ\",\"type\":\"public-unwrap\"}"); + final String userId = Codec.base64UrlEncode(UUID.randomUUID().toString().getBytes()); + final String username = "paulo"; + final String credID = "vp6cvoSgvTWSyFpnmdpm1dwiuREvsm-Kqw0Jt0Y0PQfjHsEhKE82KompUXqEt5yQIQl9ZKj6L1-700LGaVUMoQ"; + final String publicKey = "pQECAyYgASFYINE091XO4J5juKbQMyeu9X2oZYFvAq6oIgp_3z1hXhG3Ilgg_cCyBgk8U8Zm3umoX2ELm6Is1k-2PLtwWmCkmul07cQ"; + webAuthN.authenticate( new JsonObject() .put("webauthn", webauthn) .put("challenge", "PeyhuUXiT3xnyWZjecZSSqhUSuKmbfOWGFDCt8fCQv0") - .put("username", "paulo") + .put("username", username) + .put("userId", userId) .put("origin", "http://localhost:3000") , fn -> { should.assertTrue(fn.succeeded()); + + final User user = fn.result(); + should.assertEquals(userId, user.principal().getString("userId")); + + final Authenticator expectedAuthenticator = new Authenticator() + .setUserId(userId) + .setUserName(username) + .setCredID(credID) + .setPublicKey(publicKey); + + testAuthenticatorStored( + should, + new Authenticator().setCredID(credID), + expectedAuthenticator + ); + test.complete(); }); } + private void testAuthenticatorStored(final TestContext should, final Authenticator query, final Authenticator expected) { + final List results = database.fetch(query).result(); + should.assertEquals(1, results.size()); + + final Authenticator result = results.get(0); + if (expected.getUserId() != null) { + should.assertEquals(expected.getUserId(), result.getUserId()); + } + if (expected.getUserId() != null) { + should.assertEquals(expected.getUserName(), result.getUserName()); + } + if (expected.getCredID() != null) { + should.assertEquals(expected.getCredID(), result.getCredID()); + } + if (expected.getPublicKey() != null) { + should.assertEquals(expected.getPublicKey(), result.getPublicKey()); + } + } + @Test(timeout = 1000) public void testFIDOLogin(TestContext should) { final Async test = should.async(); @@ -73,11 +110,14 @@ public void testFIDOLogin(TestContext should) { .authenticatorFetcher(database::fetch) .authenticatorUpdater(database::store); + final String userId = Codec.base64UrlEncode(UUID.randomUUID().toString().getBytes()); + database.add( new Authenticator() .setCredID("-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw") .setPublicKey("pQECAyYgASFYIB4QBsdBFyVm79aQFrgdhAFsV0bD0-UfzsRRihvSU8bnIlggdBaaNC3nGWGcZd1msfoD0vMt0Ydg9InOFKkz6PKUEf8") .setCounter(0) + .setUserId(userId) ); final JsonObject webauthn = new JsonObject("{\"getClientExtensionResults\":{},\"rawId\":\"-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw\",\"response\":{\"authenticatorData\":\"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAFA\",\"signature\":\"MEUCIA3bv92hSE3wNz1CNGIinx27YLJgucNnBwqjV7qWqHqiAiEAjBsxBaK2nEfCilGSZ3yzoHVJilwkhOOkwZAJ52xp-h8\",\"userHandle\":\"null\",\"clientDataJSON\":\"eyJjaGFsbGVuZ2UiOiI2b2pkb19LS0c0a1hvWjVKRF9BbHY2Q2hyVXRPT3o3dXFlaWlvRmxCc3pvIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9\"},\"id\":\"-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw\",\"type\":\"public-unwrap\"}"); @@ -89,6 +129,32 @@ public void testFIDOLogin(TestContext should) { .put("challenge", "6ojdo_KKG4kXoZ5JD_Alv6ChrUtOOz7uqeiioFlBszo") , fn -> { should.assertTrue(fn.succeeded()); + + final User user = fn.result(); + should.assertEquals(userId, user.principal().getString("userId")); + + test.complete(); + }); + } + + @Test(timeout = 1000) + public void testFIDOLoginWhenNoAuthenticatorsFoundByCredID(TestContext should) { + final Async test = should.async(); + WebAuthn webAuthN = WebAuthn.create( + rule.vertx(), + new WebAuthnOptions().setRelyingParty(new RelyingParty().setName("FIDO Examples Corporation")).setRequireResidentKey(true)) + .authenticatorFetcher(database::fetch) + .authenticatorUpdater(database::store); + + final JsonObject webauthn = new JsonObject("{\"getClientExtensionResults\":{},\"rawId\":\"-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw\",\"response\":{\"authenticatorData\":\"SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAAFA\",\"signature\":\"MEUCIA3bv92hSE3wNz1CNGIinx27YLJgucNnBwqjV7qWqHqiAiEAjBsxBaK2nEfCilGSZ3yzoHVJilwkhOOkwZAJ52xp-h8\",\"userHandle\":\"null\",\"clientDataJSON\":\"eyJjaGFsbGVuZ2UiOiI2b2pkb19LS0c0a1hvWjVKRF9BbHY2Q2hyVXRPT3o3dXFlaWlvRmxCc3pvIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9\"},\"id\":\"-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw\",\"type\":\"public-unwrap\"}"); + + webAuthN.authenticate( + new JsonObject() + .put("webauthn", webauthn) + .put("origin", "http://localhost:3000") + .put("challenge", "6ojdo_KKG4kXoZ5JD_Alv6ChrUtOOz7uqeiioFlBszo") + , fn -> { + should.assertFalse(fn.succeeded()); test.complete(); }); }