Don't show retry button if trying to use an already claimed session

This commit is contained in:
Manav Rathi
2024-06-14 14:29:01 +05:30
parent ddd4d3e16c
commit 9608cfaa4e
5 changed files with 50 additions and 7 deletions

View File

@@ -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")} />;
};

View File

@@ -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]

View File

@@ -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}`);
}

View File

@@ -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",

View File

@@ -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(),
);