@authhero/widget 0.29.0 → 0.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authhero-widget/authhero-widget.esm.js +1 -1
- package/dist/authhero-widget/p-88f80b3e.entry.js +1 -0
- package/dist/authhero-widget/p-975a907f.entry.js +1 -0
- package/dist/cjs/authhero-node.cjs.entry.js +4 -3
- package/dist/cjs/authhero-widget.cjs.entry.js +236 -20
- package/dist/collection/components/authhero-node/authhero-node.js +4 -3
- package/dist/collection/components/authhero-widget/authhero-widget.js +236 -20
- package/dist/components/authhero-node.js +1 -1
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/p-B0ntyox9.js +1 -0
- package/dist/esm/authhero-node.entry.js +4 -3
- package/dist/esm/authhero-widget.entry.js +236 -20
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +16 -0
- package/hydrate/index.js +240 -23
- package/hydrate/index.mjs +240 -23
- package/package.json +2 -2
- package/dist/authhero-widget/p-5428e2e1.entry.js +0 -1
- package/dist/authhero-widget/p-8514f73f.entry.js +0 -1
- package/dist/components/p-CncHQfQk.js +0 -1
|
@@ -631,6 +631,11 @@ const AuthheroWidget = class {
|
|
|
631
631
|
* Form data collected from inputs.
|
|
632
632
|
*/
|
|
633
633
|
formData = {};
|
|
634
|
+
/**
|
|
635
|
+
* AbortController for an in-flight conditional mediation request.
|
|
636
|
+
* Aborted on screen change or component disconnect.
|
|
637
|
+
*/
|
|
638
|
+
conditionalMediationAbort;
|
|
634
639
|
/**
|
|
635
640
|
* Emitted when the form is submitted.
|
|
636
641
|
* The consuming application should handle the submission unless autoSubmit is true.
|
|
@@ -665,6 +670,9 @@ const AuthheroWidget = class {
|
|
|
665
670
|
*/
|
|
666
671
|
screenChange;
|
|
667
672
|
watchScreen(newValue) {
|
|
673
|
+
// Abort any in-flight conditional mediation when screen changes
|
|
674
|
+
this.conditionalMediationAbort?.abort();
|
|
675
|
+
this.conditionalMediationAbort = undefined;
|
|
668
676
|
if (typeof newValue === "string") {
|
|
669
677
|
try {
|
|
670
678
|
this._screen = JSON.parse(newValue);
|
|
@@ -898,6 +906,8 @@ const AuthheroWidget = class {
|
|
|
898
906
|
}
|
|
899
907
|
disconnectedCallback() {
|
|
900
908
|
window.removeEventListener("popstate", this.handlePopState);
|
|
909
|
+
this.conditionalMediationAbort?.abort();
|
|
910
|
+
this.conditionalMediationAbort = undefined;
|
|
901
911
|
}
|
|
902
912
|
async componentWillLoad() {
|
|
903
913
|
// Parse initial props - this prevents unnecessary state changes during hydration that cause flashes
|
|
@@ -986,6 +996,10 @@ const AuthheroWidget = class {
|
|
|
986
996
|
this.updateDataScreenAttribute();
|
|
987
997
|
this.persistState();
|
|
988
998
|
this.focusFirstInput();
|
|
999
|
+
// Start WebAuthn ceremony if returned with the screen (e.g. conditional mediation)
|
|
1000
|
+
if (data.ceremony) {
|
|
1001
|
+
this.performWebAuthnCeremony(data.ceremony);
|
|
1002
|
+
}
|
|
989
1003
|
return true;
|
|
990
1004
|
}
|
|
991
1005
|
}
|
|
@@ -1177,9 +1191,19 @@ const AuthheroWidget = class {
|
|
|
1177
1191
|
console.error("Invalid WebAuthn ceremony payload", ceremony);
|
|
1178
1192
|
return;
|
|
1179
1193
|
}
|
|
1194
|
+
if (ceremony.type === "webauthn-authentication-conditional") {
|
|
1195
|
+
// Conditional mediation runs in the background, no requestAnimationFrame needed
|
|
1196
|
+
this.executeWebAuthnConditionalMediation(ceremony);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1180
1199
|
requestAnimationFrame(() => {
|
|
1181
1200
|
this.overrideFormSubmit();
|
|
1182
|
-
|
|
1201
|
+
if (ceremony.type === "webauthn-authentication") {
|
|
1202
|
+
this.executeWebAuthnAuthentication(ceremony);
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
this.executeWebAuthnRegistration(ceremony);
|
|
1206
|
+
}
|
|
1183
1207
|
});
|
|
1184
1208
|
}
|
|
1185
1209
|
/**
|
|
@@ -1190,8 +1214,6 @@ const AuthheroWidget = class {
|
|
|
1190
1214
|
if (typeof data !== "object" || data === null)
|
|
1191
1215
|
return false;
|
|
1192
1216
|
const obj = data;
|
|
1193
|
-
if (obj.type !== "webauthn-registration")
|
|
1194
|
-
return false;
|
|
1195
1217
|
if (typeof obj.successAction !== "string")
|
|
1196
1218
|
return false;
|
|
1197
1219
|
const opts = obj.options;
|
|
@@ -1200,23 +1222,30 @@ const AuthheroWidget = class {
|
|
|
1200
1222
|
const o = opts;
|
|
1201
1223
|
if (typeof o.challenge !== "string")
|
|
1202
1224
|
return false;
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
typeof rp.
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
typeof u.
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1225
|
+
if (obj.type === "webauthn-registration") {
|
|
1226
|
+
const rp = o.rp;
|
|
1227
|
+
if (typeof rp !== "object" || rp === null)
|
|
1228
|
+
return false;
|
|
1229
|
+
if (typeof rp.id !== "string" ||
|
|
1230
|
+
typeof rp.name !== "string")
|
|
1231
|
+
return false;
|
|
1232
|
+
const user = o.user;
|
|
1233
|
+
if (typeof user !== "object" || user === null)
|
|
1234
|
+
return false;
|
|
1235
|
+
const u = user;
|
|
1236
|
+
if (typeof u.id !== "string" ||
|
|
1237
|
+
typeof u.name !== "string" ||
|
|
1238
|
+
typeof u.displayName !== "string")
|
|
1239
|
+
return false;
|
|
1240
|
+
if (!Array.isArray(o.pubKeyCredParams))
|
|
1241
|
+
return false;
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
if (obj.type === "webauthn-authentication" ||
|
|
1245
|
+
obj.type === "webauthn-authentication-conditional") {
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
return false;
|
|
1220
1249
|
}
|
|
1221
1250
|
/**
|
|
1222
1251
|
* Perform the WebAuthn navigator.credentials.create() ceremony and submit
|
|
@@ -1329,6 +1358,193 @@ const AuthheroWidget = class {
|
|
|
1329
1358
|
}
|
|
1330
1359
|
}
|
|
1331
1360
|
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Perform the WebAuthn navigator.credentials.get() ceremony (explicit modal)
|
|
1363
|
+
* and submit the credential result via the form.
|
|
1364
|
+
*/
|
|
1365
|
+
async executeWebAuthnAuthentication(ceremony) {
|
|
1366
|
+
const opts = ceremony.options;
|
|
1367
|
+
const b64uToBuf = (s) => {
|
|
1368
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1369
|
+
while (s.length % 4)
|
|
1370
|
+
s += "=";
|
|
1371
|
+
const b = atob(s);
|
|
1372
|
+
const a = new Uint8Array(b.length);
|
|
1373
|
+
for (let i = 0; i < b.length; i++)
|
|
1374
|
+
a[i] = b.charCodeAt(i);
|
|
1375
|
+
return a.buffer;
|
|
1376
|
+
};
|
|
1377
|
+
const bufToB64u = (b) => {
|
|
1378
|
+
const a = new Uint8Array(b);
|
|
1379
|
+
let s = "";
|
|
1380
|
+
for (let i = 0; i < a.length; i++)
|
|
1381
|
+
s += String.fromCharCode(a[i]);
|
|
1382
|
+
return btoa(s)
|
|
1383
|
+
.replace(/\+/g, "-")
|
|
1384
|
+
.replace(/\//g, "_")
|
|
1385
|
+
.replace(/=+$/, "");
|
|
1386
|
+
};
|
|
1387
|
+
const findForm = () => {
|
|
1388
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
1389
|
+
if (shadowRoot) {
|
|
1390
|
+
const f = shadowRoot.querySelector("form");
|
|
1391
|
+
if (f)
|
|
1392
|
+
return f;
|
|
1393
|
+
}
|
|
1394
|
+
return document.querySelector("form");
|
|
1395
|
+
};
|
|
1396
|
+
try {
|
|
1397
|
+
const publicKey = {
|
|
1398
|
+
challenge: b64uToBuf(opts.challenge),
|
|
1399
|
+
rpId: opts.rpId,
|
|
1400
|
+
timeout: opts.timeout,
|
|
1401
|
+
userVerification: opts.userVerification || "preferred",
|
|
1402
|
+
};
|
|
1403
|
+
if (opts.allowCredentials?.length) {
|
|
1404
|
+
publicKey.allowCredentials = opts.allowCredentials.map((c) => ({
|
|
1405
|
+
id: b64uToBuf(c.id),
|
|
1406
|
+
type: c.type,
|
|
1407
|
+
transports: (c.transports || []),
|
|
1408
|
+
}));
|
|
1409
|
+
}
|
|
1410
|
+
const cred = (await navigator.credentials.get({
|
|
1411
|
+
publicKey,
|
|
1412
|
+
}));
|
|
1413
|
+
const response = cred.response;
|
|
1414
|
+
const resp = {
|
|
1415
|
+
id: cred.id,
|
|
1416
|
+
rawId: bufToB64u(cred.rawId),
|
|
1417
|
+
type: cred.type,
|
|
1418
|
+
response: {
|
|
1419
|
+
authenticatorData: bufToB64u(response.authenticatorData),
|
|
1420
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
1421
|
+
signature: bufToB64u(response.signature),
|
|
1422
|
+
},
|
|
1423
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
1424
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
1425
|
+
};
|
|
1426
|
+
if (response.userHandle) {
|
|
1427
|
+
resp.response.userHandle = bufToB64u(response.userHandle);
|
|
1428
|
+
}
|
|
1429
|
+
const form = findForm();
|
|
1430
|
+
if (form) {
|
|
1431
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
1432
|
+
form.querySelector("#credential-field");
|
|
1433
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1434
|
+
form.querySelector("#action-field");
|
|
1435
|
+
if (cf)
|
|
1436
|
+
cf.value = JSON.stringify(resp);
|
|
1437
|
+
if (af)
|
|
1438
|
+
af.value = ceremony.successAction;
|
|
1439
|
+
form.submit();
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
catch (e) {
|
|
1443
|
+
console.error("WebAuthn authentication error:", e);
|
|
1444
|
+
const form = findForm();
|
|
1445
|
+
if (form) {
|
|
1446
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1447
|
+
form.querySelector("#action-field");
|
|
1448
|
+
if (af)
|
|
1449
|
+
af.value = "error";
|
|
1450
|
+
form.submit();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Execute WebAuthn conditional mediation (autofill-assisted passkeys).
|
|
1456
|
+
* Runs in the background — the browser shows passkey suggestions in the
|
|
1457
|
+
* username field's autofill dropdown. Silently ignored if unsupported.
|
|
1458
|
+
*/
|
|
1459
|
+
async executeWebAuthnConditionalMediation(ceremony) {
|
|
1460
|
+
// Feature detection
|
|
1461
|
+
if (!window.PublicKeyCredential ||
|
|
1462
|
+
!PublicKeyCredential.isConditionalMediationAvailable) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const available = await PublicKeyCredential.isConditionalMediationAvailable();
|
|
1466
|
+
if (!available)
|
|
1467
|
+
return;
|
|
1468
|
+
// Abort any previous conditional mediation request
|
|
1469
|
+
this.conditionalMediationAbort?.abort();
|
|
1470
|
+
const abortController = new AbortController();
|
|
1471
|
+
this.conditionalMediationAbort = abortController;
|
|
1472
|
+
const opts = ceremony.options;
|
|
1473
|
+
const b64uToBuf = (s) => {
|
|
1474
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1475
|
+
while (s.length % 4)
|
|
1476
|
+
s += "=";
|
|
1477
|
+
const b = atob(s);
|
|
1478
|
+
const a = new Uint8Array(b.length);
|
|
1479
|
+
for (let i = 0; i < b.length; i++)
|
|
1480
|
+
a[i] = b.charCodeAt(i);
|
|
1481
|
+
return a.buffer;
|
|
1482
|
+
};
|
|
1483
|
+
const bufToB64u = (b) => {
|
|
1484
|
+
const a = new Uint8Array(b);
|
|
1485
|
+
let s = "";
|
|
1486
|
+
for (let i = 0; i < a.length; i++)
|
|
1487
|
+
s += String.fromCharCode(a[i]);
|
|
1488
|
+
return btoa(s)
|
|
1489
|
+
.replace(/\+/g, "-")
|
|
1490
|
+
.replace(/\//g, "_")
|
|
1491
|
+
.replace(/=+$/, "");
|
|
1492
|
+
};
|
|
1493
|
+
try {
|
|
1494
|
+
const cred = (await navigator.credentials.get({
|
|
1495
|
+
mediation: "conditional",
|
|
1496
|
+
signal: abortController.signal,
|
|
1497
|
+
publicKey: {
|
|
1498
|
+
challenge: b64uToBuf(opts.challenge),
|
|
1499
|
+
rpId: opts.rpId,
|
|
1500
|
+
timeout: opts.timeout,
|
|
1501
|
+
userVerification: opts.userVerification ||
|
|
1502
|
+
"preferred",
|
|
1503
|
+
},
|
|
1504
|
+
}));
|
|
1505
|
+
const response = cred.response;
|
|
1506
|
+
const resp = {
|
|
1507
|
+
id: cred.id,
|
|
1508
|
+
rawId: bufToB64u(cred.rawId),
|
|
1509
|
+
type: cred.type,
|
|
1510
|
+
response: {
|
|
1511
|
+
authenticatorData: bufToB64u(response.authenticatorData),
|
|
1512
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
1513
|
+
signature: bufToB64u(response.signature),
|
|
1514
|
+
},
|
|
1515
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
1516
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
1517
|
+
};
|
|
1518
|
+
if (response.userHandle) {
|
|
1519
|
+
resp.response.userHandle = bufToB64u(response.userHandle);
|
|
1520
|
+
}
|
|
1521
|
+
// Submit via the widget's form handling
|
|
1522
|
+
this.formData["credential-field"] = JSON.stringify(resp);
|
|
1523
|
+
this.formData["action-field"] = ceremony.successAction;
|
|
1524
|
+
// Ensure form submit override is set up, then submit
|
|
1525
|
+
this.overrideFormSubmit();
|
|
1526
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
1527
|
+
const form = shadowRoot?.querySelector("form");
|
|
1528
|
+
if (form) {
|
|
1529
|
+
// Set the hidden input values directly
|
|
1530
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
1531
|
+
form.querySelector("#credential-field");
|
|
1532
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1533
|
+
form.querySelector("#action-field");
|
|
1534
|
+
if (cf)
|
|
1535
|
+
cf.value = JSON.stringify(resp);
|
|
1536
|
+
if (af)
|
|
1537
|
+
af.value = ceremony.successAction;
|
|
1538
|
+
form.submit();
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
catch (e) {
|
|
1542
|
+
// Silently ignore AbortError and NotAllowedError
|
|
1543
|
+
if (e?.name === "AbortError" || e?.name === "NotAllowedError")
|
|
1544
|
+
return;
|
|
1545
|
+
console.error("Conditional mediation error:", e);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1332
1548
|
handleButtonClick = (detail) => {
|
|
1333
1549
|
// If this is a submit button click, trigger form submission
|
|
1334
1550
|
if (detail.type === "submit") {
|
|
@@ -173,6 +173,11 @@ export declare class AuthheroWidget {
|
|
|
173
173
|
* Form data collected from inputs.
|
|
174
174
|
*/
|
|
175
175
|
formData: Record<string, string>;
|
|
176
|
+
/**
|
|
177
|
+
* AbortController for an in-flight conditional mediation request.
|
|
178
|
+
* Aborted on screen change or component disconnect.
|
|
179
|
+
*/
|
|
180
|
+
private conditionalMediationAbort?;
|
|
176
181
|
/**
|
|
177
182
|
* Emitted when the form is submitted.
|
|
178
183
|
* The consuming application should handle the submission unless autoSubmit is true.
|
|
@@ -282,6 +287,17 @@ export declare class AuthheroWidget {
|
|
|
282
287
|
* the credential result via the form.
|
|
283
288
|
*/
|
|
284
289
|
private executeWebAuthnRegistration;
|
|
290
|
+
/**
|
|
291
|
+
* Perform the WebAuthn navigator.credentials.get() ceremony (explicit modal)
|
|
292
|
+
* and submit the credential result via the form.
|
|
293
|
+
*/
|
|
294
|
+
private executeWebAuthnAuthentication;
|
|
295
|
+
/**
|
|
296
|
+
* Execute WebAuthn conditional mediation (autofill-assisted passkeys).
|
|
297
|
+
* Runs in the background — the browser shows passkey suggestions in the
|
|
298
|
+
* username field's autofill dropdown. Silently ignored if unsupported.
|
|
299
|
+
*/
|
|
300
|
+
private executeWebAuthnConditionalMediation;
|
|
285
301
|
private handleButtonClick;
|
|
286
302
|
/**
|
|
287
303
|
* Handle social login redirect
|
package/hydrate/index.js
CHANGED
|
@@ -5557,20 +5557,21 @@ class AuthheroNode {
|
|
|
5557
5557
|
renderTextField(component) {
|
|
5558
5558
|
const inputId = `input-${component.id}`;
|
|
5559
5559
|
const errors = this.getErrors();
|
|
5560
|
-
const { multiline, max_length } = component.config ?? {};
|
|
5560
|
+
const { multiline, max_length, autocomplete } = component.config ?? {};
|
|
5561
5561
|
const effectiveValue = this.getEffectiveValue();
|
|
5562
5562
|
const hasValue = !!(effectiveValue && effectiveValue.length > 0);
|
|
5563
5563
|
if (multiline) {
|
|
5564
5564
|
return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, this.renderLabel(component.label, inputId, component.required), hAsync("textarea", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input textarea", name: component.id, placeholder: " ", required: component.required, disabled: this.disabled, maxLength: max_length, onInput: this.handleInput }, effectiveValue ?? ""), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
5565
5565
|
}
|
|
5566
|
-
return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, hAsync("div", { class: "input-container" }, hAsync("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: component.sensitive ? "password" : "text", name: component.id, "data-input-name": component.id, value: effectiveValue ?? "", placeholder: " ", required: component.required, disabled: this.disabled, maxLength: max_length, onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderFloatingLabel(component.label, inputId, component.required, hasValue)), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
5566
|
+
return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, hAsync("div", { class: "input-container" }, hAsync("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: component.sensitive ? "password" : "text", name: component.id, "data-input-name": component.id, value: effectiveValue ?? "", placeholder: " ", required: component.required, disabled: this.disabled, maxLength: max_length, autoComplete: autocomplete, onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderFloatingLabel(component.label, inputId, component.required, hasValue)), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
5567
5567
|
}
|
|
5568
5568
|
renderEmailField(component) {
|
|
5569
5569
|
const inputId = `input-${component.id}`;
|
|
5570
5570
|
const errors = this.getErrors();
|
|
5571
5571
|
const effectiveValue = this.getEffectiveValue();
|
|
5572
5572
|
const hasValue = !!(effectiveValue && effectiveValue.length > 0);
|
|
5573
|
-
|
|
5573
|
+
const { autocomplete } = (component.config ?? {});
|
|
5574
|
+
return (hAsync("div", { class: "input-wrapper", part: "input-wrapper" }, hAsync("div", { class: "input-container" }, hAsync("input", { id: inputId, class: this.getInputFieldClass(errors.length > 0), part: "input", type: "email", name: component.id, "data-input-name": component.id, value: effectiveValue ?? "", placeholder: " ", required: component.required, disabled: this.disabled, autocomplete: autocomplete || "email", onInput: this.handleInput, onKeyDown: this.handleKeyDown }), this.renderFloatingLabel(component.label, inputId, component.required, hasValue)), this.renderErrors(), errors.length === 0 && this.renderHint(component.hint)));
|
|
5574
5575
|
}
|
|
5575
5576
|
renderPasswordField(component) {
|
|
5576
5577
|
const inputId = `input-${component.id}`;
|
|
@@ -6435,6 +6436,11 @@ class AuthheroWidget {
|
|
|
6435
6436
|
* Form data collected from inputs.
|
|
6436
6437
|
*/
|
|
6437
6438
|
formData = {};
|
|
6439
|
+
/**
|
|
6440
|
+
* AbortController for an in-flight conditional mediation request.
|
|
6441
|
+
* Aborted on screen change or component disconnect.
|
|
6442
|
+
*/
|
|
6443
|
+
conditionalMediationAbort;
|
|
6438
6444
|
/**
|
|
6439
6445
|
* Emitted when the form is submitted.
|
|
6440
6446
|
* The consuming application should handle the submission unless autoSubmit is true.
|
|
@@ -6469,6 +6475,9 @@ class AuthheroWidget {
|
|
|
6469
6475
|
*/
|
|
6470
6476
|
screenChange;
|
|
6471
6477
|
watchScreen(newValue) {
|
|
6478
|
+
// Abort any in-flight conditional mediation when screen changes
|
|
6479
|
+
this.conditionalMediationAbort?.abort();
|
|
6480
|
+
this.conditionalMediationAbort = undefined;
|
|
6472
6481
|
if (typeof newValue === "string") {
|
|
6473
6482
|
try {
|
|
6474
6483
|
this._screen = JSON.parse(newValue);
|
|
@@ -6702,6 +6711,8 @@ class AuthheroWidget {
|
|
|
6702
6711
|
}
|
|
6703
6712
|
disconnectedCallback() {
|
|
6704
6713
|
window.removeEventListener("popstate", this.handlePopState);
|
|
6714
|
+
this.conditionalMediationAbort?.abort();
|
|
6715
|
+
this.conditionalMediationAbort = undefined;
|
|
6705
6716
|
}
|
|
6706
6717
|
async componentWillLoad() {
|
|
6707
6718
|
// Parse initial props - this prevents unnecessary state changes during hydration that cause flashes
|
|
@@ -6790,6 +6801,10 @@ class AuthheroWidget {
|
|
|
6790
6801
|
this.updateDataScreenAttribute();
|
|
6791
6802
|
this.persistState();
|
|
6792
6803
|
this.focusFirstInput();
|
|
6804
|
+
// Start WebAuthn ceremony if returned with the screen (e.g. conditional mediation)
|
|
6805
|
+
if (data.ceremony) {
|
|
6806
|
+
this.performWebAuthnCeremony(data.ceremony);
|
|
6807
|
+
}
|
|
6793
6808
|
return true;
|
|
6794
6809
|
}
|
|
6795
6810
|
}
|
|
@@ -6981,9 +6996,19 @@ class AuthheroWidget {
|
|
|
6981
6996
|
console.error("Invalid WebAuthn ceremony payload", ceremony);
|
|
6982
6997
|
return;
|
|
6983
6998
|
}
|
|
6999
|
+
if (ceremony.type === "webauthn-authentication-conditional") {
|
|
7000
|
+
// Conditional mediation runs in the background, no requestAnimationFrame needed
|
|
7001
|
+
this.executeWebAuthnConditionalMediation(ceremony);
|
|
7002
|
+
return;
|
|
7003
|
+
}
|
|
6984
7004
|
requestAnimationFrame(() => {
|
|
6985
7005
|
this.overrideFormSubmit();
|
|
6986
|
-
|
|
7006
|
+
if (ceremony.type === "webauthn-authentication") {
|
|
7007
|
+
this.executeWebAuthnAuthentication(ceremony);
|
|
7008
|
+
}
|
|
7009
|
+
else {
|
|
7010
|
+
this.executeWebAuthnRegistration(ceremony);
|
|
7011
|
+
}
|
|
6987
7012
|
});
|
|
6988
7013
|
}
|
|
6989
7014
|
/**
|
|
@@ -6994,8 +7019,6 @@ class AuthheroWidget {
|
|
|
6994
7019
|
if (typeof data !== "object" || data === null)
|
|
6995
7020
|
return false;
|
|
6996
7021
|
const obj = data;
|
|
6997
|
-
if (obj.type !== "webauthn-registration")
|
|
6998
|
-
return false;
|
|
6999
7022
|
if (typeof obj.successAction !== "string")
|
|
7000
7023
|
return false;
|
|
7001
7024
|
const opts = obj.options;
|
|
@@ -7004,23 +7027,30 @@ class AuthheroWidget {
|
|
|
7004
7027
|
const o = opts;
|
|
7005
7028
|
if (typeof o.challenge !== "string")
|
|
7006
7029
|
return false;
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
typeof rp.
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
typeof u.
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7030
|
+
if (obj.type === "webauthn-registration") {
|
|
7031
|
+
const rp = o.rp;
|
|
7032
|
+
if (typeof rp !== "object" || rp === null)
|
|
7033
|
+
return false;
|
|
7034
|
+
if (typeof rp.id !== "string" ||
|
|
7035
|
+
typeof rp.name !== "string")
|
|
7036
|
+
return false;
|
|
7037
|
+
const user = o.user;
|
|
7038
|
+
if (typeof user !== "object" || user === null)
|
|
7039
|
+
return false;
|
|
7040
|
+
const u = user;
|
|
7041
|
+
if (typeof u.id !== "string" ||
|
|
7042
|
+
typeof u.name !== "string" ||
|
|
7043
|
+
typeof u.displayName !== "string")
|
|
7044
|
+
return false;
|
|
7045
|
+
if (!Array.isArray(o.pubKeyCredParams))
|
|
7046
|
+
return false;
|
|
7047
|
+
return true;
|
|
7048
|
+
}
|
|
7049
|
+
if (obj.type === "webauthn-authentication" ||
|
|
7050
|
+
obj.type === "webauthn-authentication-conditional") {
|
|
7051
|
+
return true;
|
|
7052
|
+
}
|
|
7053
|
+
return false;
|
|
7024
7054
|
}
|
|
7025
7055
|
/**
|
|
7026
7056
|
* Perform the WebAuthn navigator.credentials.create() ceremony and submit
|
|
@@ -7133,6 +7163,193 @@ class AuthheroWidget {
|
|
|
7133
7163
|
}
|
|
7134
7164
|
}
|
|
7135
7165
|
}
|
|
7166
|
+
/**
|
|
7167
|
+
* Perform the WebAuthn navigator.credentials.get() ceremony (explicit modal)
|
|
7168
|
+
* and submit the credential result via the form.
|
|
7169
|
+
*/
|
|
7170
|
+
async executeWebAuthnAuthentication(ceremony) {
|
|
7171
|
+
const opts = ceremony.options;
|
|
7172
|
+
const b64uToBuf = (s) => {
|
|
7173
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
7174
|
+
while (s.length % 4)
|
|
7175
|
+
s += "=";
|
|
7176
|
+
const b = atob(s);
|
|
7177
|
+
const a = new Uint8Array(b.length);
|
|
7178
|
+
for (let i = 0; i < b.length; i++)
|
|
7179
|
+
a[i] = b.charCodeAt(i);
|
|
7180
|
+
return a.buffer;
|
|
7181
|
+
};
|
|
7182
|
+
const bufToB64u = (b) => {
|
|
7183
|
+
const a = new Uint8Array(b);
|
|
7184
|
+
let s = "";
|
|
7185
|
+
for (let i = 0; i < a.length; i++)
|
|
7186
|
+
s += String.fromCharCode(a[i]);
|
|
7187
|
+
return btoa(s)
|
|
7188
|
+
.replace(/\+/g, "-")
|
|
7189
|
+
.replace(/\//g, "_")
|
|
7190
|
+
.replace(/=+$/, "");
|
|
7191
|
+
};
|
|
7192
|
+
const findForm = () => {
|
|
7193
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
7194
|
+
if (shadowRoot) {
|
|
7195
|
+
const f = shadowRoot.querySelector("form");
|
|
7196
|
+
if (f)
|
|
7197
|
+
return f;
|
|
7198
|
+
}
|
|
7199
|
+
return document.querySelector("form");
|
|
7200
|
+
};
|
|
7201
|
+
try {
|
|
7202
|
+
const publicKey = {
|
|
7203
|
+
challenge: b64uToBuf(opts.challenge),
|
|
7204
|
+
rpId: opts.rpId,
|
|
7205
|
+
timeout: opts.timeout,
|
|
7206
|
+
userVerification: opts.userVerification || "preferred",
|
|
7207
|
+
};
|
|
7208
|
+
if (opts.allowCredentials?.length) {
|
|
7209
|
+
publicKey.allowCredentials = opts.allowCredentials.map((c) => ({
|
|
7210
|
+
id: b64uToBuf(c.id),
|
|
7211
|
+
type: c.type,
|
|
7212
|
+
transports: (c.transports || []),
|
|
7213
|
+
}));
|
|
7214
|
+
}
|
|
7215
|
+
const cred = (await navigator.credentials.get({
|
|
7216
|
+
publicKey,
|
|
7217
|
+
}));
|
|
7218
|
+
const response = cred.response;
|
|
7219
|
+
const resp = {
|
|
7220
|
+
id: cred.id,
|
|
7221
|
+
rawId: bufToB64u(cred.rawId),
|
|
7222
|
+
type: cred.type,
|
|
7223
|
+
response: {
|
|
7224
|
+
authenticatorData: bufToB64u(response.authenticatorData),
|
|
7225
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
7226
|
+
signature: bufToB64u(response.signature),
|
|
7227
|
+
},
|
|
7228
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
7229
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
7230
|
+
};
|
|
7231
|
+
if (response.userHandle) {
|
|
7232
|
+
resp.response.userHandle = bufToB64u(response.userHandle);
|
|
7233
|
+
}
|
|
7234
|
+
const form = findForm();
|
|
7235
|
+
if (form) {
|
|
7236
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
7237
|
+
form.querySelector("#credential-field");
|
|
7238
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
7239
|
+
form.querySelector("#action-field");
|
|
7240
|
+
if (cf)
|
|
7241
|
+
cf.value = JSON.stringify(resp);
|
|
7242
|
+
if (af)
|
|
7243
|
+
af.value = ceremony.successAction;
|
|
7244
|
+
form.submit();
|
|
7245
|
+
}
|
|
7246
|
+
}
|
|
7247
|
+
catch (e) {
|
|
7248
|
+
console.error("WebAuthn authentication error:", e);
|
|
7249
|
+
const form = findForm();
|
|
7250
|
+
if (form) {
|
|
7251
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
7252
|
+
form.querySelector("#action-field");
|
|
7253
|
+
if (af)
|
|
7254
|
+
af.value = "error";
|
|
7255
|
+
form.submit();
|
|
7256
|
+
}
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
/**
|
|
7260
|
+
* Execute WebAuthn conditional mediation (autofill-assisted passkeys).
|
|
7261
|
+
* Runs in the background — the browser shows passkey suggestions in the
|
|
7262
|
+
* username field's autofill dropdown. Silently ignored if unsupported.
|
|
7263
|
+
*/
|
|
7264
|
+
async executeWebAuthnConditionalMediation(ceremony) {
|
|
7265
|
+
// Feature detection
|
|
7266
|
+
if (!window.PublicKeyCredential ||
|
|
7267
|
+
!PublicKeyCredential.isConditionalMediationAvailable) {
|
|
7268
|
+
return;
|
|
7269
|
+
}
|
|
7270
|
+
const available = await PublicKeyCredential.isConditionalMediationAvailable();
|
|
7271
|
+
if (!available)
|
|
7272
|
+
return;
|
|
7273
|
+
// Abort any previous conditional mediation request
|
|
7274
|
+
this.conditionalMediationAbort?.abort();
|
|
7275
|
+
const abortController = new AbortController();
|
|
7276
|
+
this.conditionalMediationAbort = abortController;
|
|
7277
|
+
const opts = ceremony.options;
|
|
7278
|
+
const b64uToBuf = (s) => {
|
|
7279
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
7280
|
+
while (s.length % 4)
|
|
7281
|
+
s += "=";
|
|
7282
|
+
const b = atob(s);
|
|
7283
|
+
const a = new Uint8Array(b.length);
|
|
7284
|
+
for (let i = 0; i < b.length; i++)
|
|
7285
|
+
a[i] = b.charCodeAt(i);
|
|
7286
|
+
return a.buffer;
|
|
7287
|
+
};
|
|
7288
|
+
const bufToB64u = (b) => {
|
|
7289
|
+
const a = new Uint8Array(b);
|
|
7290
|
+
let s = "";
|
|
7291
|
+
for (let i = 0; i < a.length; i++)
|
|
7292
|
+
s += String.fromCharCode(a[i]);
|
|
7293
|
+
return btoa(s)
|
|
7294
|
+
.replace(/\+/g, "-")
|
|
7295
|
+
.replace(/\//g, "_")
|
|
7296
|
+
.replace(/=+$/, "");
|
|
7297
|
+
};
|
|
7298
|
+
try {
|
|
7299
|
+
const cred = (await navigator.credentials.get({
|
|
7300
|
+
mediation: "conditional",
|
|
7301
|
+
signal: abortController.signal,
|
|
7302
|
+
publicKey: {
|
|
7303
|
+
challenge: b64uToBuf(opts.challenge),
|
|
7304
|
+
rpId: opts.rpId,
|
|
7305
|
+
timeout: opts.timeout,
|
|
7306
|
+
userVerification: opts.userVerification ||
|
|
7307
|
+
"preferred",
|
|
7308
|
+
},
|
|
7309
|
+
}));
|
|
7310
|
+
const response = cred.response;
|
|
7311
|
+
const resp = {
|
|
7312
|
+
id: cred.id,
|
|
7313
|
+
rawId: bufToB64u(cred.rawId),
|
|
7314
|
+
type: cred.type,
|
|
7315
|
+
response: {
|
|
7316
|
+
authenticatorData: bufToB64u(response.authenticatorData),
|
|
7317
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
7318
|
+
signature: bufToB64u(response.signature),
|
|
7319
|
+
},
|
|
7320
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
7321
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
7322
|
+
};
|
|
7323
|
+
if (response.userHandle) {
|
|
7324
|
+
resp.response.userHandle = bufToB64u(response.userHandle);
|
|
7325
|
+
}
|
|
7326
|
+
// Submit via the widget's form handling
|
|
7327
|
+
this.formData["credential-field"] = JSON.stringify(resp);
|
|
7328
|
+
this.formData["action-field"] = ceremony.successAction;
|
|
7329
|
+
// Ensure form submit override is set up, then submit
|
|
7330
|
+
this.overrideFormSubmit();
|
|
7331
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
7332
|
+
const form = shadowRoot?.querySelector("form");
|
|
7333
|
+
if (form) {
|
|
7334
|
+
// Set the hidden input values directly
|
|
7335
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
7336
|
+
form.querySelector("#credential-field");
|
|
7337
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
7338
|
+
form.querySelector("#action-field");
|
|
7339
|
+
if (cf)
|
|
7340
|
+
cf.value = JSON.stringify(resp);
|
|
7341
|
+
if (af)
|
|
7342
|
+
af.value = ceremony.successAction;
|
|
7343
|
+
form.submit();
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
catch (e) {
|
|
7347
|
+
// Silently ignore AbortError and NotAllowedError
|
|
7348
|
+
if (e?.name === "AbortError" || e?.name === "NotAllowedError")
|
|
7349
|
+
return;
|
|
7350
|
+
console.error("Conditional mediation error:", e);
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7136
7353
|
handleButtonClick = (detail) => {
|
|
7137
7354
|
// If this is a submit button click, trigger form submission
|
|
7138
7355
|
if (detail.type === "submit") {
|