Don't show retry button if trying to use an already claimed session
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
isWebAuthnSupported,
|
||||
isWhitelistedRedirect,
|
||||
passkeyAuthenticationSuccessRedirectURL,
|
||||
passkeySessionAlreadyClaimedErrorMessage,
|
||||
redirectToPasskeyRecoverPage,
|
||||
signChallenge,
|
||||
type BeginPasskeyAuthenticationResponse,
|
||||
@@ -31,6 +32,7 @@ const Page = () => {
|
||||
| "loading" /* Can happen multiple times in the flow */
|
||||
| "webAuthnNotSupported" /* Unrecoverable error */
|
||||
| "unknownRedirect" /* Unrecoverable error */
|
||||
| "sessionAlreadyClaimed" /* Unrecoverable error */
|
||||
| "unrecoverableFailure" /* Unrecoverable error - generic */
|
||||
| "failedDuringSignChallenge" /* Recoverable error in signChallenge */
|
||||
| "failed" /* Recoverable error otherwise */
|
||||
@@ -117,7 +119,12 @@ const Page = () => {
|
||||
beginResponse = await beginPasskeyAuthentication(passkeySessionID);
|
||||
} catch (e) {
|
||||
log.error("Failed to begin passkey authentication", e);
|
||||
setStatus("failed");
|
||||
setStatus(
|
||||
e instanceof Error &&
|
||||
e.message == passkeySessionAlreadyClaimedErrorMessage
|
||||
? "sessionAlreadyClaimed"
|
||||
: "failed",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,6 +238,7 @@ const Page = () => {
|
||||
loading: <Loading />,
|
||||
unknownRedirect: <UnknownRedirect />,
|
||||
webAuthnNotSupported: <WebAuthnNotSupported />,
|
||||
sessionAlreadyClaimed: <SessionAlreadyClaimed />,
|
||||
unrecoverableFailure: <UnrecoverableFailure />,
|
||||
failedDuringSignChallenge: (
|
||||
<RetriableFailed
|
||||
@@ -277,6 +285,26 @@ const WebAuthnNotSupported: React.FC = () => {
|
||||
return <Failed message={t("passkeys_not_supported")} />;
|
||||
};
|
||||
|
||||
const SessionAlreadyClaimed: React.FC = () => {
|
||||
return (
|
||||
<Content>
|
||||
<SessionAlreadyClaimed_>
|
||||
<InfoIcon color="secondary" />
|
||||
<Typography>
|
||||
{t("passkey_login_already_claimed_session")}
|
||||
</Typography>
|
||||
</SessionAlreadyClaimed_>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
const SessionAlreadyClaimed_ = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
`;
|
||||
|
||||
const UnrecoverableFailure: React.FC = () => {
|
||||
return <Failed message={t("passkey_login_generic_error")} />;
|
||||
};
|
||||
|
||||
@@ -368,6 +368,13 @@ export interface BeginPasskeyAuthenticationResponse {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The passkey session which we are trying to start an authentication ceremony
|
||||
* for has already finished elsewhere.
|
||||
*/
|
||||
export const passkeySessionAlreadyClaimedErrorMessage =
|
||||
"Passkey session already claimed";
|
||||
|
||||
/**
|
||||
* Create a authentication ceremony session and return a challenge and a list of
|
||||
* public key credentials that can be used to attest that challenge.
|
||||
@@ -379,6 +386,9 @@ export interface BeginPasskeyAuthenticationResponse {
|
||||
*
|
||||
* @param passkeySessionID A session created by the requesting app that can be
|
||||
* used to initiate a passkey authentication ceremony on the accounts app.
|
||||
*
|
||||
* @throws In addition to arbitrary errors, it throws errors with the message
|
||||
* {@link passkeySessionAlreadyClaimedErrorMessage}.
|
||||
*/
|
||||
export const beginPasskeyAuthentication = async (
|
||||
passkeySessionID: string,
|
||||
@@ -388,7 +398,11 @@ export const beginPasskeyAuthentication = async (
|
||||
method: "POST",
|
||||
body: JSON.stringify({ sessionID: passkeySessionID }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
if (!res.ok) {
|
||||
if (res.status == 409)
|
||||
throw new Error(passkeySessionAlreadyClaimedErrorMessage);
|
||||
throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
// See: [Note: Converting binary data in WebAuthn API payloads]
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ const getAccountsToken = async () => {
|
||||
* The passkey session whose status we are trying to check has already expired.
|
||||
* The user should attempt to login again.
|
||||
*/
|
||||
export const passKeySessionExpiredErrorMessage = "Passkey session has expired";
|
||||
export const passkeySessionExpiredErrorMessage = "Passkey session has expired";
|
||||
|
||||
/**
|
||||
* Check if the user has already authenticated using their passkey for the given
|
||||
@@ -229,7 +229,7 @@ export const passKeySessionExpiredErrorMessage = "Passkey session has expired";
|
||||
* authentication has completed, and `undefined` otherwise.
|
||||
*
|
||||
* @throws In addition to arbitrary errors, it throws errors with the message
|
||||
* {@link passKeySessionExpiredErrorMessage}.
|
||||
* {@link passkeySessionExpiredErrorMessage}.
|
||||
*/
|
||||
export const checkPasskeyVerificationStatus = async (
|
||||
sessionID: string,
|
||||
@@ -241,7 +241,7 @@ export const checkPasskeyVerificationStatus = async (
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (res.status == 404 || res.status == 410)
|
||||
throw new Error(passKeySessionExpiredErrorMessage);
|
||||
throw new Error(passkeySessionExpiredErrorMessage);
|
||||
if (res.status == 400) return undefined; /* verification pending */
|
||||
throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
@@ -622,6 +622,7 @@
|
||||
"passkey_add_failed": "Could not add passkey",
|
||||
"passkey_login_failed": "Passkey login failed",
|
||||
"passkey_login_invalid_url": "The login URL is invalid.",
|
||||
"passkey_login_already_claimed_session": "This session has already been verified.",
|
||||
"passkey_login_generic_error": "An error occurred while logging in with passkey.",
|
||||
"passkey_login_credential_hint": "If your passkeys are on a different device, you can open this page on that device to verify.",
|
||||
"passkeys_not_supported": "Passkeys are not supported in this browser",
|
||||
|
||||
@@ -3,7 +3,7 @@ import log from "@/next/log";
|
||||
import type { BaseAppContextT } from "@/next/types/app";
|
||||
import {
|
||||
checkPasskeyVerificationStatus,
|
||||
passKeySessionExpiredErrorMessage,
|
||||
passkeySessionExpiredErrorMessage,
|
||||
saveCredentialsAndNavigateTo,
|
||||
} from "@ente/accounts/services/passkey";
|
||||
import EnteButton from "@ente/shared/components/EnteButton";
|
||||
@@ -108,7 +108,7 @@ export const VerifyingPasskey: React.FC<VerifyingPasskeyProps> = ({
|
||||
log.error("Passkey verification status check failed", e);
|
||||
setDialogBoxAttributesV2(
|
||||
e instanceof Error &&
|
||||
e.message == passKeySessionExpiredErrorMessage
|
||||
e.message == passkeySessionExpiredErrorMessage
|
||||
? sessionExpiredDialogAttributes(logout)
|
||||
: genericErrorAttributes(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user