fix(auth): show one terminal MFA expiry state
This commit is contained in:
1 parent
0c77de93b4
commit
d0e0dfeb26
5 files changed
+26
-10
No files matched your search
@@ -235,7 +235,7 @@
|
|||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.23",
|
"version": "0.8.24",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
|
|||||||
@@ -424,11 +424,16 @@
|
|||||||
form?.querySelectorAll("input, button").forEach((control) => {
|
form?.querySelectorAll("input, button").forEach((control) => {
|
||||||
control.disabled = true;
|
control.disabled = true;
|
||||||
});
|
});
|
||||||
setMessage(
|
// A form response's finalizer or hydration may re-enable buttons. Hide the
|
||||||
status,
|
// entire stale form instead of leaving a second apparently usable action.
|
||||||
"Your sign-in request has expired. Sign in again to get a fresh verification request.",
|
element.dataset.mfaExpired = "true";
|
||||||
true,
|
if (form) form.hidden = true;
|
||||||
);
|
setMessage(status, "");
|
||||||
|
form?.querySelectorAll("[data-error]").forEach((error) => {
|
||||||
|
error.textContent = "";
|
||||||
|
});
|
||||||
|
const expiredMessage = element.querySelector("[data-auth-mfa-expired]");
|
||||||
|
if (expiredMessage) expiredMessage.hidden = false;
|
||||||
const restart = element.querySelector("[data-auth-mfa-restart]");
|
const restart = element.querySelector("[data-auth-mfa-restart]");
|
||||||
if (restart) restart.hidden = false;
|
if (restart) restart.hidden = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ component TwoFactorChallenge {
|
|||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
</form>
|
</form>
|
||||||
|
<p data-auth-mfa-expired role="alert" hidden class="wrn-auth-mfa-expired">Your sign-in request has expired. Sign in again to get a fresh verification request.</p>
|
||||||
<nav class="wrn-auth-mfa-links" aria-label="Sign-in help">
|
<nav class="wrn-auth-mfa-links" aria-label="Sign-in help">
|
||||||
<a href='{signInHref}' data-auth-mfa-restart hidden>Back to sign in</a>
|
<a href='{signInHref}' data-auth-mfa-restart hidden>Back to sign in</a>
|
||||||
<a href='{recoveryHref}'>Account recovery options</a>
|
<a href='{recoveryHref}'>Account recovery options</a>
|
||||||
@@ -58,6 +59,8 @@ component TwoFactorChallenge {
|
|||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
style {
|
style {
|
||||||
|
[data-auth-mfa-challenge][data-mfa-expired="true"] [data-auth-mfa-form] { display: none; }
|
||||||
|
.wrn-auth-mfa-expired { margin: 1.5rem 0 0; padding: 1rem; border: 1px solid var(--wrn-color-danger); border-radius: var(--wrn-radius-sm); background: color-mix(in srgb, var(--wrn-color-danger) 10%, transparent); color: var(--wrn-color-danger); font-size: 0.875rem; line-height: 1.5; }
|
||||||
.wrn-auth-mfa-links { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; margin-top: 1.25rem; }
|
.wrn-auth-mfa-links { display: flex; flex-direction: column; align-items: flex-start; gap: 0.75rem; margin-top: 1.25rem; }
|
||||||
.wrn-auth-mfa-links a { color: var(--wrn-color-primary); font-size: 0.875rem; }
|
.wrn-auth-mfa-links a { color: var(--wrn-color-primary); font-size: 0.875rem; }
|
||||||
.wrn-auth-mfa-links a:hover { text-decoration: underline; }
|
.wrn-auth-mfa-links a:hover { text-decoration: underline; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.23",
|
"version": "0.8.24",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ test("expired and legacy MFA requests are rejected on mount before entering a co
|
|||||||
);
|
);
|
||||||
window.document.body.innerHTML = `<section data-auth-mfa-challenge><form data-auth-mfa-form>
|
window.document.body.innerHTML = `<section data-auth-mfa-challenge><form data-auth-mfa-form>
|
||||||
<input name="mfaToken"><input type="radio" name="method" value="totp"><input name="code"><p data-auth-mfa-status hidden></p><button type="submit">Verify</button>
|
<input name="mfaToken"><input type="radio" name="method" value="totp"><input name="code"><p data-auth-mfa-status hidden></p><button type="submit">Verify</button>
|
||||||
</form><a data-auth-mfa-restart hidden href="/login">Back to sign in</a></section>`;
|
</form><p data-auth-mfa-expired role="alert" hidden>Sign in again</p><a data-auth-mfa-restart hidden href="/login">Back to sign in</a></section>`;
|
||||||
window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8"));
|
window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8"));
|
||||||
(window as any).WRNexusAuth.mount(window.document);
|
(window as any).WRNexusAuth.mount(window.document);
|
||||||
expect(window.document.querySelector("button")!.disabled).toBe(true);
|
expect(window.document.querySelector("button")!.disabled).toBe(true);
|
||||||
expect(window.document.querySelector<HTMLElement>("[data-auth-mfa-restart]")!.hidden).toBe(
|
expect(window.document.querySelector<HTMLElement>("[data-auth-mfa-restart]")!.hidden).toBe(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
expect(window.document.querySelector("[data-auth-mfa-status]")!.textContent).toContain(
|
expect(window.document.querySelector<HTMLElement>("[data-auth-mfa-expired]")!.hidden).toBe(
|
||||||
"Sign in again",
|
false,
|
||||||
);
|
);
|
||||||
|
expect(window.document.querySelector("form")!.hidden).toBe(true);
|
||||||
|
expect(window.document.querySelector("[data-auth-mfa-status]")!.textContent).toBe("");
|
||||||
expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull();
|
expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull();
|
||||||
} finally {
|
} finally {
|
||||||
await window.happyDOM.close();
|
await window.happyDOM.close();
|
||||||
@@ -121,6 +123,12 @@ test("MFA radios preserve allowed methods and restore continuation without reviv
|
|||||||
detail: { code: "mfa-transaction-expired" },
|
detail: { code: "mfa-transaction-expired" },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
// Validation finalizers may restore button.disabled after dispatching error.
|
||||||
|
form.querySelector("button")!.disabled = false;
|
||||||
|
expect(form.hidden).toBe(true);
|
||||||
|
expect(
|
||||||
|
window.document.querySelector<HTMLElement>("[data-auth-mfa-challenge]")!.dataset.mfaExpired,
|
||||||
|
).toBe("true");
|
||||||
expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull();
|
expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull();
|
||||||
expect(window.document.querySelector<HTMLElement>("[data-auth-mfa-restart]")!.hidden).toBe(
|
expect(window.document.querySelector<HTMLElement>("[data-auth-mfa-restart]")!.hidden).toBe(
|
||||||
false,
|
false,
|
||||||
|
|||||||
Reference in new issue
Block a user