Skip to content

Commit 7a8a277

Browse files
committed
WebAuthn: Add user id to PublicKeyCredentialsCreateOptions, Authenticator and WebAuthnCredentials (eclipse-vertx#580, eclipse-vertx#581)
1 parent c9da04a commit 7a8a277

File tree

9 files changed

+244
-19
lines changed

9 files changed

+244
-19
lines changed

vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/AuthenticatorConverter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json,
6060
obj.setUserName((String)member.getValue());
6161
}
6262
break;
63+
case "userId":
64+
if (member.getValue() instanceof String) {
65+
obj.setUserId((String)member.getValue());
66+
}
6367
}
6468
}
6569
}
@@ -91,5 +95,8 @@ public static void toJson(Authenticator obj, java.util.Map<String, Object> json)
9195
if (obj.getUserName() != null) {
9296
json.put("userName", obj.getUserName());
9397
}
98+
if (obj.getUserId() != null) {
99+
json.put("userId", obj.getUserId());
100+
}
94101
}
95102
}

vertx-auth-webauthn/src/main/generated/io/vertx/ext/auth/webauthn/WebAuthnCredentialsConverter.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json,
4545
obj.setWebauthn(((JsonObject)member.getValue()).copy());
4646
}
4747
break;
48+
case "userId":
49+
if (member.getValue() instanceof String) {
50+
obj.setUserId((String)member.getValue());
51+
}
52+
break;
4853
}
4954
}
5055
}
@@ -69,5 +74,8 @@ public static void toJson(WebAuthnCredentials obj, java.util.Map<String, Object>
6974
if (obj.getWebauthn() != null) {
7075
json.put("webauthn", obj.getWebauthn());
7176
}
77+
if (obj.getUserId() != null) {
78+
json.put("userId", obj.getUserId());
79+
}
7280
}
7381
}

vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/Authenticator.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public class Authenticator {
7373
private AttestationCertificates attestationCertificates;
7474
private String fmt;
7575

76+
/**
77+
* The base64 url encoded user handle associated with this authenticator.
78+
*/
79+
private String userId;
80+
7681
public Authenticator() {}
7782
public Authenticator(JsonObject json) {
7883
AuthenticatorConverter.fromJson(json, this);
@@ -168,4 +173,13 @@ public Authenticator setAaguid(String aaguid) {
168173
public String getAaguid() {
169174
return aaguid;
170175
}
176+
177+
public String getUserId() {
178+
return userId;
179+
}
180+
181+
public Authenticator setUserId(String userId) {
182+
this.userId = userId;
183+
return this;
184+
}
171185
}

vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthn.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ static WebAuthn create(Vertx vertx, WebAuthnOptions options) {
5959
* Gets a challenge and any other parameters for the {@code navigator.credentials.create()} call.
6060
*
6161
* The object being returned is described here <a href="https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions">https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions</a>
62+
*
63+
* The caller <strong>must</strong> extract the generated challenge and be able to re-produce it
64+
* later for the {@link #authenticate(JsonObject)} call.
65+
*
66+
* The user object <strong>must</strong> contain base64 url encoded <code>id</code> attribute representing the
67+
* user handle (see <a href="https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialuserentity-id">https://www.w3.org/TR/webauthn-2/#dom-publickeycredentialuserentity-id</a>)
68+
*
6269
* @param user - the user object with name and optionally displayName and icon
6370
* @param handler server encoded make credentials request
6471
* @return fluent self

vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/WebAuthnCredentials.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class WebAuthnCredentials implements Credentials {
2626
private String challenge;
2727
private JsonObject webauthn;
2828
private String username;
29+
private String userId;
2930
private String origin;
3031
private String domain;
3132

@@ -80,6 +81,15 @@ public WebAuthnCredentials setDomain(String domain) {
8081
return this;
8182
}
8283

84+
public String getUserId() {
85+
return userId;
86+
}
87+
88+
public WebAuthnCredentials setUserId(String userId) {
89+
this.userId = userId;
90+
return this;
91+
}
92+
8393
@Override
8494
public <V> void checkValid(V arg) throws CredentialValidationException {
8595
if (challenge == null || challenge.length() == 0) {

vertx-auth-webauthn/src/main/java/io/vertx/ext/auth/webauthn/impl/WebAuthnImpl.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,17 @@ public WebAuthn authenticatorUpdater(Function<Authenticator, Future<Void>> updat
159159

160160
@Override
161161
public Future<JsonObject> createCredentialsOptions(JsonObject user) {
162+
String userId;
163+
if (user.getString("id") != null) {
164+
userId = base64UrlEncode(user.getString("id").getBytes());
165+
} else if (user.getString("rawId") != null) {
166+
userId = user.getString("rawId");
167+
} else {
168+
userId = uUIDtoBase64Url(UUID.randomUUID());
169+
}
162170

163171
return fetcher
164-
.apply(new Authenticator().setUserName(user.getString("name")))
172+
.apply(new Authenticator().setUserId(userId).setUserName(user.getString("name")))
165173
.map(authenticators -> {
166174
// empty structure with all required fields
167175
JsonObject json = new JsonObject()
@@ -177,10 +185,11 @@ public Future<JsonObject> createCredentialsOptions(JsonObject user) {
177185
putOpt(json.getJsonObject("rp"), "icon", options.getRelyingParty().getIcon());
178186

179187
// put non null values for User
180-
putOpt(json.getJsonObject("user"), "id", uUIDtoBase64Url(UUID.randomUUID()));
188+
putOpt(json.getJsonObject("user"), "id", userId);
181189
putOpt(json.getJsonObject("user"), "name", user.getString("name"));
182190
putOpt(json.getJsonObject("user"), "displayName", user.getString("displayName"));
183191
putOpt(json.getJsonObject("user"), "icon", user.getString("icon"));
192+
184193
// put the public key credentials parameters
185194
for (PublicKeyCredential pubKeyCredParam : options.getPubKeyCredParams()) {
186195
addOpt(
@@ -294,6 +303,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> han
294303
WebAuthnCredentials authInfo = (WebAuthnCredentials) credentials;
295304
// check
296305
authInfo.checkValid(null);
306+
297307
// The basic data supplied with any kind of validation is:
298308
// {
299309
// "rawId": "base64url",
@@ -339,6 +349,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> han
339349
}
340350

341351
// optional data
352+
342353
if (clientData.containsKey("tokenBinding")) {
343354
JsonObject tokenBinding = clientData.getJsonObject("tokenBinding");
344355
if (tokenBinding == null) {
@@ -358,6 +369,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> han
358369
}
359370
}
360371

372+
final String userId = authInfo.getUserId();
361373
final String username = authInfo.getUsername();
362374

363375
// Step #4
@@ -379,6 +391,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> han
379391
final Authenticator authrInfo = verifyWebAuthNCreate(authInfo, clientDataJSON);
380392
// by default the store can upsert if a credential is missing, the user has been verified so it is valid
381393
// the store however might disallow this operation
394+
authrInfo.setUserId(userId);
382395
authrInfo.setUserName(username);
383396

384397
// the create challenge is complete we can finally safe this
@@ -393,6 +406,7 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> han
393406
return;
394407
case "webauthn.get":
395408
Authenticator query = new Authenticator();
409+
396410
if (options.getRequireResidentKey()) {
397411
// username are not provided (RK) we now need to lookup by id
398412
query.setCredID(webauthn.getString("id"));
@@ -402,9 +416,14 @@ public void authenticate(Credentials credentials, Handler<AsyncResult<User>> han
402416
handler.handle(Future.failedFuture("username can't be null!"));
403417
return;
404418
}
419+
405420
query.setUserName(username);
406421
}
407422

423+
if (userId != null) {
424+
query.setUserId(userId);
425+
}
426+
408427
fetcher.apply(query)
409428
.onFailure(err -> handler.handle(Future.failedFuture(err)))
410429
.onSuccess(authenticators -> {

vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/DummyStore.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.util.ArrayList;
66
import java.util.List;
7+
import java.util.Objects;
78
import java.util.stream.Collectors;
89

910
public class DummyStore {
@@ -20,17 +21,25 @@ public void clear() {
2021
}
2122

2223
public Future<List<Authenticator>> fetch(Authenticator query) {
24+
if (query.getUserName() == null && query.getCredID() == null && query.getUserId() == null) {
25+
throw new RuntimeException("Bad authenticator query! All conditions were null");
26+
}
27+
2328
return Future.succeededFuture(
2429
database.stream()
2530
.filter(entry -> {
31+
boolean matches = true;
2632
if (query.getUserName() != null) {
27-
return query.getUserName().equals(entry.getUserName());
33+
matches = query.getUserName().equals(entry.getUserName());
2834
}
2935
if (query.getCredID() != null) {
30-
return query.getCredID().equals(entry.getCredID());
36+
matches = matches || query.getCredID().equals(entry.getCredID());
3137
}
32-
// This is a bad query! both username and credID are null
33-
return false;
38+
if (query.getUserId() != null) {
39+
matches = matches || query.getUserId().equals(entry.getUserId());
40+
}
41+
42+
return matches;
3443
})
3544
.collect(Collectors.toList())
3645
);

vertx-auth-webauthn/src/test/java/io/vertx/ext/auth/webauthn/NavigatorCredentialsCreate.java

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package io.vertx.ext.auth.webauthn;
22

3+
import io.vertx.core.json.JsonArray;
34
import io.vertx.core.json.JsonObject;
5+
import io.vertx.ext.auth.impl.Codec;
46
import io.vertx.ext.unit.Async;
57
import io.vertx.ext.unit.TestContext;
68
import io.vertx.ext.unit.junit.RunTestOnContext;
@@ -10,7 +12,13 @@
1012
import org.junit.Test;
1113
import org.junit.runner.RunWith;
1214

13-
import static org.junit.Assert.assertNotNull;
15+
import javax.naming.AuthenticationException;
16+
17+
import java.util.Arrays;
18+
import java.util.List;
19+
import java.util.UUID;
20+
21+
import static org.junit.Assert.*;
1422

1523
@RunWith(VertxUnitRunner.class)
1624
public class NavigatorCredentialsCreate {
@@ -36,10 +44,19 @@ public void testRequestRegister(TestContext should) {
3644
.authenticatorFetcher(database::fetch)
3745
.authenticatorUpdater(database::store);
3846

47+
final String userId = UUID.randomUUID().toString();
48+
49+
// Authenticator to test excludedCredentials
50+
database.add(
51+
new Authenticator()
52+
.setUserId(Codec.base64UrlEncode(userId.getBytes()))
53+
.setType("public-key")
54+
.setCredID("-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw")
55+
);
56+
3957
// Dummy user
4058
JsonObject user = new JsonObject()
41-
// id is expected to be a base64url string
42-
.put("id", "000000000000000000000000")
59+
.put("id", userId)
4360
.put("name", "[email protected]")
4461
.put("displayName", "John Doe")
4562
.put("icon", "https://pics.example.com/00/p/aBjjjpqPb.png");
@@ -56,7 +73,75 @@ public void testRequestRegister(TestContext should) {
5673
assertNotNull(challengeResponse.getJsonArray("pubKeyCredParams"));
5774
// ensure that challenge and user.id are base64url encoded
5875
assertNotNull(challengeResponse.getBinary("challenge"));
59-
assertNotNull(challengeResponse.getJsonObject("user").getBinary("id"));
76+
77+
final JsonObject challengeResponseUser = challengeResponse.getJsonObject("user");
78+
assertNotNull(challengeResponseUser);
79+
assertArrayEquals(user.getString("id").getBytes(), Codec.base64UrlDecode(challengeResponseUser.getString("id")));
80+
assertEquals(user.getString("name"), challengeResponseUser.getString("name"));
81+
assertEquals(user.getString("displayName"), challengeResponseUser.getString("displayName"));
82+
assertEquals(user.getString("icon"), challengeResponseUser.getString("icon"));
83+
84+
final JsonArray excludeCredentials = challengeResponse.getJsonArray("excludeCredentials");
85+
assertEquals(1, excludeCredentials.size());
86+
87+
final JsonObject excludeCredential = excludeCredentials.getJsonObject(0);
88+
assertEquals("public-key", excludeCredential.getString("type"));
89+
assertEquals("-r1iW_eHUyIpU93f77odIrdUlNVfYzN-JPCTWGtdn-1wxdLxhlS9NmzLNbYsQ7XVZlGSWbh_63E5oFHcNh4JNw", excludeCredential.getString("id"));
90+
assertEquals(new JsonArray(Arrays.asList("usb", "nfc", "ble", "internal")), excludeCredential.getJsonArray("transports"));
91+
92+
test.complete();
93+
});
94+
}
95+
96+
@Test
97+
public void testRequestRegisterWithRawId(TestContext should) {
98+
final Async test = should.async();
99+
100+
WebAuthn webAuthN = WebAuthn.create(
101+
rule.vertx(),
102+
new WebAuthnOptions().setRelyingParty(new RelyingParty().setName("ACME Corporation"))
103+
.setAttestation(Attestation.of("direct")))
104+
.authenticatorFetcher(database::fetch)
105+
.authenticatorUpdater(database::store);
106+
107+
// Dummy user
108+
JsonObject user = new JsonObject()
109+
.put("rawId", "000000000000000000000000")
110+
.put("displayName", "John Doe");
111+
112+
webAuthN
113+
.createCredentialsOptions(user)
114+
.onFailure(should::fail)
115+
.onSuccess(challengeResponse -> {
116+
final JsonObject challengeResponseUser = challengeResponse.getJsonObject("user");
117+
assertNotNull(challengeResponseUser);
118+
assertEquals("rawId should have been used as-is", user.getString("rawId"), challengeResponseUser.getString("id"));
119+
test.complete();
120+
});
121+
}
122+
123+
@Test
124+
public void testRequestRegisterWithNoId(TestContext should) {
125+
final Async test = should.async();
126+
127+
WebAuthn webAuthN = WebAuthn.create(
128+
rule.vertx(),
129+
new WebAuthnOptions().setRelyingParty(new RelyingParty().setName("ACME Corporation"))
130+
.setAttestation(Attestation.of("direct")))
131+
.authenticatorFetcher(database::fetch)
132+
.authenticatorUpdater(database::store);
133+
134+
// Dummy user
135+
JsonObject user = new JsonObject()
136+
.put("displayName", "John Doe");
137+
138+
webAuthN
139+
.createCredentialsOptions(user)
140+
.onFailure(should::fail)
141+
.onSuccess(challengeResponse -> {
142+
final JsonObject challengeResponseUser = challengeResponse.getJsonObject("user");
143+
assertNotNull(challengeResponseUser);
144+
assertNotNull("random id should have been generated", challengeResponseUser.getBinary("id"));
60145
test.complete();
61146
});
62147
}

0 commit comments

Comments
 (0)