@authhero/widget 0.28.2 → 0.29.1
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/index.esm.js +1 -1
- package/dist/authhero-widget/p-5428e2e1.entry.js +1 -0
- package/dist/cjs/authhero-widget.cjs.entry.js +206 -3
- package/dist/cjs/index.cjs.js +23 -5
- package/dist/collection/components/authhero-widget/authhero-widget.js +206 -3
- package/dist/components/authhero-widget.js +1 -1
- package/dist/components/index.js +1 -1
- package/dist/esm/authhero-widget.entry.js +206 -3
- package/dist/esm/index.js +23 -5
- package/dist/types/components/authhero-widget/authhero-widget.d.ts +22 -0
- package/hydrate/index.js +206 -3
- package/hydrate/index.mjs +206 -3
- package/package.json +2 -2
- package/dist/authhero-widget/p-b9ae0275.entry.js +0 -1
|
@@ -1019,7 +1019,18 @@ const AuthheroWidget = class {
|
|
|
1019
1019
|
e.preventDefault();
|
|
1020
1020
|
if (!this._screen || this.loading)
|
|
1021
1021
|
return;
|
|
1022
|
-
|
|
1022
|
+
let submitData = { ...this.formData, ...(overrideData || {}) };
|
|
1023
|
+
// Merge hidden input values from DOM (may have been set programmatically
|
|
1024
|
+
// by inline scripts, e.g. passkey management buttons)
|
|
1025
|
+
const form = this.el.shadowRoot?.querySelector("form");
|
|
1026
|
+
if (form) {
|
|
1027
|
+
const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
|
|
1028
|
+
hiddenInputs.forEach((input) => {
|
|
1029
|
+
if (input.name && input.value) {
|
|
1030
|
+
submitData[input.name] = input.value;
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1023
1034
|
// Always emit the submit event
|
|
1024
1035
|
this.formSubmit.emit({
|
|
1025
1036
|
screen: this._screen,
|
|
@@ -1095,6 +1106,10 @@ const AuthheroWidget = class {
|
|
|
1095
1106
|
this.state = result.state;
|
|
1096
1107
|
this.persistState();
|
|
1097
1108
|
}
|
|
1109
|
+
// Perform WebAuthn ceremony if present (structured data, not script)
|
|
1110
|
+
if (result.ceremony) {
|
|
1111
|
+
this.performWebAuthnCeremony(result.ceremony);
|
|
1112
|
+
}
|
|
1098
1113
|
// Focus first input on new screen
|
|
1099
1114
|
this.focusFirstInput();
|
|
1100
1115
|
}
|
|
@@ -1128,6 +1143,192 @@ const AuthheroWidget = class {
|
|
|
1128
1143
|
this.loading = false;
|
|
1129
1144
|
}
|
|
1130
1145
|
};
|
|
1146
|
+
/**
|
|
1147
|
+
* Override form.submit() so that scripts (e.g. WebAuthn ceremony) that call
|
|
1148
|
+
* form.submit() go through the widget's JSON fetch pipeline instead of a
|
|
1149
|
+
* native form-urlencoded POST.
|
|
1150
|
+
*/
|
|
1151
|
+
overrideFormSubmit() {
|
|
1152
|
+
const shadowRoot = this.el.shadowRoot;
|
|
1153
|
+
if (!shadowRoot)
|
|
1154
|
+
return;
|
|
1155
|
+
const form = shadowRoot.querySelector("form");
|
|
1156
|
+
if (!form)
|
|
1157
|
+
return;
|
|
1158
|
+
form.submit = () => {
|
|
1159
|
+
const formData = new FormData(form);
|
|
1160
|
+
const data = {};
|
|
1161
|
+
formData.forEach((value, key) => {
|
|
1162
|
+
if (typeof value === "string") {
|
|
1163
|
+
data[key] = value;
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
const syntheticEvent = { preventDefault: () => { } };
|
|
1167
|
+
this.handleSubmit(syntheticEvent, data);
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Validate and execute a structured WebAuthn ceremony returned by the server.
|
|
1172
|
+
* Instead of injecting arbitrary script content, this parses the ceremony JSON,
|
|
1173
|
+
* validates the expected fields, and calls the WebAuthn API natively.
|
|
1174
|
+
*/
|
|
1175
|
+
performWebAuthnCeremony(ceremony) {
|
|
1176
|
+
if (!this.isValidWebAuthnCeremony(ceremony)) {
|
|
1177
|
+
console.error("Invalid WebAuthn ceremony payload", ceremony);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
requestAnimationFrame(() => {
|
|
1181
|
+
this.overrideFormSubmit();
|
|
1182
|
+
this.executeWebAuthnRegistration(ceremony);
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Schema validation for WebAuthn ceremony payloads.
|
|
1187
|
+
* Checks required fields and types before invoking browser APIs.
|
|
1188
|
+
*/
|
|
1189
|
+
isValidWebAuthnCeremony(data) {
|
|
1190
|
+
if (typeof data !== "object" || data === null)
|
|
1191
|
+
return false;
|
|
1192
|
+
const obj = data;
|
|
1193
|
+
if (obj.type !== "webauthn-registration")
|
|
1194
|
+
return false;
|
|
1195
|
+
if (typeof obj.successAction !== "string")
|
|
1196
|
+
return false;
|
|
1197
|
+
const opts = obj.options;
|
|
1198
|
+
if (typeof opts !== "object" || opts === null)
|
|
1199
|
+
return false;
|
|
1200
|
+
const o = opts;
|
|
1201
|
+
if (typeof o.challenge !== "string")
|
|
1202
|
+
return false;
|
|
1203
|
+
const rp = o.rp;
|
|
1204
|
+
if (typeof rp !== "object" || rp === null)
|
|
1205
|
+
return false;
|
|
1206
|
+
if (typeof rp.id !== "string" ||
|
|
1207
|
+
typeof rp.name !== "string")
|
|
1208
|
+
return false;
|
|
1209
|
+
const user = o.user;
|
|
1210
|
+
if (typeof user !== "object" || user === null)
|
|
1211
|
+
return false;
|
|
1212
|
+
const u = user;
|
|
1213
|
+
if (typeof u.id !== "string" ||
|
|
1214
|
+
typeof u.name !== "string" ||
|
|
1215
|
+
typeof u.displayName !== "string")
|
|
1216
|
+
return false;
|
|
1217
|
+
if (!Array.isArray(o.pubKeyCredParams))
|
|
1218
|
+
return false;
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Perform the WebAuthn navigator.credentials.create() ceremony and submit
|
|
1223
|
+
* the credential result via the form.
|
|
1224
|
+
*/
|
|
1225
|
+
async executeWebAuthnRegistration(ceremony) {
|
|
1226
|
+
const opts = ceremony.options;
|
|
1227
|
+
const b64uToBuf = (s) => {
|
|
1228
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
1229
|
+
while (s.length % 4)
|
|
1230
|
+
s += "=";
|
|
1231
|
+
const b = atob(s);
|
|
1232
|
+
const a = new Uint8Array(b.length);
|
|
1233
|
+
for (let i = 0; i < b.length; i++)
|
|
1234
|
+
a[i] = b.charCodeAt(i);
|
|
1235
|
+
return a.buffer;
|
|
1236
|
+
};
|
|
1237
|
+
const bufToB64u = (b) => {
|
|
1238
|
+
const a = new Uint8Array(b);
|
|
1239
|
+
let s = "";
|
|
1240
|
+
for (let i = 0; i < a.length; i++)
|
|
1241
|
+
s += String.fromCharCode(a[i]);
|
|
1242
|
+
return btoa(s)
|
|
1243
|
+
.replace(/\+/g, "-")
|
|
1244
|
+
.replace(/\//g, "_")
|
|
1245
|
+
.replace(/=+$/, "");
|
|
1246
|
+
};
|
|
1247
|
+
const findForm = () => {
|
|
1248
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
1249
|
+
if (shadowRoot) {
|
|
1250
|
+
const f = shadowRoot.querySelector("form");
|
|
1251
|
+
if (f)
|
|
1252
|
+
return f;
|
|
1253
|
+
}
|
|
1254
|
+
return document.querySelector("form");
|
|
1255
|
+
};
|
|
1256
|
+
try {
|
|
1257
|
+
const publicKey = {
|
|
1258
|
+
challenge: b64uToBuf(opts.challenge),
|
|
1259
|
+
rp: { id: opts.rp.id, name: opts.rp.name },
|
|
1260
|
+
user: {
|
|
1261
|
+
id: b64uToBuf(opts.user.id),
|
|
1262
|
+
name: opts.user.name,
|
|
1263
|
+
displayName: opts.user.displayName,
|
|
1264
|
+
},
|
|
1265
|
+
pubKeyCredParams: opts.pubKeyCredParams.map((p) => ({
|
|
1266
|
+
alg: p.alg,
|
|
1267
|
+
type: p.type,
|
|
1268
|
+
})),
|
|
1269
|
+
timeout: opts.timeout,
|
|
1270
|
+
attestation: (opts.attestation || "none"),
|
|
1271
|
+
authenticatorSelection: opts.authenticatorSelection
|
|
1272
|
+
? {
|
|
1273
|
+
residentKey: (opts.authenticatorSelection.residentKey ||
|
|
1274
|
+
"preferred"),
|
|
1275
|
+
userVerification: (opts.authenticatorSelection
|
|
1276
|
+
.userVerification ||
|
|
1277
|
+
"preferred"),
|
|
1278
|
+
}
|
|
1279
|
+
: undefined,
|
|
1280
|
+
};
|
|
1281
|
+
if (opts.excludeCredentials?.length) {
|
|
1282
|
+
publicKey.excludeCredentials = opts.excludeCredentials.map((c) => ({
|
|
1283
|
+
id: b64uToBuf(c.id),
|
|
1284
|
+
type: c.type,
|
|
1285
|
+
transports: (c.transports || []),
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1288
|
+
const cred = (await navigator.credentials.create({
|
|
1289
|
+
publicKey,
|
|
1290
|
+
}));
|
|
1291
|
+
const response = cred.response;
|
|
1292
|
+
const resp = {
|
|
1293
|
+
id: cred.id,
|
|
1294
|
+
rawId: bufToB64u(cred.rawId),
|
|
1295
|
+
type: cred.type,
|
|
1296
|
+
response: {
|
|
1297
|
+
attestationObject: bufToB64u(response.attestationObject),
|
|
1298
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
1299
|
+
},
|
|
1300
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
1301
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
1302
|
+
};
|
|
1303
|
+
if (typeof response.getTransports === "function") {
|
|
1304
|
+
resp.response.transports =
|
|
1305
|
+
response.getTransports();
|
|
1306
|
+
}
|
|
1307
|
+
const form = findForm();
|
|
1308
|
+
if (form) {
|
|
1309
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
1310
|
+
form.querySelector("#credential-field");
|
|
1311
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1312
|
+
form.querySelector("#action-field");
|
|
1313
|
+
if (cf)
|
|
1314
|
+
cf.value = JSON.stringify(resp);
|
|
1315
|
+
if (af)
|
|
1316
|
+
af.value = ceremony.successAction;
|
|
1317
|
+
form.submit();
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
catch (e) {
|
|
1321
|
+
console.error("WebAuthn registration error:", e);
|
|
1322
|
+
const form = findForm();
|
|
1323
|
+
if (form) {
|
|
1324
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
1325
|
+
form.querySelector("#action-field");
|
|
1326
|
+
if (af)
|
|
1327
|
+
af.value = "error";
|
|
1328
|
+
form.submit();
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1131
1332
|
handleButtonClick = (detail) => {
|
|
1132
1333
|
// If this is a submit button click, trigger form submission
|
|
1133
1334
|
if (detail.type === "submit") {
|
|
@@ -1330,9 +1531,11 @@ const AuthheroWidget = class {
|
|
|
1330
1531
|
// Use the local screen variable for all rendering
|
|
1331
1532
|
const screenErrors = screen.messages?.filter((m) => m.type === "error") || [];
|
|
1332
1533
|
const screenSuccesses = screen.messages?.filter((m) => m.type === "success") || [];
|
|
1333
|
-
const
|
|
1534
|
+
const allComponents = [...(screen.components ?? [])];
|
|
1535
|
+
const components = allComponents
|
|
1334
1536
|
.filter((c) => c.visible !== false)
|
|
1335
1537
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1538
|
+
const hiddenComponents = allComponents.filter((c) => c.visible === false);
|
|
1336
1539
|
// Separate social, divider, and field components for layout ordering
|
|
1337
1540
|
const socialComponents = components.filter((c) => this.isSocialComponent(c));
|
|
1338
1541
|
const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
|
|
@@ -1367,7 +1570,7 @@ const AuthheroWidget = class {
|
|
|
1367
1570
|
};
|
|
1368
1571
|
// Get logo URL from theme.widget (takes precedence) or branding
|
|
1369
1572
|
const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
|
|
1370
|
-
return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
1573
|
+
return (h("div", { class: "widget-container", part: "container", "data-authstack-container": true }, h("header", { class: "widget-header", part: "header" }, logoUrl && (h("div", { class: "logo-wrapper", part: "logo-wrapper" }, h("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (h("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (h("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), h("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (h("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (h("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), h("form", { onSubmit: this.handleSubmit, part: "form" }, hiddenComponents.map((c) => (h("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), h("div", { class: "form-content" }, socialComponents.length > 0 && (h("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
1371
1574
|
fieldComponents.length > 0 &&
|
|
1372
1575
|
hasDivider && (h("div", { class: "divider", part: "divider" }, h("span", { class: "divider-text" }, dividerText))), h("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (h("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading })))))), screen.links && screen.links.length > 0 && (h("div", { class: "links", part: "links" }, screen.links.map((link) => (h("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (h("span", null, link.text, " ", h("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
|
|
1373
1576
|
id: link.id,
|
package/dist/esm/index.js
CHANGED
|
@@ -1580,20 +1580,38 @@ var decodeURIComponent_ = decodeURIComponent;
|
|
|
1580
1580
|
// src/utils/cookie.ts
|
|
1581
1581
|
var validCookieNameRegEx = /^[\w!#$%&'*.^`|~+-]+$/;
|
|
1582
1582
|
var validCookieValueRegEx = /^[ !#-:<-[\]-~]*$/;
|
|
1583
|
+
var trimCookieWhitespace = (value) => {
|
|
1584
|
+
let start = 0;
|
|
1585
|
+
let end = value.length;
|
|
1586
|
+
while (start < end) {
|
|
1587
|
+
const charCode = value.charCodeAt(start);
|
|
1588
|
+
if (charCode !== 32 && charCode !== 9) {
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
start++;
|
|
1592
|
+
}
|
|
1593
|
+
while (end > start) {
|
|
1594
|
+
const charCode = value.charCodeAt(end - 1);
|
|
1595
|
+
if (charCode !== 32 && charCode !== 9) {
|
|
1596
|
+
break;
|
|
1597
|
+
}
|
|
1598
|
+
end--;
|
|
1599
|
+
}
|
|
1600
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
1601
|
+
};
|
|
1583
1602
|
var parse = (cookie, name) => {
|
|
1584
|
-
const pairs = cookie.
|
|
1603
|
+
const pairs = cookie.split(";");
|
|
1585
1604
|
const parsedCookie = {};
|
|
1586
|
-
for (
|
|
1587
|
-
pairStr = pairStr.trim();
|
|
1605
|
+
for (const pairStr of pairs) {
|
|
1588
1606
|
const valueStartPos = pairStr.indexOf("=");
|
|
1589
1607
|
if (valueStartPos === -1) {
|
|
1590
1608
|
continue;
|
|
1591
1609
|
}
|
|
1592
|
-
const cookieName = pairStr.substring(0, valueStartPos)
|
|
1610
|
+
const cookieName = trimCookieWhitespace(pairStr.substring(0, valueStartPos));
|
|
1593
1611
|
if (!validCookieNameRegEx.test(cookieName)) {
|
|
1594
1612
|
continue;
|
|
1595
1613
|
}
|
|
1596
|
-
let cookieValue = pairStr.substring(valueStartPos + 1)
|
|
1614
|
+
let cookieValue = trimCookieWhitespace(pairStr.substring(valueStartPos + 1));
|
|
1597
1615
|
if (cookieValue.startsWith('"') && cookieValue.endsWith('"')) {
|
|
1598
1616
|
cookieValue = cookieValue.slice(1, -1);
|
|
1599
1617
|
}
|
|
@@ -260,6 +260,28 @@ export declare class AuthheroWidget {
|
|
|
260
260
|
fetchScreen(screenIdOverride?: string, nodeId?: string): Promise<boolean>;
|
|
261
261
|
private handleInputChange;
|
|
262
262
|
private handleSubmit;
|
|
263
|
+
/**
|
|
264
|
+
* Override form.submit() so that scripts (e.g. WebAuthn ceremony) that call
|
|
265
|
+
* form.submit() go through the widget's JSON fetch pipeline instead of a
|
|
266
|
+
* native form-urlencoded POST.
|
|
267
|
+
*/
|
|
268
|
+
private overrideFormSubmit;
|
|
269
|
+
/**
|
|
270
|
+
* Validate and execute a structured WebAuthn ceremony returned by the server.
|
|
271
|
+
* Instead of injecting arbitrary script content, this parses the ceremony JSON,
|
|
272
|
+
* validates the expected fields, and calls the WebAuthn API natively.
|
|
273
|
+
*/
|
|
274
|
+
private performWebAuthnCeremony;
|
|
275
|
+
/**
|
|
276
|
+
* Schema validation for WebAuthn ceremony payloads.
|
|
277
|
+
* Checks required fields and types before invoking browser APIs.
|
|
278
|
+
*/
|
|
279
|
+
private isValidWebAuthnCeremony;
|
|
280
|
+
/**
|
|
281
|
+
* Perform the WebAuthn navigator.credentials.create() ceremony and submit
|
|
282
|
+
* the credential result via the form.
|
|
283
|
+
*/
|
|
284
|
+
private executeWebAuthnRegistration;
|
|
263
285
|
private handleButtonClick;
|
|
264
286
|
/**
|
|
265
287
|
* Handle social login redirect
|
package/hydrate/index.js
CHANGED
|
@@ -6823,7 +6823,18 @@ class AuthheroWidget {
|
|
|
6823
6823
|
e.preventDefault();
|
|
6824
6824
|
if (!this._screen || this.loading)
|
|
6825
6825
|
return;
|
|
6826
|
-
|
|
6826
|
+
let submitData = { ...this.formData, ...(overrideData || {}) };
|
|
6827
|
+
// Merge hidden input values from DOM (may have been set programmatically
|
|
6828
|
+
// by inline scripts, e.g. passkey management buttons)
|
|
6829
|
+
const form = this.el.shadowRoot?.querySelector("form");
|
|
6830
|
+
if (form) {
|
|
6831
|
+
const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
|
|
6832
|
+
hiddenInputs.forEach((input) => {
|
|
6833
|
+
if (input.name && input.value) {
|
|
6834
|
+
submitData[input.name] = input.value;
|
|
6835
|
+
}
|
|
6836
|
+
});
|
|
6837
|
+
}
|
|
6827
6838
|
// Always emit the submit event
|
|
6828
6839
|
this.formSubmit.emit({
|
|
6829
6840
|
screen: this._screen,
|
|
@@ -6899,6 +6910,10 @@ class AuthheroWidget {
|
|
|
6899
6910
|
this.state = result.state;
|
|
6900
6911
|
this.persistState();
|
|
6901
6912
|
}
|
|
6913
|
+
// Perform WebAuthn ceremony if present (structured data, not script)
|
|
6914
|
+
if (result.ceremony) {
|
|
6915
|
+
this.performWebAuthnCeremony(result.ceremony);
|
|
6916
|
+
}
|
|
6902
6917
|
// Focus first input on new screen
|
|
6903
6918
|
this.focusFirstInput();
|
|
6904
6919
|
}
|
|
@@ -6932,6 +6947,192 @@ class AuthheroWidget {
|
|
|
6932
6947
|
this.loading = false;
|
|
6933
6948
|
}
|
|
6934
6949
|
};
|
|
6950
|
+
/**
|
|
6951
|
+
* Override form.submit() so that scripts (e.g. WebAuthn ceremony) that call
|
|
6952
|
+
* form.submit() go through the widget's JSON fetch pipeline instead of a
|
|
6953
|
+
* native form-urlencoded POST.
|
|
6954
|
+
*/
|
|
6955
|
+
overrideFormSubmit() {
|
|
6956
|
+
const shadowRoot = this.el.shadowRoot;
|
|
6957
|
+
if (!shadowRoot)
|
|
6958
|
+
return;
|
|
6959
|
+
const form = shadowRoot.querySelector("form");
|
|
6960
|
+
if (!form)
|
|
6961
|
+
return;
|
|
6962
|
+
form.submit = () => {
|
|
6963
|
+
const formData = new FormData(form);
|
|
6964
|
+
const data = {};
|
|
6965
|
+
formData.forEach((value, key) => {
|
|
6966
|
+
if (typeof value === "string") {
|
|
6967
|
+
data[key] = value;
|
|
6968
|
+
}
|
|
6969
|
+
});
|
|
6970
|
+
const syntheticEvent = { preventDefault: () => { } };
|
|
6971
|
+
this.handleSubmit(syntheticEvent, data);
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
/**
|
|
6975
|
+
* Validate and execute a structured WebAuthn ceremony returned by the server.
|
|
6976
|
+
* Instead of injecting arbitrary script content, this parses the ceremony JSON,
|
|
6977
|
+
* validates the expected fields, and calls the WebAuthn API natively.
|
|
6978
|
+
*/
|
|
6979
|
+
performWebAuthnCeremony(ceremony) {
|
|
6980
|
+
if (!this.isValidWebAuthnCeremony(ceremony)) {
|
|
6981
|
+
console.error("Invalid WebAuthn ceremony payload", ceremony);
|
|
6982
|
+
return;
|
|
6983
|
+
}
|
|
6984
|
+
requestAnimationFrame(() => {
|
|
6985
|
+
this.overrideFormSubmit();
|
|
6986
|
+
this.executeWebAuthnRegistration(ceremony);
|
|
6987
|
+
});
|
|
6988
|
+
}
|
|
6989
|
+
/**
|
|
6990
|
+
* Schema validation for WebAuthn ceremony payloads.
|
|
6991
|
+
* Checks required fields and types before invoking browser APIs.
|
|
6992
|
+
*/
|
|
6993
|
+
isValidWebAuthnCeremony(data) {
|
|
6994
|
+
if (typeof data !== "object" || data === null)
|
|
6995
|
+
return false;
|
|
6996
|
+
const obj = data;
|
|
6997
|
+
if (obj.type !== "webauthn-registration")
|
|
6998
|
+
return false;
|
|
6999
|
+
if (typeof obj.successAction !== "string")
|
|
7000
|
+
return false;
|
|
7001
|
+
const opts = obj.options;
|
|
7002
|
+
if (typeof opts !== "object" || opts === null)
|
|
7003
|
+
return false;
|
|
7004
|
+
const o = opts;
|
|
7005
|
+
if (typeof o.challenge !== "string")
|
|
7006
|
+
return false;
|
|
7007
|
+
const rp = o.rp;
|
|
7008
|
+
if (typeof rp !== "object" || rp === null)
|
|
7009
|
+
return false;
|
|
7010
|
+
if (typeof rp.id !== "string" ||
|
|
7011
|
+
typeof rp.name !== "string")
|
|
7012
|
+
return false;
|
|
7013
|
+
const user = o.user;
|
|
7014
|
+
if (typeof user !== "object" || user === null)
|
|
7015
|
+
return false;
|
|
7016
|
+
const u = user;
|
|
7017
|
+
if (typeof u.id !== "string" ||
|
|
7018
|
+
typeof u.name !== "string" ||
|
|
7019
|
+
typeof u.displayName !== "string")
|
|
7020
|
+
return false;
|
|
7021
|
+
if (!Array.isArray(o.pubKeyCredParams))
|
|
7022
|
+
return false;
|
|
7023
|
+
return true;
|
|
7024
|
+
}
|
|
7025
|
+
/**
|
|
7026
|
+
* Perform the WebAuthn navigator.credentials.create() ceremony and submit
|
|
7027
|
+
* the credential result via the form.
|
|
7028
|
+
*/
|
|
7029
|
+
async executeWebAuthnRegistration(ceremony) {
|
|
7030
|
+
const opts = ceremony.options;
|
|
7031
|
+
const b64uToBuf = (s) => {
|
|
7032
|
+
s = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
7033
|
+
while (s.length % 4)
|
|
7034
|
+
s += "=";
|
|
7035
|
+
const b = atob(s);
|
|
7036
|
+
const a = new Uint8Array(b.length);
|
|
7037
|
+
for (let i = 0; i < b.length; i++)
|
|
7038
|
+
a[i] = b.charCodeAt(i);
|
|
7039
|
+
return a.buffer;
|
|
7040
|
+
};
|
|
7041
|
+
const bufToB64u = (b) => {
|
|
7042
|
+
const a = new Uint8Array(b);
|
|
7043
|
+
let s = "";
|
|
7044
|
+
for (let i = 0; i < a.length; i++)
|
|
7045
|
+
s += String.fromCharCode(a[i]);
|
|
7046
|
+
return btoa(s)
|
|
7047
|
+
.replace(/\+/g, "-")
|
|
7048
|
+
.replace(/\//g, "_")
|
|
7049
|
+
.replace(/=+$/, "");
|
|
7050
|
+
};
|
|
7051
|
+
const findForm = () => {
|
|
7052
|
+
const shadowRoot = this.el?.shadowRoot;
|
|
7053
|
+
if (shadowRoot) {
|
|
7054
|
+
const f = shadowRoot.querySelector("form");
|
|
7055
|
+
if (f)
|
|
7056
|
+
return f;
|
|
7057
|
+
}
|
|
7058
|
+
return document.querySelector("form");
|
|
7059
|
+
};
|
|
7060
|
+
try {
|
|
7061
|
+
const publicKey = {
|
|
7062
|
+
challenge: b64uToBuf(opts.challenge),
|
|
7063
|
+
rp: { id: opts.rp.id, name: opts.rp.name },
|
|
7064
|
+
user: {
|
|
7065
|
+
id: b64uToBuf(opts.user.id),
|
|
7066
|
+
name: opts.user.name,
|
|
7067
|
+
displayName: opts.user.displayName,
|
|
7068
|
+
},
|
|
7069
|
+
pubKeyCredParams: opts.pubKeyCredParams.map((p) => ({
|
|
7070
|
+
alg: p.alg,
|
|
7071
|
+
type: p.type,
|
|
7072
|
+
})),
|
|
7073
|
+
timeout: opts.timeout,
|
|
7074
|
+
attestation: (opts.attestation || "none"),
|
|
7075
|
+
authenticatorSelection: opts.authenticatorSelection
|
|
7076
|
+
? {
|
|
7077
|
+
residentKey: (opts.authenticatorSelection.residentKey ||
|
|
7078
|
+
"preferred"),
|
|
7079
|
+
userVerification: (opts.authenticatorSelection
|
|
7080
|
+
.userVerification ||
|
|
7081
|
+
"preferred"),
|
|
7082
|
+
}
|
|
7083
|
+
: undefined,
|
|
7084
|
+
};
|
|
7085
|
+
if (opts.excludeCredentials?.length) {
|
|
7086
|
+
publicKey.excludeCredentials = opts.excludeCredentials.map((c) => ({
|
|
7087
|
+
id: b64uToBuf(c.id),
|
|
7088
|
+
type: c.type,
|
|
7089
|
+
transports: (c.transports || []),
|
|
7090
|
+
}));
|
|
7091
|
+
}
|
|
7092
|
+
const cred = (await navigator.credentials.create({
|
|
7093
|
+
publicKey,
|
|
7094
|
+
}));
|
|
7095
|
+
const response = cred.response;
|
|
7096
|
+
const resp = {
|
|
7097
|
+
id: cred.id,
|
|
7098
|
+
rawId: bufToB64u(cred.rawId),
|
|
7099
|
+
type: cred.type,
|
|
7100
|
+
response: {
|
|
7101
|
+
attestationObject: bufToB64u(response.attestationObject),
|
|
7102
|
+
clientDataJSON: bufToB64u(response.clientDataJSON),
|
|
7103
|
+
},
|
|
7104
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
7105
|
+
authenticatorAttachment: cred.authenticatorAttachment || undefined,
|
|
7106
|
+
};
|
|
7107
|
+
if (typeof response.getTransports === "function") {
|
|
7108
|
+
resp.response.transports =
|
|
7109
|
+
response.getTransports();
|
|
7110
|
+
}
|
|
7111
|
+
const form = findForm();
|
|
7112
|
+
if (form) {
|
|
7113
|
+
const cf = form.querySelector('[name="credential-field"]') ||
|
|
7114
|
+
form.querySelector("#credential-field");
|
|
7115
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
7116
|
+
form.querySelector("#action-field");
|
|
7117
|
+
if (cf)
|
|
7118
|
+
cf.value = JSON.stringify(resp);
|
|
7119
|
+
if (af)
|
|
7120
|
+
af.value = ceremony.successAction;
|
|
7121
|
+
form.submit();
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
catch (e) {
|
|
7125
|
+
console.error("WebAuthn registration error:", e);
|
|
7126
|
+
const form = findForm();
|
|
7127
|
+
if (form) {
|
|
7128
|
+
const af = form.querySelector('[name="action-field"]') ||
|
|
7129
|
+
form.querySelector("#action-field");
|
|
7130
|
+
if (af)
|
|
7131
|
+
af.value = "error";
|
|
7132
|
+
form.submit();
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
6935
7136
|
handleButtonClick = (detail) => {
|
|
6936
7137
|
// If this is a submit button click, trigger form submission
|
|
6937
7138
|
if (detail.type === "submit") {
|
|
@@ -7134,9 +7335,11 @@ class AuthheroWidget {
|
|
|
7134
7335
|
// Use the local screen variable for all rendering
|
|
7135
7336
|
const screenErrors = screen.messages?.filter((m) => m.type === "error") || [];
|
|
7136
7337
|
const screenSuccesses = screen.messages?.filter((m) => m.type === "success") || [];
|
|
7137
|
-
const
|
|
7338
|
+
const allComponents = [...(screen.components ?? [])];
|
|
7339
|
+
const components = allComponents
|
|
7138
7340
|
.filter((c) => c.visible !== false)
|
|
7139
7341
|
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
7342
|
+
const hiddenComponents = allComponents.filter((c) => c.visible === false);
|
|
7140
7343
|
// Separate social, divider, and field components for layout ordering
|
|
7141
7344
|
const socialComponents = components.filter((c) => this.isSocialComponent(c));
|
|
7142
7345
|
const fieldComponents = components.filter((c) => !this.isSocialComponent(c) && !this.isDividerComponent(c));
|
|
@@ -7171,7 +7374,7 @@ class AuthheroWidget {
|
|
|
7171
7374
|
};
|
|
7172
7375
|
// Get logo URL from theme.widget (takes precedence) or branding
|
|
7173
7376
|
const logoUrl = this._theme?.widget?.logo_url || this._branding?.logo_url;
|
|
7174
|
-
return (hAsync("div", { class: "widget-container", part: "container", "data-authstack-container": true }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (hAsync("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (hAsync("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, part: "form" }, hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
7377
|
+
return (hAsync("div", { class: "widget-container", part: "container", "data-authstack-container": true }, hAsync("header", { class: "widget-header", part: "header" }, logoUrl && (hAsync("div", { class: "logo-wrapper", part: "logo-wrapper" }, hAsync("img", { class: "logo", part: "logo", src: logoUrl, alt: "Logo" }))), screen.title && (hAsync("h1", { class: "title", part: "title", innerHTML: sanitizeHtml(screen.title) })), screen.description && (hAsync("p", { class: "description", part: "description", innerHTML: sanitizeHtml(screen.description) }))), hAsync("div", { class: "widget-body", part: "body" }, screenErrors.map((err) => (hAsync("div", { class: "message message-error", part: "message message-error", key: err.id ?? err.text }, err.text))), screenSuccesses.map((msg) => (hAsync("div", { class: "message message-success", part: "message message-success", key: msg.id ?? msg.text }, msg.text))), hAsync("form", { onSubmit: this.handleSubmit, part: "form" }, hiddenComponents.map((c) => (hAsync("input", { type: "hidden", name: c.id, id: c.id, key: c.id, value: this.formData[c.id] || "" }))), hAsync("div", { class: "form-content" }, socialComponents.length > 0 && (hAsync("div", { class: "social-section", part: "social-section" }, socialComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading, exportparts: getExportParts(component) }))))), socialComponents.length > 0 &&
|
|
7175
7378
|
fieldComponents.length > 0 &&
|
|
7176
7379
|
hasDivider && (hAsync("div", { class: "divider", part: "divider" }, hAsync("span", { class: "divider-text" }, dividerText))), hAsync("div", { class: "fields-section", part: "fields-section" }, fieldComponents.map((component) => (hAsync("authhero-node", { key: component.id, component: component, value: this.formData[component.id], onFieldChange: (e) => this.handleInputChange(e.detail.id, e.detail.value), onButtonClick: (e) => this.handleButtonClick(e.detail), disabled: this.loading })))))), screen.links && screen.links.length > 0 && (hAsync("div", { class: "links", part: "links" }, screen.links.map((link) => (hAsync("span", { class: "link-wrapper", part: "link-wrapper", key: link.id ?? link.href }, link.linkText ? (hAsync("span", null, link.text, " ", hAsync("a", { href: link.href, class: "link", part: "link", onClick: (e) => this.handleLinkClick(e, {
|
|
7177
7380
|
id: link.id,
|