Skip to content

Commit

Permalink
Add interstitial step before retrying PRF
Browse files Browse the repository at this point in the history
  • Loading branch information
emlun committed Sep 22, 2023
1 parent 095e69a commit 43d30d1
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 27 deletions.
84 changes: 72 additions & 12 deletions src/pages/AccountSettings/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ const WebauthnRegistation = ({
const [beginData, setBeginData] = useState(null);
const [pendingCredential, setPendingCredential] = useState(null);
const [nickname, setNickname] = useState("");
const [nicknameChosen, setNicknameChosen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [needPrfRetry, setNeedPrfRetry] = useState(false);
const [resolvePrfRetryPrompt, setResolvePrfRetryPrompt] = useState<null | ((accept: boolean) => void)>(null);
const [prfRetryAccepted, setPrfRetryAccepted] = useState(false);
const { t } = useTranslation();
const keystore = useLocalStorageKeystore();
const unlocked = Boolean(existingPrfKey && wrappedMainKey);

const stateChooseNickname = Boolean(beginData) && !needPrfRetry;

const onBegin = useCallback(
async () => {
setBeginData(null);
Expand Down Expand Up @@ -96,25 +102,37 @@ const WebauthnRegistation = ({
console.log("onCancel");
setPendingCredential(null);
setBeginData(null);
setNeedPrfRetry(false);
setResolvePrfRetryPrompt(null);
setPrfRetryAccepted(false);
setIsSubmitting(false);
};

const onFinish = async (event) => {
event.preventDefault();
console.log("onFinish", event);
setNicknameChosen(true);

if (beginData && pendingCredential && existingPrfKey && wrappedMainKey) {
setIsSubmitting(true);

const [newPrivateData, keystoreCommit] = await keystore.addPrf(
pendingCredential,
beginData.createOptions.publicKey.rp.id,
existingPrfKey,
wrappedMainKey,
async () => true,
);

try {
const [newPrivateData, keystoreCommit] = await keystore.addPrf(
pendingCredential,
beginData.createOptions.publicKey.rp.id,
existingPrfKey,
wrappedMainKey,
async () => {
setNeedPrfRetry(true);
return new Promise<boolean>((resolve, reject) => {
setResolvePrfRetryPrompt(() => resolve);
}).finally(() => {
setNeedPrfRetry(false);
setPrfRetryAccepted(true);
setResolvePrfRetryPrompt(null);
});
},
);

setIsSubmitting(true);
await api.post('/user/session/webauthn/register-finish', {
challengeId: beginData.challengeId,
nickname,
Expand All @@ -138,8 +156,10 @@ const WebauthnRegistation = ({

} catch (e) {
console.error("Failed to finish registration", e);

} finally {
onCancel();
}
onCancel();
} else {
console.error("Invalid state:", beginData, pendingCredential, existingPrfKey, wrappedMainKey);
}
Expand All @@ -161,7 +181,7 @@ const WebauthnRegistation = ({
</button>

<Dialog
open={Boolean(beginData)}
open={stateChooseNickname}
onCancel={onCancel}
>
<form method="dialog" onSubmit={onFinish}>
Expand Down Expand Up @@ -212,6 +232,46 @@ const WebauthnRegistation = ({

</form>
</Dialog>

<Dialog
open={needPrfRetry && !prfRetryAccepted}
onCancel={() => resolvePrfRetryPrompt(false)}
>
<h3 className="text-2xl mt-4 mb-2 font-bold text-custom-blue">Almost done!</h3>
<p>Your passkey has been created.</p>
<p>To finish setting up, please authenticate with it once more.</p>

<button
type="button"
className="bg-white px-4 py-2 border border-gray-300 rounded-md cursor-pointer hover:bg-gray-100 mr-2"
onClick={() => resolvePrfRetryPrompt(false)}
>
Cancel
</button>
<button
type="button"
className="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => resolvePrfRetryPrompt(true)}
disabled={prfRetryAccepted}
>
Continue
</button>
</Dialog>

<Dialog
open={prfRetryAccepted}
onCancel={onCancel}
>
<p>Please authenticate with the new passkey...</p>

<button
type="button"
className="bg-white px-4 py-2 border border-gray-300 rounded-md cursor-pointer hover:bg-gray-100 mr-2"
onClick={onCancel}
>
Cancel
</button>
</Dialog>
</>
);
};
Expand Down
87 changes: 72 additions & 15 deletions src/pages/Login/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const WebauthnSignupLogin = ({
const [inProgress, setInProgress] = useState(false);
const [name, setName] = useState("");
const [error, setError] = useState('');
const [needPrfRetry, setNeedPrfRetry] = useState(false);
const [resolvePrfRetryPrompt, setResolvePrfRetryPrompt] = useState(null);
const [prfRetryAccepted, setPrfRetryAccepted] = useState(false);
const navigate = useNavigate();
const { t } = useTranslation();
const keystore = useLocalStorageKeystore();
Expand All @@ -113,13 +116,24 @@ const WebauthnSignupLogin = ({
[isLogin],
);

const promptForPrfRetry = async () => {
setNeedPrfRetry(true);
return new Promise((resolve, reject) => {
setResolvePrfRetryPrompt(() => resolve);
}).finally(() => {
setNeedPrfRetry(false);
setPrfRetryAccepted(true);
setResolvePrfRetryPrompt(null);
});
};

const onLogin = useCallback(
async () => {
const result = await api.loginWebauthn(keystore, async () => true);
if (result.ok) {
async () => {
const result = await api.loginWebauthn(keystore, promptForPrfRetry);
if (result.ok) {
navigate('/');

} else {
} else {
// Using a switch here so the t() argument can be a literal, to ease searching
switch (result.val) {
case 'loginKeystoreFailed':
Expand Down Expand Up @@ -148,7 +162,7 @@ const WebauthnSignupLogin = ({

const onSignup = useCallback(
async (name) => {
const result = await api.signupWebauthn(name, keystore, async () => true);
const result = await api.signupWebauthn(name, keystore, promptForPrfRetry);
if (result.ok) {
navigate('/');

Expand Down Expand Up @@ -199,23 +213,66 @@ const WebauthnSignupLogin = ({
const onCancel = () => {
console.log("onCancel");
setInProgress(false);
setNeedPrfRetry(false);
setPrfRetryAccepted(false);
setResolvePrfRetryPrompt(null);
setIsSubmitting(false);
};

return (
<form onSubmit={onSubmit}>
{inProgress
? (
<>
<p className="dark:text-white pb-3">Please interact with your authenticator...</p>
<button
type="button"
className="w-full text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center flex flex-row flex-nowrap items-center justify-center"
onClick={onCancel}
>
Cancel
</button>
</>
needPrfRetry
? (
<div className="text-center">
{
prfRetryAccepted
? (
<p className="dark:text-white pb-3">Please interact with your authenticator...</p>
)
: (
<>
<h3 className="text-2xl mt-4 mb-2 font-bold text-custom-blue">Almost done!</h3>
<p className="dark:text-white pb-3">
{isLogin
? 'To finish unlocking the wallet, please authenticate with your passkey once more.'
: 'To finish setting up your wallet, please authenticate with your passkey once more.'
}
</p>
</>
)
}

<button
type="button"
className="bg-white px-4 py-2 border border-gray-300 rounded-md cursor-pointer hover:bg-gray-100 mr-2"
onClick={() => resolvePrfRetryPrompt(false)}
>
Cancel
</button>
<button
type="button"
className="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={() => resolvePrfRetryPrompt(true)}
disabled={prfRetryAccepted}
>
Continue
</button>
</div>
)
: (
<>
<p className="dark:text-white pb-3">Please interact with your authenticator...</p>
<button
type="button"
className="w-full text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center flex flex-row flex-nowrap items-center justify-center"
onClick={onCancel}
>
Cancel
</button>
</>
)
)
: (
<>
Expand Down

0 comments on commit 43d30d1

Please sign in to comment.