Skip to content

Commit 2c2cde0

Browse files
authoredMay 9, 2022
Merge pull request #19 from YubicoLabs/wip/v2.1.0
Wip/v2.1.0
2 parents 509656d + a482b74 commit 2c2cde0

File tree

20 files changed

+1114
-818
lines changed

20 files changed

+1114
-818
lines changed
 

‎NEWS

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
== Version 2.0.0 RC ==
1+
== Version 2.1.0 ==
2+
3+
- Integration with FIDO MDS
4+
- Automatic nicknames given to authenticators through MDS
5+
- New Edit modal for Trusted Devices
6+
- Various bug fixes for internationalization, Android resident key settings, and Safari user handle default values
7+
8+
== Version 2.0.0 ==
29

310
- Updated look and feel of UI
411
- Attestation data now displayed to the user (if they are using a YubiKey)

‎backend/lambda-functions/CreateAuth/CreateAuthChallengeFIDO2.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ async function getCreateCredentialsOptions(event, creds) {
131131

132132
const coseLookup = {"ES256": -7, "EdDSA": -8, "RS256": -257};
133133

134-
startRegisterPayload.requestId = startRegisterPayload.requestId.base64;
135-
startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64;
136-
startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64;
134+
startRegisterPayload.requestId = startRegisterPayload.requestId.base64url;
135+
startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64url;
136+
startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64url;
137137
startRegisterPayload.publicKeyCredentialCreationOptions.attestation = startRegisterPayload.publicKeyCredentialCreationOptions.attestation.toLowerCase();
138138
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification = startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification.toLowerCase();
139139
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.authenticatorAttachment = authSelectorResolve[startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.authenticatorAttachment];
@@ -179,14 +179,14 @@ async function getCredentialsOptions(username) {
179179
let startAuthPayload = JSON.parse(JSON.parse(response.Payload));
180180
console.log("startAuthPayload: ", startAuthPayload);
181181

182-
startAuthPayload.requestId = startAuthPayload.requestId.base64;
182+
startAuthPayload.requestId = startAuthPayload.requestId.base64url;
183183
console.log("requestId: ", startAuthPayload.requestId);
184184
startAuthPayload.publicKeyCredentialRequestOptions.userVerification = startAuthPayload.publicKeyCredentialRequestOptions.userVerification.toLowerCase();
185-
startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64;
185+
startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64url;
186186
console.log("challenge: ", startAuthPayload.publicKeyCredentialRequestOptions.challenge);
187187
startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials = startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials.map( (cred) => {
188188
cred.type = cred.type.toLowerCase().replace('_','-');
189-
cred.id = cred.id.base64;
189+
cred.id = cred.id.base64url;
190190
return cred
191191
});
192192
console.log("response payload: ", startAuthPayload);

‎backend/lambda-functions/FIDO2KitAPI/FIDO2KitAPI.js

+9-17
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ async function updateFIDO2CredentialNickname(username, body) {
180180
const payload = JSON.stringify({
181181
"type": "updateCredentialNickname",
182182
"username": username,
183-
"credentialId": data.credential.credentialId.base64,
183+
"credentialId": data.credential.credentialId.base64url,
184184
"nickname": data.credentialNickname.value,
185185
});
186186
console.log("updateCredentialNickname request payload: "+payload);
@@ -264,15 +264,15 @@ async function startUsernamelessAuthentication() {
264264
let startAuthPayload = JSON.parse(JSON.parse(response.Payload));
265265
console.log("startAuthPayload: ", startAuthPayload);
266266

267-
startAuthPayload.requestId = startAuthPayload.requestId.base64;
267+
startAuthPayload.requestId = startAuthPayload.requestId.base64url;
268268
console.log("requestId: ", startAuthPayload.requestId);
269269
startAuthPayload.publicKeyCredentialRequestOptions.userVerification = startAuthPayload.publicKeyCredentialRequestOptions.userVerification.toLowerCase();
270-
startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64;
270+
startAuthPayload.publicKeyCredentialRequestOptions.challenge = startAuthPayload.publicKeyCredentialRequestOptions.challenge.base64url;
271271
console.log("challenge: ", startAuthPayload.publicKeyCredentialRequestOptions.challenge);
272272
if(startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials){
273273
startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials = startAuthPayload.publicKeyCredentialRequestOptions.allowCredentials.map( (cred) => {
274274
cred.type = cred.type.toLowerCase().replace('_','-');
275-
cred.id = cred.id.base64;
275+
cred.id = cred.id.url;
276276
return cred
277277
});
278278
}
@@ -289,18 +289,11 @@ async function startUsernamelessAuthentication() {
289289
async function startRegisterFIDO2Credential(profile, body, uid) {
290290
console.log("startRegisterFIDO2Credential userId: "+profile.id+" body:",body);
291291
const jsonBody = JSON.parse(body);
292-
293-
let invalidResult = validate({nickname: jsonBody.nickname}, constraints);
294-
console.log("nickname invalidResult: ", invalidResult);
295-
if(invalidResult && invalidResult.nickname) {
296-
return error(invalidResult.nickname.join(". "));
297-
}
298292

299293
const payload = JSON.stringify({
300294
"type": "startRegistration",
301295
"username": profile.username,
302296
"displayName": profile.username,
303-
"credentialNickname": jsonBody.nickname,
304297
"requireResidentKey": jsonBody.requireResidentKey,
305298
"requireAuthenticatorAttachment": jsonBody.requireAuthenticatorAttachment,
306299
"uid": uid
@@ -322,14 +315,13 @@ async function startRegisterFIDO2Credential(profile, body, uid) {
322315

323316
const coseLookup = {"ES256": -7, "EdDSA": -8, "RS256": -257};
324317

325-
startRegisterPayload.requestId = startRegisterPayload.requestId.base64;
326-
startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64;
327-
startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64;
318+
startRegisterPayload.requestId = startRegisterPayload.requestId.base64url;
319+
startRegisterPayload.publicKeyCredentialCreationOptions.user.id = startRegisterPayload.publicKeyCredentialCreationOptions.user.id.base64url;
320+
startRegisterPayload.publicKeyCredentialCreationOptions.challenge = startRegisterPayload.publicKeyCredentialCreationOptions.challenge.base64url;
328321
startRegisterPayload.publicKeyCredentialCreationOptions.attestation = startRegisterPayload.publicKeyCredentialCreationOptions.attestation.toLowerCase();
329322
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification = startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.userVerification.toLowerCase();
330323
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.residentKey = startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.residentKey.toLowerCase();
331-
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.requireResidentKey = false;
332-
if(startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.residentKey === "required") {
324+
if(startRegisterPayload.requireResidentKey) {
333325
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.requireResidentKey = true;
334326
}
335327
startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.authenticatorAttachment = authSelectorResolve[startRegisterPayload.publicKeyCredentialCreationOptions.authenticatorSelection.authenticatorAttachment];
@@ -341,7 +333,7 @@ async function startRegisterFIDO2Credential(profile, body, uid) {
341333
});
342334
startRegisterPayload.publicKeyCredentialCreationOptions.excludeCredentials = startRegisterPayload.publicKeyCredentialCreationOptions.excludeCredentials.map( (cred) => {
343335
cred.type = cred.type.toLowerCase().replace('_','-');
344-
cred.id = cred.id.base64;
336+
cred.id = cred.id.base64url;
345337
console.log("cred: "+ JSON.stringify(cred));
346338
return cred;
347339
});

‎backend/lambda-functions/JavaWebAuthnLib/pom.xml

+45-2
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,30 @@
6464
<version>2.13.1</version>
6565
</dependency>
6666

67+
<dependency>
68+
<groupId>com.fasterxml.jackson.core</groupId>
69+
<artifactId>jackson-core</artifactId>
70+
<version>2.13.2</version>
71+
</dependency>
72+
73+
<dependency>
74+
<groupId>com.fasterxml.jackson.core</groupId>
75+
<artifactId>jackson-annotations</artifactId>
76+
<version>2.13.2</version>
77+
</dependency>
78+
79+
<dependency>
80+
<groupId>com.fasterxml.jackson.datatype</groupId>
81+
<artifactId>jackson-datatype-jdk8</artifactId>
82+
<version>2.13.2</version>
83+
</dependency>
84+
85+
<dependency>
86+
<groupId>com.fasterxml.jackson.datatype</groupId>
87+
<artifactId>jackson-datatype-jsr310</artifactId>
88+
<version>2.13.2</version>
89+
</dependency>
90+
6791
<dependency>
6892
<groupId>software.amazon.awssdk</groupId>
6993
<artifactId>url-connection-client</artifactId>
@@ -124,13 +148,32 @@
124148
<dependency>
125149
<groupId>com.yubico</groupId>
126150
<artifactId>webauthn-server-core</artifactId>
127-
<version>1.12.1</version>
151+
<version>2.0.0</version>
128152
</dependency>
129153

130154
<dependency>
131155
<groupId>com.yubico</groupId>
132156
<artifactId>webauthn-server-attestation</artifactId>
133-
<version>1.12.1</version>
157+
<version>2.0.0</version>
158+
</dependency>
159+
160+
<dependency>
161+
<groupId>com.yubico</groupId>
162+
<artifactId>yubico-util</artifactId>
163+
<version>2.0.0</version>
164+
</dependency>
165+
166+
167+
<dependency>
168+
<groupId>com.upokecenter</groupId>
169+
<artifactId>cbor</artifactId>
170+
<version>4.5.2</version>
171+
</dependency>
172+
173+
<dependency>
174+
<groupId>com.augustcellars.cose</groupId>
175+
<artifactId>cose-java</artifactId>
176+
<version>1.1.0</version>
134177
</dependency>
135178

136179
<!-- Test Dependencies -->

‎backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubico/webauthn/attestation/resolver/SimpleTrustResolverWithEquality.java

-71
This file was deleted.

‎backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/App.java

+144-89
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.yubicolabs.data;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import java.util.Set;
5+
6+
import com.yubico.fido.metadata.AttachmentHint;
7+
import com.yubico.webauthn.data.AuthenticatorTransport;
8+
9+
import lombok.Builder;
10+
import lombok.Value;
11+
import lombok.With;
12+
13+
@Value
14+
@Builder
15+
@With
16+
public class AttestationRegistration {
17+
@JsonInclude(JsonInclude.Include.NON_NULL)
18+
public String aaguid;
19+
@JsonInclude(JsonInclude.Include.NON_NULL)
20+
public String aaid;
21+
@JsonInclude(JsonInclude.Include.NON_NULL)
22+
public Set<AttachmentHint> attachmentHint;
23+
@JsonInclude(JsonInclude.Include.NON_NULL)
24+
public String icon;
25+
@JsonInclude(JsonInclude.Include.NON_NULL)
26+
public String description;
27+
@JsonInclude(JsonInclude.Include.NON_NULL)
28+
public Set<AuthenticatorTransport> authenticatorTransport;
29+
}

‎backend/lambda-functions/JavaWebAuthnLib/src/main/java/com/yubicolabs/data/CredentialRegistration.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.fasterxml.jackson.annotation.JsonIgnore;
44
import com.fasterxml.jackson.annotation.JsonProperty;
55
import com.yubico.webauthn.RegisteredCredential;
6-
import com.yubico.webauthn.attestation.Attestation;
76
import com.yubico.webauthn.data.UserIdentity;
87
import java.time.Instant;
98
import java.util.Optional;
@@ -13,7 +12,7 @@
1312

1413
@Value
1514
@Builder
16-
@With
15+
@With
1716
public class CredentialRegistration {
1817

1918
long signatureCount;
@@ -33,7 +32,7 @@ public class CredentialRegistration {
3332

3433
RegisteredCredential credential;
3534

36-
Optional<Attestation> attestationMetadata;
35+
Optional<AttestationRegistration> attestationMetadata;
3736

3837
RegistrationRequest registrationRequest;
3938

@@ -57,4 +56,3 @@ public String getUsername() {
5756
}
5857

5958
}
60-

‎backend/template.yaml

+479-434
Large diffs are not rendered by default.

‎clients/web/react/public/i18n/en-US.json

+21-4
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,20 @@
4242
"add-form-label": "Nickname",
4343
"date-last-used": "Date last used:",
4444
"delete-button": "Delete",
45-
"trusted-devices": "Trusted Devices"
45+
"trusted-devices": "Trusted Devices",
46+
"edit-button": "Edit",
47+
"edit-header": "Edit your Trusted Device",
48+
"edit-form-label": "Nickname",
49+
"edit-usernameless": "Usernameless a.k.a. Client-Side Discoverable Credential:",
50+
"edit-cancel-button": "Cancel",
51+
"edit-delete-button": "Delete",
52+
"edit-save-button": "Save changes",
53+
"att-device-name": "Device name:",
54+
"att-device-info": "Device info",
55+
"att-device-interfaces": "Available interfaces:",
56+
"att-aaguid": "Device AAGUID:",
57+
"att-aaid": "Device AAID:",
58+
"yubico-att-label": "Device Information:"
4659
},
4760
"logout": "Thank you for joining us!",
4861
"registration": {
@@ -54,6 +67,7 @@
5467
"add-key-2": "Follow the steps in the browser",
5568
"add-key-3": "Give your Security Key a nickname to easily identify it later",
5669
"primary-button": "Continue",
70+
"secondary-button": "Continue without editing credential nickname",
5771
"primary-button-loading": "Creating your account",
5872
"success-header": "Security Key added",
5973
"success-prompt": "You have successfully registered your Security Key",
@@ -88,10 +102,12 @@
88102
"last-time-used": "Last used time:",
89103
"last-update-time": "Last updates time:",
90104
"registration-time": "Registration time:",
91-
"yubico-att-label": "Yubico Device Information:",
105+
"yubico-att-label": "Device Information:",
92106
"att-device-name": "Device name:",
93107
"att-device-info": "Device info",
94108
"att-device-interfaces": "Available interfaces:",
109+
"att-aaguid": "Device AAGUID:",
110+
"att-aaid": "Device AAID:",
95111
"edit-cancel-button": "Cancel",
96112
"edit-delete-button": "Delete",
97113
"edit-save-button": "Save changes"
@@ -133,7 +149,8 @@
133149
"generate-codes": "Generate new recovery codes",
134150
"generate-codes-text": "When you generate new recovery codes, you must copy them to a safe spot. Your old codes will not work anymore.",
135151
"generate-codes-button": "Generate",
136-
"close-button": "Close"
152+
"close-button": "Not now, ask me again later",
153+
"ignore": "Ignore, and don't ask again"
137154
},
138155
"sv-pin": {
139156
"enter-pin": "Enter U2F Password",
@@ -184,7 +201,7 @@
184201
"If prompted, select Use Face ID",
185202
"Allow your device to scan your face"
186203
],
187-
"HELLO": [
204+
"WINDOWS_HELLO": [
188205
"In the prompt, select Built in Authenticator",
189206
"Scan your Fingerprint or enter your PIN"
190207
]

‎clients/web/react/src/RegisterPage/RegisterKeySuccessStep.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const RegisterKeySuccessStep = function ({ setForm, formData, navigation }) {
6464
const credentialToUpdate = {
6565
credential: {
6666
credentialId: {
67-
base64: ls_credential.id,
67+
base64url: ls_credential.id,
6868
},
6969
},
7070
credentialNickname: {
@@ -91,6 +91,10 @@ const RegisterKeySuccessStep = function ({ setForm, formData, navigation }) {
9191
dispatch(credentialActions.validateCredentialNickname(nickname));
9292
}
9393

94+
function skipStep() {
95+
history.push("/");
96+
}
97+
9498
return (
9599
<>
96100
<div className={styles.default["textCenter"]}>
@@ -125,6 +129,9 @@ const RegisterKeySuccessStep = function ({ setForm, formData, navigation }) {
125129
variant="primary btn-block mt-3">
126130
{t("registration.primary-button")}{" "}
127131
</Button>
132+
<Button onClick={() => skipStep()} variant="secondary btn-block mt-3">
133+
{t("registration.secondary-button")}{" "}
134+
</Button>
128135
</div>
129136
</div>
130137
</>

‎clients/web/react/src/_components/Credential/AddCredential.tsx

+39-56
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import AddCredentialGuidance from "./AddCredentialGuidance";
1111
// eslint-disable-next-line camelcase
1212
import aws_exports from "../../aws-exports";
1313
import { WebAuthnClient } from "..";
14+
import DetectBrowser from "../../_helpers/DetectBrowser";
1415

1516
// eslint-disable-next-line camelcase
1617
axios.defaults.baseURL = aws_exports.apiEndpoint;
@@ -69,26 +70,19 @@ const AddCredential = function () {
6970
*/
7071
const handleSaveAdd = async () => {
7172
setSubmitted(true);
72-
73-
const result = validate({ keyName: nickname }, constraints);
74-
if (result) {
75-
setInvalidNickname(result.keyName.join(". "));
76-
} else {
77-
setInvalidNickname(undefined);
78-
setLoading(true);
79-
try {
80-
await register();
81-
} catch (error) {
82-
console.error(
83-
t("console.error", {
84-
COMPONENT: "AddCredential",
85-
METHOD: "handleSaveAdd()",
86-
REASON: t("console.reason.addCredential0"),
87-
}),
88-
error
89-
);
90-
setLoading(false);
91-
}
73+
setLoading(true);
74+
try {
75+
await register();
76+
} catch (error) {
77+
console.error(
78+
t("console.error", {
79+
COMPONENT: "AddCredential",
80+
METHOD: "handleSaveAdd()",
81+
REASON: t("console.reason.addCredential0"),
82+
}),
83+
error
84+
);
85+
setLoading(false);
9286
}
9387
};
9488

@@ -133,6 +127,16 @@ const AddCredential = function () {
133127
return pinResult.value;
134128
}
135129

130+
/**
131+
* Android will not allow for a ResidentKey to be created - and will return an error during the WebAuthn ceremony if RequireResidentKey is True and ResidentKey is set to required
132+
* This method will hide the checkbox to create a resident key from the UI on android device
133+
* @returns false to hide if on android, true if otherwise
134+
*/
135+
function handleAndroidResidentKey() {
136+
if (DetectBrowser.getPlatform().id === "ANDROID_BIOMETRICS") return false;
137+
return true;
138+
}
139+
136140
/**
137141
* Primary logic of this method
138142
* Calls to the register API, and creates the credential on the security key
@@ -147,7 +151,6 @@ const AddCredential = function () {
147151
* More information can be found here: https://www.w3.org/TR/webauthn-2/#enum-attachment
148152
*/
149153
await WebAuthnClient.registerNewCredential(
150-
nickname,
151154
isResidentKey,
152155
"CROSS_PLATFORM",
153156
registerUV
@@ -189,41 +192,21 @@ const AddCredential = function () {
189192
)}
190193
<AddCredentialGuidance />
191194

192-
<label>{t("credential.add-form-label")}</label>
193-
<input
194-
type="text"
195-
name="nickname"
196-
autoFocus
197-
value={nickname}
198-
ref={inputRef}
199-
onChange={handleChange}
200-
className={`form-control${
201-
submitted && invalidNickname ? " is-invalid" : ""
202-
}`}
203-
onKeyPress={(ev) => {
204-
if (ev.key === "Enter") {
205-
handleSaveAdd();
206-
ev.preventDefault();
207-
}
208-
}}
209-
/>
210-
{invalidNickname ? (
211-
<Alert variant="danger">{invalidNickname}</Alert>
212-
) : null}
213-
<br />
214-
<label>
215-
<input
216-
name="isResidentKey"
217-
type="checkbox"
218-
checked={isResidentKey}
219-
onChange={handleCheckboxChange}
220-
/>{" "}
221-
{t("credential.usernameless-label")}
222-
<br />
223-
<em>
224-
<small>{t("credential.usernameless-note")}</small>
225-
</em>
226-
</label>
195+
{handleAndroidResidentKey() && (
196+
<label>
197+
<input
198+
name="isResidentKey"
199+
type="checkbox"
200+
checked={isResidentKey}
201+
onChange={handleCheckboxChange}
202+
/>{" "}
203+
{t("credential.usernameless-label")}
204+
<br />
205+
<em>
206+
<small>{t("credential.usernameless-note")}</small>
207+
</em>
208+
</label>
209+
)}
227210
</Modal.Body>
228211
<Modal.Footer>
229212
<Button variant="secondary" onClick={handleClose}>

‎clients/web/react/src/_components/Credential/Credential.tsx

+6-10
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ const checkAttestation = (credential) => {
2424
* @returns
2525
*/
2626
const getAttestationImage = (credential) => {
27-
const imgUrl =
28-
credential.attestationMetadata?.value?.deviceProperties?.imageUrl;
27+
const imgUrl = credential.attestationMetadata?.value?.icon;
2928
if (imgUrl) return imgUrl;
3029
return "https://www.yubico.com/wp-content/uploads//2021/02/illus-shield-lock-r1-dkteal.svg";
3130
};
@@ -52,14 +51,11 @@ const Credential = function ({ credential }) {
5251
</div>
5352
<div className="p-2 flex-grow-1">
5453
<h5>{credential.credentialNickname.value}</h5>
55-
{checkAttestation(credential) && (
56-
<h6>
57-
{
58-
credential.attestationMetadata.value.deviceProperties
59-
.displayName
60-
}
61-
</h6>
62-
)}
54+
{credential?.attestationMetadata?.value?.description &&
55+
credential.attestationMetadata.value.description !==
56+
credential.credentialNickname.value && (
57+
<h6>{credential.attestationMetadata.value.description}</h6>
58+
)}
6359
<p>
6460
{t("credential.date-last-used")}:{" "}
6561
{new Date(credential.lastUsedTime.seconds * 1000).toLocaleString()}

‎clients/web/react/src/_components/Credential/EditCredential.tsx

+34-48
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const EditCredential = function ({ credential }) {
4040
const handleDelete = () => {
4141
setShow(false);
4242
dispatch(
43-
credentialActions.delete(credential.credential.credentialId.base64)
43+
credentialActions.delete(credential.credential.credentialId.base64url)
4444
);
4545
};
4646
const constraints = {
@@ -86,18 +86,6 @@ const EditCredential = function ({ credential }) {
8686
};
8787
const inputRef = useRef(null);
8888

89-
/**
90-
* Checks if attestation data exists on the credential, used to determine if additional data should be displayed
91-
* @param credential credential that was passed into this component by parent
92-
* @returns
93-
*/
94-
const checkAttestation = (credential) => {
95-
const credAtt =
96-
credential.attestationMetadata?.value?.deviceProperties?.displayName;
97-
if (credAtt) return true;
98-
return false;
99-
};
100-
10189
return (
10290
<>
10391
<Button variant="secondary" onClick={handleShow}>
@@ -134,6 +122,39 @@ const EditCredential = function ({ credential }) {
134122
) : null}
135123
&nbsp;&nbsp;
136124
<br />
125+
<h4>{t("credential.yubico-att-label")}</h4>
126+
{credential?.attestationMetadata?.value?.description && (
127+
<p>
128+
<em>{t("credential.att-device-name")}</em>{" "}
129+
{credential.attestationMetadata.value.description}
130+
</p>
131+
)}
132+
{credential?.attestationMetadata?.value?.aaguid && (
133+
<p>
134+
<em>{t("credential.att-aaguid")}</em>{" "}
135+
{credential.attestationMetadata.value.aaguid}
136+
</p>
137+
)}
138+
{credential?.attestationMetadata?.value?.aaid && (
139+
<p>
140+
<em>{t("credential.att-aaid")}</em>{" "}
141+
{credential.attestationMetadata.value.aaid}
142+
</p>
143+
)}
144+
{credential?.attestationMetadata?.value?.authenticatorTransport &&
145+
credential?.attestationMetadata?.value?.authenticatorTransport
146+
.length > 0 && (
147+
<p>
148+
<em>{t("credential.att-device-interfaces")}</em>{" "}
149+
<ul>
150+
{credential.attestationMetadata.value.authenticatorTransport.map(
151+
(transport, index) => (
152+
<li key={index}>{transport.id}</li>
153+
)
154+
)}
155+
</ul>
156+
</p>
157+
)}
137158
<label>
138159
<em>{t("credential.edit-usernameless")}</em>{" "}
139160
{credential.registrationRequest
@@ -168,41 +189,6 @@ const EditCredential = function ({ credential }) {
168189
: ""}
169190
</label>
170191
<br />
171-
<br />
172-
{checkAttestation(credential) && (
173-
<>
174-
<h4 style={{ color: "#9aca3c" }}>
175-
{t("credential.yubico-att-label")}
176-
</h4>
177-
<p>
178-
<em>{t("credential.att-device-name")}</em>{" "}
179-
{
180-
credential.attestationMetadata.value.deviceProperties
181-
.displayName
182-
}{" "}
183-
-{" "}
184-
<a
185-
href={
186-
credential.attestationMetadata.value.deviceProperties
187-
.deviceUrl
188-
}
189-
target="_blank"
190-
rel="noreferrer">
191-
{t("credential.att-device-info")}
192-
</a>
193-
</p>
194-
<div>
195-
<em>{t("credential.att-device-interfaces")}</em>{" "}
196-
<ul>
197-
{credential.attestationMetadata.value.transports.map(
198-
(transport, index) => (
199-
<li key={index}>{transport}</li>
200-
)
201-
)}
202-
</ul>
203-
</div>
204-
</>
205-
)}
206192
</Modal.Body>
207193
<Modal.Footer>
208194
<Button variant="secondary" onClick={handleClose}>

‎clients/web/react/src/_components/RecoveryCodes/RecoveryCodes.tsx

+20-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ const RecoveryCodes = function ({ credentials }) {
2424
// Indicates if all codes have been consumed by login
2525
const { allRecoveryCodesUsed } = credentials;
2626

27+
const [ignoreModal, setIgnoreMoral] = useState(
28+
localStorage.getItem("recoveryCodesModal") === "true"
29+
);
30+
2731
const recoveryCodes = useSelector(
2832
(state: RootStateOrAny) => state.recoveryCodes
2933
);
@@ -54,16 +58,27 @@ const RecoveryCodes = function ({ credentials }) {
5458
dispatch(credentialActions.generateRecoveryCodes());
5559
};
5660

61+
const handleIgnore = () => {
62+
localStorage.setItem("recoveryCodesModal", "true");
63+
setShowCodes(false);
64+
};
65+
5766
/**
5867
* The modal will continue to appear on the parent component if the user has not generated new credentials
5968
* OR if all the codes have been consumed
6069
*/
6170
useEffect(() => {
62-
if (!recoveryCodesViewed || allRecoveryCodesUsed) {
71+
if ((!recoveryCodesViewed || allRecoveryCodesUsed) && !ignoreModal) {
6372
handleShow();
6473
}
6574
}, [recoveryCodesViewed, allRecoveryCodesUsed]);
6675

76+
useEffect(() => {
77+
if (localStorage.getItem("recoveryCodesModal") === "true") {
78+
setIgnoreMoral(true);
79+
}
80+
}, []);
81+
6782
return (
6883
<>
6984
<Card className={styles.default["cardSpacing"]}>
@@ -134,9 +149,12 @@ const RecoveryCodes = function ({ credentials }) {
134149
</Button>
135150
</Modal.Body>
136151
<Modal.Footer>
137-
<Button variant="primary" onClick={handleClose}>
152+
<Button onClick={handleClose} variant="primary btn-block mt-3">
138153
{t("recovery-codes.close-button")}
139154
</Button>
155+
<Button onClick={handleIgnore} variant="light btn-block mt-3">
156+
{t("recovery-codes.ignore")}
157+
</Button>
140158
</Modal.Footer>
141159
</Modal>
142160
</>

‎clients/web/react/src/_components/TrustedDevices/AddTrustedDevice.tsx

+21-44
Original file line numberDiff line numberDiff line change
@@ -78,30 +78,28 @@ const AddTrustedDevice = function ({ continueStep }) {
7878
*/
7979
const handleSaveAdd = async () => {
8080
setSubmitted(true);
81-
82-
const result = validate({ nickname }, constraints);
83-
if (result) {
84-
setInvalidNickname(result.nickname.join(". "));
85-
} else {
86-
setInvalidNickname(undefined);
87-
setLoading(true);
88-
try {
89-
await register();
90-
continueStep();
91-
} catch (error) {
92-
console.error(
93-
t("console.error", {
94-
COMPONENT: "AddTrustedDevice",
95-
METHOD: "handleSaveAdd()",
96-
REASON: t("console.reason.addTrustedDevice0"),
97-
}),
98-
error
99-
);
100-
setLoading(false);
101-
}
81+
setLoading(true);
82+
try {
83+
await register();
84+
continueStep();
85+
} catch (error) {
86+
console.error(
87+
t("console.error", {
88+
COMPONENT: "AddTrustedDevice",
89+
METHOD: "handleSaveAdd()",
90+
REASON: t("console.reason.addTrustedDevice0"),
91+
}),
92+
error
93+
);
94+
setLoading(false);
10295
}
10396
};
10497

98+
function handleAndroidAuthenticator() {
99+
if (DetectBrowser.getPlatform().id === "ANDROID_BIOMETRICS") return false;
100+
return true;
101+
}
102+
105103
/**
106104
* Primary logic of this method
107105
* Calls to the register API, and creates the credential on the authenticator
@@ -117,8 +115,8 @@ const AddTrustedDevice = function ({ continueStep }) {
117115
* true required to force discoverable credentials to be created on the platform authenticator, which is required
118116
*/
119117
await WebAuthnClient.registerNewCredential(
120-
nickname,
121-
true,
118+
// nickname,
119+
handleAndroidAuthenticator(),
122120
"PLATFORM",
123121
null
124122
);
@@ -155,27 +153,6 @@ const AddTrustedDevice = function ({ continueStep }) {
155153
<></>
156154
)}
157155
<AddTrustedDeviceGuidance PLAT_AUTH={PLAT_AUTH} />
158-
<label>{t("trusted-device.add-form-label")}</label>
159-
<input
160-
type="text"
161-
name="nickname"
162-
autoFocus
163-
value={nickname}
164-
ref={inputRef}
165-
onChange={handleChange}
166-
className={`form-control${
167-
submitted && invalidNickname ? " is-invalid" : ""
168-
}`}
169-
onKeyPress={(ev) => {
170-
if (ev.key === "Enter") {
171-
ev.preventDefault();
172-
handleSaveAdd();
173-
}
174-
}}
175-
/>
176-
{invalidNickname ? (
177-
<Alert variant="danger">{invalidNickname}</Alert>
178-
) : null}
179156
</Modal.Body>
180157
<Modal.Footer>
181158
<Button variant="secondary" onClick={handleClose}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import React, { useState, useRef } from "react";
2+
3+
import { Button, Modal, Alert } from "react-bootstrap";
4+
import { useDispatch } from "react-redux";
5+
import validate from "validate.js";
6+
import { useTranslation } from "react-i18next";
7+
import { credentialActions } from "../../_actions";
8+
9+
/**
10+
* Component used to display additional details about a credential, as well as allowing
11+
* the user to update the nickname, or delete
12+
* @param credential Data related to a specific credential
13+
* @returns
14+
*/
15+
const EditTrustedDevice = function ({ credential }) {
16+
const { t } = useTranslation();
17+
18+
const [show, setShow] = useState(false);
19+
20+
const [nickname, setNickname] = useState("");
21+
22+
const [invalidNickname, setInvalidNickname] = useState(undefined);
23+
24+
const [submitted, setSubmitted] = useState(false);
25+
26+
const dispatch = useDispatch();
27+
28+
/**
29+
* Closes the modal
30+
*/
31+
const handleClose = () => setShow(false);
32+
const handleShow = () => {
33+
setNickname(credential.credentialNickname.value);
34+
setShow(true);
35+
};
36+
37+
const constraints = {
38+
nickname: {
39+
length: {
40+
minimum: 1,
41+
maximum: 20,
42+
},
43+
},
44+
};
45+
/**
46+
* Handles the pressing of the delete button
47+
* IF the ID of the deleted device matches the ID of the Trusted Device locally stored
48+
* then it removes any local information related to the trusted device
49+
*/
50+
const handleDelete = () => {
51+
setShow(false);
52+
dispatch(
53+
credentialActions.delete(credential.credential.credentialId.base64url)
54+
);
55+
if (
56+
credential.credential.credentialId.base64url ===
57+
localStorage.getItem("trustedDeviceID")
58+
) {
59+
localStorage.removeItem("trustedDevice");
60+
localStorage.removeItem("trustedDeviceID");
61+
}
62+
};
63+
64+
/**
65+
* Handles when the user saves a new nickname for their security key
66+
* First validates if the nickname is valid before allowing an update
67+
*/
68+
const handleSave = () => {
69+
setSubmitted(true);
70+
const result = validate({ nickname }, constraints);
71+
if (result) {
72+
setInvalidNickname(result.nickname.join(". "));
73+
return result;
74+
}
75+
setInvalidNickname(undefined);
76+
77+
setShow(false);
78+
credential.credentialNickname.value = nickname;
79+
dispatch(credentialActions.update(credential));
80+
};
81+
82+
/**
83+
* Used to validate nickname changes on user input
84+
* @param e Event triggered by user action
85+
*/
86+
const handleChange = (e) => {
87+
const { name, value } = e.target;
88+
setNickname(value);
89+
const result = validate({ nickname: value }, constraints);
90+
if (result) {
91+
setInvalidNickname(result.nickname.join(". "));
92+
} else {
93+
setInvalidNickname(undefined);
94+
}
95+
};
96+
const inputRef = useRef(null);
97+
98+
return (
99+
<>
100+
<Button variant="secondary" onClick={handleShow}>
101+
{t("trusted-device.edit-button")}
102+
</Button>
103+
<Modal show={show} onHide={handleClose}>
104+
<Modal.Header closeButton>
105+
<Modal.Title>{t("trusted-device.edit-header")}</Modal.Title>
106+
</Modal.Header>
107+
<Modal.Body>
108+
<label>
109+
{t("trusted-device.edit-form-label")}{" "}
110+
<input
111+
type="text"
112+
name="nickname"
113+
autoFocus
114+
value={nickname}
115+
ref={inputRef}
116+
onChange={handleChange}
117+
className={`form-control${
118+
submitted && invalidNickname ? " is-invalid" : ""
119+
}`}
120+
onKeyPress={(ev) => {
121+
if (ev.key === "Enter") {
122+
handleSave();
123+
ev.preventDefault();
124+
}
125+
}}
126+
/>
127+
</label>
128+
<br />
129+
{invalidNickname ? (
130+
<Alert variant="danger">{invalidNickname}</Alert>
131+
) : null}
132+
&nbsp;&nbsp;
133+
<br />
134+
<h4>{t("trusted-device.yubico-att-label")}</h4>
135+
{credential?.attestationMetadata?.value?.description && (
136+
<p>
137+
<em>{t("credential.att-device-name")}</em>{" "}
138+
{credential.attestationMetadata.value.description}
139+
</p>
140+
)}
141+
{credential?.attestationMetadata?.value?.aaguid && (
142+
<p>
143+
<em>{t("trusted-device.att-aaguid")}</em>{" "}
144+
{credential.attestationMetadata.value.aaguid}
145+
</p>
146+
)}
147+
{credential?.attestationMetadata?.value?.aaid && (
148+
<p>
149+
<em>{t("trusted-device.att-aaid")}</em>{" "}
150+
{credential.attestationMetadata.value.aaid}
151+
</p>
152+
)}
153+
{credential?.attestationMetadata?.value?.authenticatorTransport &&
154+
credential?.attestationMetadata?.value?.authenticatorTransport
155+
.length > 0 && (
156+
<p>
157+
<em>{t("trusted-device.att-device-interfaces")}</em>{" "}
158+
<ul>
159+
{credential.attestationMetadata.value.authenticatorTransport.map(
160+
(transport, index) => (
161+
<li key={index}>{transport.id}</li>
162+
)
163+
)}
164+
</ul>
165+
</p>
166+
)}
167+
<label>
168+
<em>{t("trusted-device.edit-usernameless")}</em>{" "}
169+
{credential.registrationRequest
170+
? credential.registrationRequest.requireResidentKey.toString()
171+
: ""}
172+
</label>
173+
<br />
174+
<label>
175+
<em>{t("trusted-device.last-time-used")}</em>{" "}
176+
{credential.lastUsedTime
177+
? new Date(
178+
credential.lastUsedTime.seconds * 1000
179+
).toLocaleString()
180+
: ""}
181+
</label>
182+
<br />
183+
<label>
184+
<em>{t("trusted-device.last-update-time")}</em>{" "}
185+
{credential.lastUpdatedTime
186+
? new Date(
187+
credential.lastUpdatedTime.seconds * 1000
188+
).toLocaleString()
189+
: ""}
190+
</label>
191+
<br />
192+
<label>
193+
<em>{t("trusted-device.registration-time")}</em>{" "}
194+
{credential.registrationTime
195+
? new Date(
196+
credential.registrationTime.seconds * 1000
197+
).toLocaleString()
198+
: ""}
199+
</label>
200+
<br />
201+
</Modal.Body>
202+
<Modal.Footer>
203+
<Button variant="secondary" onClick={handleClose}>
204+
{t("trusted-device.edit-cancel-button")}
205+
</Button>
206+
<Button variant="danger" onClick={handleDelete}>
207+
{t("trusted-device.edit-delete-button")}
208+
</Button>
209+
<Button variant="primary" onClick={handleSave}>
210+
{t("trusted-device.edit-save-button")}
211+
</Button>
212+
</Modal.Footer>
213+
</Modal>
214+
</>
215+
);
216+
};
217+
218+
export default EditTrustedDevice;

‎clients/web/react/src/_components/TrustedDevices/TrustedDevice.tsx

+18-22
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from "react";
22
import { useDispatch } from "react-redux";
3-
import { Button, Image } from "react-bootstrap";
4-
import { credentialActions } from "../../_actions";
3+
import { Image } from "react-bootstrap";
54
import { useTranslation } from "react-i18next";
5+
import { credentialActions } from "../../_actions";
6+
import EditTrustedDevice from "./EditTrustedDevice";
67

78
const styles = require("../component.module.css");
89

@@ -13,24 +14,16 @@ const styles = require("../component.module.css");
1314
const TrustedDevice = function ({ credential }) {
1415
const { t } = useTranslation();
1516

16-
const dispatch = useDispatch();
17-
1817
/**
19-
* Handles the pressing of the delete button
20-
* IF the ID of the deleted device matches the ID of the Trusted Device locally stored
21-
* then it removes any local information related to the trusted device
18+
* Takes the image URL provided by the credential after attestation
19+
* If no image is found, then a default is set
20+
* @param credential
21+
* @returns URL from the attestation response, or a default image if no icon is present
2222
*/
23-
const handleDelete = () => {
24-
dispatch(
25-
credentialActions.delete(credential.credential.credentialId.base64)
26-
);
27-
if (
28-
credential.credential.credentialId.base64 ===
29-
localStorage.getItem("trustedDeviceID")
30-
) {
31-
localStorage.removeItem("trustedDevice");
32-
localStorage.removeItem("trustedDeviceID");
33-
}
23+
const getAttestationImage = (credential) => {
24+
const imgUrl = credential.attestationMetadata?.value?.icon;
25+
if (imgUrl) return imgUrl;
26+
return "https://www.yubico.com/wp-content/uploads//2021/02/illus-shield-lock-r1-dkteal.svg";
3427
};
3528

3629
return (
@@ -39,21 +32,24 @@ const TrustedDevice = function ({ credential }) {
3932
<div className="p-2">
4033
<Image
4134
className={styles.default["security-key-image"]}
42-
src="https://www.yubico.com/wp-content/uploads/2021/01/illus-fingerprint-r1-dk-teal-1.svg"
35+
src={getAttestationImage(credential)}
4336
roundedCircle
4437
/>
4538
</div>
4639
<div className="p-2 flex-grow-1">
4740
<h5>{credential.credentialNickname.value}</h5>
41+
{credential?.attestationMetadata?.value?.description &&
42+
credential.attestationMetadata.value.description !==
43+
credential.credentialNickname.value && (
44+
<h6>{credential.attestationMetadata.value.description}</h6>
45+
)}
4846
<p>
4947
{t("trusted-device.date-last-used")}{" "}
5048
{new Date(credential.lastUsedTime.seconds * 1000).toLocaleString()}
5149
</p>
5250
</div>
5351
<div className="m-2">
54-
<Button variant="danger" onClick={handleDelete}>
55-
{t("trusted-device.delete-button")}
56-
</Button>
52+
<EditTrustedDevice credential={credential} />
5753
</div>
5854
</div>
5955
<hr className={styles.default["section-divider"]} />

‎clients/web/react/src/_components/WebAuthnClient.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,14 @@ async function getAuthChallegeResponse(cognitoChallenge) {
186186
);
187187

188188
const assertionResponse = await get(publicKey);
189+
// This needs to be done as Apple sets userHandle to "", preventing the user from logging in
190+
if (assertionResponse.response.userHandle === "")
191+
assertionResponse.response.userHandle = null;
189192
console.info(
190193
t("console.info", {
191194
COMPONENT: "WebAuthnClient",
192195
METHOD: "getAuthChallegeResponse()",
193-
LOG_REASON: t("console.reason.webauthnClient3"),
196+
LOG_REASON: t("console.reason.webauthnClient4"),
194197
}),
195198
assertionResponse
196199
);
@@ -734,7 +737,6 @@ async function signUp(name, requestUV, registerWebKit) {
734737
* @param requestUV callback used to trigger modal if UV is not present on the authenticator allowing the user to provide a U2F Password
735738
*/
736739
async function registerNewCredential(
737-
nickname,
738740
isResidentKey,
739741
authenticatorAttachment = "CROSS_PLATFORM",
740742
requestUV
@@ -747,15 +749,13 @@ async function registerNewCredential(
747749
LOG_REASON: t("console.reason.webauthnClient16"),
748750
}),
749751
{
750-
nickname,
751752
isResidentKey,
752753
authenticatorAttachment,
753754
}
754755
);
755756
const startRegistrationResponse = await axios.post(
756757
"/users/credentials/fido2/register",
757758
{
758-
nickname,
759759
requireResidentKey: isResidentKey,
760760
requireAuthenticatorAttachment: authenticatorAttachment,
761761
}
@@ -805,7 +805,6 @@ async function registerNewCredential(
805805
requestId,
806806
pinSet: startRegistrationResponse.data.pinSet,
807807
pinCode: defaultInvalidPIN,
808-
nickname,
809808
};
810809
console.info(
811810
t("console.info", {

‎clients/web/react/src/_components/component.module.css

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@
2626
}
2727

2828
.security-key-image {
29-
border: 3px #e7e7e7 solid;
29+
border: 2px #9aca3c solid;
3030
padding: 10px;
3131
width: 75px;
32-
height: 75px
32+
height: 75px;
33+
background-color: #e9e9e9;
3334
}
3435

3536
.security-key-image-col {

0 commit comments

Comments
 (0)
Please sign in to comment.