@structured-id/ui-core 4.15.0 → 4.15.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/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type PasswordOperationProgress,
|
|
8
8
|
WrongCurrentPasswordError,
|
|
9
9
|
} from "./useAuth";
|
|
10
|
+
import { SignInRefusedError } from "../../utils/rpcRefusal";
|
|
10
11
|
|
|
11
12
|
// Mock the gRPC wrappers
|
|
12
13
|
vi.mock("./useAuthApi", () => ({
|
|
@@ -747,14 +748,26 @@ describe("createAuth login", () => {
|
|
|
747
748
|
expect(result.pending).toEqual(pending);
|
|
748
749
|
});
|
|
749
750
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
);
|
|
751
|
+
// A wrong password showed the client's own "invalid login" to the user; it
|
|
752
|
+
// is the sign-in's refusal, and nothing more is sent.
|
|
753
|
+
it("reports a wrong password as a refused sign-in", async () => {
|
|
754
|
+
const refused = new Error("invalid login");
|
|
755
|
+
refused.name = "ZkppInvalidLoginError";
|
|
756
|
+
vi.mocked(client.loginFinish).mockRejectedValue(refused);
|
|
754
757
|
const { login } = createAuth(loader);
|
|
755
|
-
await
|
|
756
|
-
|
|
757
|
-
);
|
|
758
|
+
const failure = await login("alice@test.com", "wrong").catch((e) => e);
|
|
759
|
+
expect(failure).toBeInstanceOf(SignInRefusedError);
|
|
760
|
+
expect((failure as SignInRefusedError).cause).toBe(refused);
|
|
761
|
+
expect(api.loginFinish).not.toHaveBeenCalled();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// Only the client's refusal judged the password: a malformed response or
|
|
765
|
+
// another fault comes through as itself.
|
|
766
|
+
it("reports a sign-in failure other than a refusal as itself", async () => {
|
|
767
|
+
const fault = new Error("response: invalid length");
|
|
768
|
+
vi.mocked(client.loginFinish).mockRejectedValue(fault);
|
|
769
|
+
const { login } = createAuth(loader);
|
|
770
|
+
await expect(login("alice@test.com", "pw")).rejects.toBe(fault);
|
|
758
771
|
expect(api.loginFinish).not.toHaveBeenCalled();
|
|
759
772
|
});
|
|
760
773
|
});
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { CredentialId, SessionId } from "@structured-id/ids";
|
|
18
18
|
import type { PendingSignIn } from "../pendingSignIn";
|
|
19
|
+
import { SignInRefusedError } from "../../utils/rpcRefusal";
|
|
19
20
|
import {
|
|
20
21
|
completePasswordReset,
|
|
21
22
|
evaluatePasswordHistory,
|
|
@@ -533,12 +534,20 @@ export function createAuth(loadClient: ZkppClientLoader) {
|
|
|
533
534
|
const started = await opaqueLoginStart(identifier, start.request);
|
|
534
535
|
onStep?.(3);
|
|
535
536
|
// An ordinary sign-in runs under the empty context.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
537
|
+
let finalization: Uint8Array;
|
|
538
|
+
try {
|
|
539
|
+
finalization = await c.loginFinish(
|
|
540
|
+
password,
|
|
541
|
+
start.state,
|
|
542
|
+
started.credentialResponse,
|
|
543
|
+
new Uint8Array(),
|
|
544
|
+
);
|
|
545
|
+
} catch (e) {
|
|
546
|
+
// The client's refusal is the sign-in's answer; any other failure (a
|
|
547
|
+
// stopped client, a malformed response) is itself.
|
|
548
|
+
if (isRefusedSignIn(e)) throw new SignInRefusedError(e);
|
|
549
|
+
throw e;
|
|
550
|
+
}
|
|
542
551
|
const finished = await opaqueLoginFinish(
|
|
543
552
|
identifier,
|
|
544
553
|
finalization,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
refusalMessage,
|
|
15
15
|
rpcReason,
|
|
16
16
|
rpcRefusal,
|
|
17
|
+
SignInRefusedError,
|
|
17
18
|
} from "./rpcRefusal";
|
|
18
19
|
|
|
19
20
|
/** A refused call as the gRPC-Web transport raises it: the status in the trailer. */
|
|
@@ -143,6 +144,16 @@ describe("rpcRefusal", () => {
|
|
|
143
144
|
);
|
|
144
145
|
});
|
|
145
146
|
|
|
147
|
+
// A password the device refused reads as the server's refusal of a sign-in,
|
|
148
|
+
// never as the client's own wording, so a wrong password and an unknown
|
|
149
|
+
// account look alike.
|
|
150
|
+
it("shows a refused sign-in as the server's sign-in refusal", () => {
|
|
151
|
+
const refused = new Error("invalid login");
|
|
152
|
+
expect(
|
|
153
|
+
refusalMessage(new SignInRefusedError(refused), "Login failed"),
|
|
154
|
+
).toBe(REFUSAL_TEXTS[ErrorReason.AUTHENTICATION_FAILED]);
|
|
155
|
+
});
|
|
156
|
+
|
|
146
157
|
// A refusal that names the fields it concerns lets the form show each
|
|
147
158
|
// text under its field: the server's text for people when it sent one,
|
|
148
159
|
// else its description.
|
package/src/utils/rpcRefusal.ts
CHANGED
|
@@ -231,15 +231,31 @@ export const REFUSAL_TEXTS: Readonly<
|
|
|
231
231
|
[ErrorReason.INTERNAL_ERROR]: "Something went wrong on the server.",
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* A password sign-in refused on this device: it did not verify (RFC 9807
|
|
236
|
+
* §6.4). It reads as the server's own refusal of a sign-in, so the text does
|
|
237
|
+
* not tell a wrong password from an unknown account.
|
|
238
|
+
*/
|
|
239
|
+
export class SignInRefusedError extends Error {
|
|
240
|
+
constructor(cause: unknown) {
|
|
241
|
+
super("The sign-in did not verify", { cause });
|
|
242
|
+
this.name = "SignInRefusedError";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
234
246
|
/**
|
|
235
247
|
* The text to show for a failed call: the server's text for people, else the
|
|
236
248
|
* client's text for SID's reason, else `fallback`, the caller's context. A
|
|
237
|
-
* client-side `Error` (no call refused) keeps its own message
|
|
249
|
+
* client-side `Error` (no call refused) keeps its own message, except a
|
|
250
|
+
* refused sign-in, which reads as the server's refusal of one.
|
|
238
251
|
*/
|
|
239
252
|
export function refusalMessage(
|
|
240
253
|
err: unknown,
|
|
241
254
|
fallback = "Request failed",
|
|
242
255
|
): string {
|
|
256
|
+
if (err instanceof SignInRefusedError) {
|
|
257
|
+
return REFUSAL_TEXTS[ErrorReason.AUTHENTICATION_FAILED];
|
|
258
|
+
}
|
|
243
259
|
const refusal = rpcRefusal(err);
|
|
244
260
|
if (refusal) {
|
|
245
261
|
if (refusal.localized) return refusal.localized;
|