@structured-id/ui-core 4.15.1 → 4.15.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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/utils/rpcRefusal.test.ts +37 -0
- package/src/utils/rpcRefusal.ts +19 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@structured-id/proto/google/rpc/error_details";
|
|
11
11
|
import { ErrorReason } from "@structured-id/proto/sid/v1/common/errors";
|
|
12
12
|
import {
|
|
13
|
+
MFA_CONTINUATION_TEXTS,
|
|
13
14
|
REFUSAL_TEXTS,
|
|
14
15
|
refusalMessage,
|
|
15
16
|
rpcReason,
|
|
@@ -195,6 +196,42 @@ describe("rpcRefusal", () => {
|
|
|
195
196
|
expect(rpcRefusal(refused([info]))?.fieldViolations).toEqual([]);
|
|
196
197
|
});
|
|
197
198
|
|
|
199
|
+
// Bug prevented: every MFA_REQUIRED read "Set up a second factor first.",
|
|
200
|
+
// so a session that was only too old was told to set up a factor it may
|
|
201
|
+
// already hold, and that setup was refused the same way. The text follows
|
|
202
|
+
// what the server asks for in `continuation`.
|
|
203
|
+
it("says what an MFA_REQUIRED refusal asks for", () => {
|
|
204
|
+
const mfa = (continuation?: string) =>
|
|
205
|
+
refused(
|
|
206
|
+
[
|
|
207
|
+
Any.pack(
|
|
208
|
+
ErrorInfo.create({
|
|
209
|
+
reason: "MFA_REQUIRED",
|
|
210
|
+
domain: "structured.id",
|
|
211
|
+
metadata: continuation ? { continuation } : {},
|
|
212
|
+
}),
|
|
213
|
+
ErrorInfo,
|
|
214
|
+
),
|
|
215
|
+
],
|
|
216
|
+
"a%20stronger%20authentication%20is%20required",
|
|
217
|
+
);
|
|
218
|
+
expect(refusalMessage(mfa("reauthenticate"))).toBe(
|
|
219
|
+
MFA_CONTINUATION_TEXTS.reauthenticate,
|
|
220
|
+
);
|
|
221
|
+
expect(refusalMessage(mfa("step_up"))).toBe(MFA_CONTINUATION_TEXTS.step_up);
|
|
222
|
+
expect(refusalMessage(mfa("authenticate"))).toBe(
|
|
223
|
+
MFA_CONTINUATION_TEXTS.authenticate,
|
|
224
|
+
);
|
|
225
|
+
expect(refusalMessage(mfa())).toBe(REFUSAL_TEXTS[ErrorReason.MFA_REQUIRED]);
|
|
226
|
+
// A continuation this client does not know keeps the reason's text.
|
|
227
|
+
expect(refusalMessage(mfa("later"))).toBe(
|
|
228
|
+
REFUSAL_TEXTS[ErrorReason.MFA_REQUIRED],
|
|
229
|
+
);
|
|
230
|
+
expect(MFA_CONTINUATION_TEXTS.reauthenticate).not.toBe(
|
|
231
|
+
REFUSAL_TEXTS[ErrorReason.MFA_REQUIRED],
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
198
235
|
it("survives an unreadable trailer", () => {
|
|
199
236
|
const err = new RpcError("m", "INTERNAL", {
|
|
200
237
|
"grpc-status-details-bin": "!!!",
|
package/src/utils/rpcRefusal.ts
CHANGED
|
@@ -231,6 +231,17 @@ export const REFUSAL_TEXTS: Readonly<
|
|
|
231
231
|
[ErrorReason.INTERNAL_ERROR]: "Something went wrong on the server.",
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* What an `MFA_REQUIRED` refusal asks of the user, by the `continuation` its
|
|
236
|
+
* `ErrorInfo` names. Without one (no second factor enrolled) the reason's own
|
|
237
|
+
* text stands.
|
|
238
|
+
*/
|
|
239
|
+
export const MFA_CONTINUATION_TEXTS = {
|
|
240
|
+
authenticate: "Sign in to your account first.",
|
|
241
|
+
step_up: "Confirm it's you with your second factor to continue.",
|
|
242
|
+
reauthenticate: "Sign in again to make this change.",
|
|
243
|
+
} as const;
|
|
244
|
+
|
|
234
245
|
/**
|
|
235
246
|
* A password sign-in refused on this device: it did not verify (RFC 9807
|
|
236
247
|
* §6.4). It reads as the server's own refusal of a sign-in, so the text does
|
|
@@ -259,6 +270,14 @@ export function refusalMessage(
|
|
|
259
270
|
const refusal = rpcRefusal(err);
|
|
260
271
|
if (refusal) {
|
|
261
272
|
if (refusal.localized) return refusal.localized;
|
|
273
|
+
if (refusal.reasonCode === ErrorReason.MFA_REQUIRED) {
|
|
274
|
+
const asked = refusal.metadata["continuation"];
|
|
275
|
+
if (asked && Object.hasOwn(MFA_CONTINUATION_TEXTS, asked)) {
|
|
276
|
+
return MFA_CONTINUATION_TEXTS[
|
|
277
|
+
asked as keyof typeof MFA_CONTINUATION_TEXTS
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
262
281
|
if (
|
|
263
282
|
refusal.reasonCode !== null &&
|
|
264
283
|
refusal.reasonCode !== ErrorReason.ERROR_REASON_UNSPECIFIED
|