Skip to content

Commit 474680a

Browse files
committed
improve: passkey multi flow signal control
1 parent 7ae0134 commit 474680a

File tree

2 files changed

+95
-58
lines changed

2 files changed

+95
-58
lines changed

web/src/components/auth/LoginForm.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC } from "react";
1+
import { FC, useRef } from "react";
22
import { createUseQuery } from "@hooks/useQuery";
33
import useMount from "@hooks/useMount";
44
import { useNavigate } from "react-router";
@@ -77,25 +77,33 @@ export const LoginForm: FC = () => {
7777
return !!window.PublicKeyCredential?.isConditionalMediationAvailable?.();
7878
};
7979

80+
const refPasskey = useRef<null | AbortController>(null);
8081
const onPasskeyLogin = async (conditional?: boolean) => {
8182
if (!isWebauthnAvailable()) {
8283
if (!conditional) toast.error("当前浏览器不支持 webauthn");
83-
return
84+
return;
8485
}
8586
if (conditional && !isWebauthnConditionalAvailable()) {
86-
return
87+
return;
8788
}
8889

90+
if (refPasskey.current) refPasskey.current.abort();
91+
const controller = new AbortController();
92+
refPasskey.current = controller;
93+
8994
try {
9095
const {
9196
data: { data: options },
92-
} = await apiV1.get("public/login/passkey/");
97+
} = await apiV1.get("public/login/passkey/", {
98+
signal: controller.signal,
99+
});
93100
options.publicKey.challenge = coerceToArrayBuffer(
94101
options.publicKey.challenge,
95102
);
96103
const credential = await navigator.credentials.get({
97104
...options,
98105
mediation: conditional && "conditional",
106+
signal: controller.signal,
99107
});
100108
if (credential?.type !== "public-key") {
101109
toast.error(`获取凭据失败,凭据类型不正确: ${credential?.type}`);
@@ -115,14 +123,24 @@ export const LoginForm: FC = () => {
115123
type: pubKeyCred.type,
116124
},
117125
},
126+
{
127+
signal: controller.signal,
128+
},
118129
);
119130
window.open(data.callback, "_self");
120131
} catch (err: any) {
121132
if (err instanceof AxiosError) {
122133
err = err as ApiError<void>;
123134
if (err.msg) toast.error(err.msg);
124135
} else {
125-
if (err.name != "NotAllowedError") toast.error(`创建凭据失败: ${err}`);
136+
switch (err.name) {
137+
case "AbortError":
138+
case "NotAllowedError":
139+
console.log("Passkey bypassed error", err)
140+
break;
141+
default:
142+
toast.error(`Passkey 认证失败: ${err}`);
143+
}
126144
}
127145
}
128146
};

web/src/components/user/U2fDialog.tsx

Lines changed: 72 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useCallback, useEffect, useState } from "react";
1+
import { FC, useCallback, useEffect, useRef, useState } from "react";
22
import useInterval from "@hooks/useInterval";
33
import useTimeout from "@hooks/useTimeout";
44
import useKeyDown from "@hooks/useKeyDown";
@@ -96,65 +96,76 @@ const U2fDialog: FC = () => {
9696
if (states.reject) states.reject("user canceled");
9797
};
9898

99+
const refPasskey = useRef<null | AbortController>(null);
100+
99101
const onSubmit = async (method: string = tabValue) => {
100102
if (tokenAvailable) {
101103
const states = useU2fDialog.getState();
102104
if (states.resolve) states.resolve(u2fToken!);
103105
states.closeDialog();
104106
return;
105107
}
106-
let data: any;
107-
switch (method) {
108-
case "phone":
109-
if (smsCode === "") {
110-
toast.error("验证码不能为空");
111-
return;
112-
}
113-
if (smsCode.length != 5) {
114-
toast.error("短信验证码有误");
115-
return;
116-
}
117-
data = { code: smsCode };
118-
break;
119-
case "mfa":
120-
if (mfaCode === "") {
121-
toast.error("校验码不能为空");
122-
return;
123-
}
124-
if (mfaCode.length != 6) {
125-
toast.error("校验码有误");
126-
return;
127-
}
128-
data = { code: mfaCode };
129-
break;
130-
case "passkey":
131-
const {
132-
data: { data: options },
133-
} = await apiV1User.get("passkey/options");
134-
options.publicKey.challenge = coerceToArrayBuffer(
135-
options.publicKey.challenge,
136-
);
137-
options.publicKey.allowCredentials =
138-
options.publicKey.allowCredentials.map((cred: any) => {
139-
cred.id = coerceToArrayBuffer(cred.id);
140-
return cred;
141-
});
142-
const credential = await navigator.credentials.get(options);
143-
if (credential?.type !== "public-key") {
144-
toast.error(`获取凭据失败,凭据类型不正确: ${credential?.type}`);
145-
return;
146-
}
147-
const pubKeyCred = credential as any;
148-
data = {
149-
id: pubKeyCred.id,
150-
rawId: coerceToBase64Url(pubKeyCred.rawId),
151-
response: coerceResponseToBase64Url(pubKeyCred.response),
152-
type: pubKeyCred.type,
153-
};
154-
break;
155-
}
156-
setIsLoading(true);
157108
try {
109+
setIsLoading(true);
110+
let data: any;
111+
switch (method) {
112+
case "phone":
113+
if (smsCode === "") {
114+
toast.error("验证码不能为空");
115+
break;
116+
}
117+
if (smsCode.length != 5) {
118+
toast.error("短信验证码有误");
119+
break;
120+
}
121+
data = { code: smsCode };
122+
break;
123+
case "mfa":
124+
if (mfaCode === "") {
125+
toast.error("校验码不能为空");
126+
break;
127+
}
128+
if (mfaCode.length != 6) {
129+
toast.error("校验码有误");
130+
break;
131+
}
132+
data = { code: mfaCode };
133+
break;
134+
case "passkey":
135+
if (refPasskey.current) refPasskey.current.abort();
136+
const controller = new AbortController();
137+
refPasskey.current = controller;
138+
const {
139+
data: { data: options },
140+
} = await apiV1User.get("passkey/options", {
141+
signal: controller.signal,
142+
});
143+
options.publicKey.challenge = coerceToArrayBuffer(
144+
options.publicKey.challenge,
145+
);
146+
options.publicKey.allowCredentials =
147+
options.publicKey.allowCredentials.map((cred: any) => {
148+
cred.id = coerceToArrayBuffer(cred.id);
149+
return cred;
150+
});
151+
const credential = await navigator.credentials.get({
152+
...options,
153+
signal: controller.signal,
154+
});
155+
if (credential?.type !== "public-key") {
156+
toast.error(`获取凭据失败,凭据类型不正确: ${credential?.type}`);
157+
break;
158+
}
159+
const pubKeyCred = credential as any;
160+
data = {
161+
id: pubKeyCred.id,
162+
rawId: coerceToBase64Url(pubKeyCred.rawId),
163+
response: coerceResponseToBase64Url(pubKeyCred.response),
164+
type: pubKeyCred.type,
165+
};
166+
break;
167+
}
168+
if (!data) return;
158169
const {
159170
data: { data: result },
160171
} = await apiV1User.post<{ data: User.U2F.Result }>(
@@ -166,7 +177,15 @@ const U2fDialog: FC = () => {
166177
if (states.resolve) states.resolve(result);
167178
states.closeDialog();
168179
} catch (err) {
169-
if (err instanceof Error) toast.error(err.message);
180+
if (err instanceof Error) {
181+
switch (err.name) {
182+
case "AbortError":
183+
console.log("U2F bypass error", err);
184+
break;
185+
default:
186+
toast.error(err.message);
187+
}
188+
}
170189
}
171190
setIsLoading(false);
172191
};

0 commit comments

Comments
 (0)