diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx
index 6f0d4a9b90..73c437cfd2 100644
--- a/web/apps/accounts/src/pages/_app.tsx
+++ b/web/apps/accounts/src/pages/_app.tsx
@@ -1,5 +1,4 @@
import { CustomHead } from "@/next/components/Head";
-import { setClientPackageForAuthenticatedRequests } from "@/next/http";
import { setupI18n } from "@/next/i18n";
import { logUnhandledErrorsAndRejections } from "@/next/log-web";
import { appTitle, type AppName, type BaseAppContextT } from "@/next/types/app";
@@ -12,7 +11,6 @@ import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { AppNavbar } from "@ente/shared/components/Navbar/app";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
-import HTTPService from "@ente/shared/network/HTTPService";
import { LS_KEYS } from "@ente/shared/storage/localStorage";
import { getTheme } from "@ente/shared/themes";
import { THEME_COLOR } from "@ente/shared/themes/constants";
@@ -64,22 +62,6 @@ export default function App({ Component, pageProps }: AppProps) {
return () => logUnhandledErrorsAndRejections(false);
}, []);
- const setupPackageName = () => {
- const clientPackage = localStorage.getItem("clientPackage");
- if (!clientPackage) return;
- setClientPackageForAuthenticatedRequests(clientPackage);
- HTTPService.setHeaders({
- "X-Client-Package": clientPackage,
- });
- };
-
- useEffect(() => {
- router.events.on("routeChangeComplete", setupPackageName);
- return () => {
- router.events.off("routeChangeComplete", setupPackageName);
- };
- }, [router.events]);
-
const closeDialogBoxV2 = () => setDialogBoxV2View(false);
const theme = getTheme(themeColor, "photos");
diff --git a/web/apps/accounts/src/pages/account-handoff.tsx b/web/apps/accounts/src/pages/account-handoff.tsx
deleted file mode 100644
index cb80e49f3f..0000000000
--- a/web/apps/accounts/src/pages/account-handoff.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useEffect } from "react";
-
-/** Legacy alias, remove once mobile code is updated (it is still in beta). */
-const Page = () => {
- useEffect(() => {
- window.location.href = window.location.href.replace(
- "account-handoff",
- "passkeys/handoff",
- );
- }, []);
-
- return <>>;
-};
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/credentials.tsx b/web/apps/accounts/src/pages/credentials.tsx
deleted file mode 100644
index 070aace4a1..0000000000
--- a/web/apps/accounts/src/pages/credentials.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/credentials";
-import { useAppContext } from "./_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/generate.tsx b/web/apps/accounts/src/pages/generate.tsx
deleted file mode 100644
index c6804255af..0000000000
--- a/web/apps/accounts/src/pages/generate.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/generate";
-import { useAppContext } from "./_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/index.tsx b/web/apps/accounts/src/pages/index.tsx
index 29de8b63f5..cb6d2b20f1 100644
--- a/web/apps/accounts/src/pages/index.tsx
+++ b/web/apps/accounts/src/pages/index.tsx
@@ -1,11 +1,9 @@
-import { useRouter } from "next/router";
import React, { useEffect } from "react";
const Page: React.FC = () => {
- const router = useRouter();
-
useEffect(() => {
- router.push("/login");
+ // There are no user navigable pages currently on accounts.ente.io.
+ window.location.href = "https://web.ente.io";
}, []);
return <>>;
diff --git a/web/apps/accounts/src/pages/login.tsx b/web/apps/accounts/src/pages/login.tsx
deleted file mode 100644
index 1a7de0497f..0000000000
--- a/web/apps/accounts/src/pages/login.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/login";
-import { useAppContext } from "./_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/passkeys/finish.tsx b/web/apps/accounts/src/pages/passkeys/finish.tsx
deleted file mode 100644
index a04b61bff5..0000000000
--- a/web/apps/accounts/src/pages/passkeys/finish.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import Page_ from "@ente/accounts/pages/passkeys/finish";
-import { useAppContext } from "../_app";
-
-// This page is for when trying to finish the passkey verification within the
-// accounts app itself. More commonly, this happens in the auth/photos app.
-//
-// See: [Note: Finish passkey flow in the requesting app]
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/passkeys/flow.tsx b/web/apps/accounts/src/pages/passkeys/flow.tsx
deleted file mode 100644
index bd7d5d96be..0000000000
--- a/web/apps/accounts/src/pages/passkeys/flow.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { useEffect } from "react";
-
-/** Legacy alias, remove once mobile code is updated (it is still in beta). */
-const Page = () => {
- useEffect(() => {
- window.location.href = window.location.href.replace("flow", "verify");
- }, []);
-
- return <>>;
-};
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/passkeys/handoff.tsx b/web/apps/accounts/src/pages/passkeys/handoff.tsx
deleted file mode 100644
index 0f14455291..0000000000
--- a/web/apps/accounts/src/pages/passkeys/handoff.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { setClientPackageForAuthenticatedRequests } from "@/next/http";
-import log from "@/next/log";
-import { VerticallyCentered } from "@ente/shared/components/Container";
-import EnteSpinner from "@ente/shared/components/EnteSpinner";
-import HTTPService from "@ente/shared/network/HTTPService";
-import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
-import { useRouter } from "next/router";
-import React, { useEffect } from "react";
-
-/**
- * Parse credentials passed as query parameters by one of our client apps, save
- * them to local storage, and then redirect to the passkeys listing.
- */
-const Page: React.FC = () => {
- const router = useRouter();
-
- useEffect(() => {
- const urlParams = new URLSearchParams(window.location.search);
-
- const clientPackage = urlParams.get("client");
- if (clientPackage) {
- // TODO-PK: mobile is not passing it. is that expected?
- localStorage.setItem("clientPackage", clientPackage);
- setClientPackageForAuthenticatedRequests(clientPackage);
- HTTPService.setHeaders({
- "X-Client-Package": clientPackage,
- });
- }
-
- const token = urlParams.get("token");
- if (!token) {
- log.error("Missing accounts token");
- router.push("/login");
- return;
- }
-
- const user = getData(LS_KEYS.USER) || {};
- user.token = token;
- setData(LS_KEYS.USER, user);
-
- router.push("/passkeys");
- }, []);
-
- return (
-
-
-
- );
-};
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/passkeys/index.tsx b/web/apps/accounts/src/pages/passkeys/index.tsx
index 618d2db0d6..6472eeb18e 100644
--- a/web/apps/accounts/src/pages/passkeys/index.tsx
+++ b/web/apps/accounts/src/pages/passkeys/index.tsx
@@ -1,4 +1,5 @@
import log from "@/next/log";
+import { ensure } from "@/utils/ensure";
import { CenteredFlex } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import EnteButton from "@ente/shared/components/EnteButton";
@@ -10,7 +11,6 @@ import MenuItemDivider from "@ente/shared/components/Menu/MenuItemDivider";
import { MenuItemGroup } from "@ente/shared/components/Menu/MenuItemGroup";
import SingleInputForm from "@ente/shared/components/SingleInputForm";
import Titlebar from "@ente/shared/components/Titlebar";
-import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { formatDateTimeFull } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
@@ -19,7 +19,6 @@ import EditIcon from "@mui/icons-material/Edit";
import KeyIcon from "@mui/icons-material/Key";
import { Box, Button, Stack, Typography, useMediaQuery } from "@mui/material";
import { t } from "i18next";
-import { useRouter } from "next/router";
import { useAppContext } from "pages/_app";
import React, { useEffect, useState } from "react";
import {
@@ -33,36 +32,49 @@ import {
const Page: React.FC = () => {
const { showNavBar, setDialogBoxAttributesV2 } = useAppContext();
+ const [token, setToken] = useState();
const [passkeys, setPasskeys] = useState([]);
const [showPasskeyDrawer, setShowPasskeyDrawer] = useState(false);
const [selectedPasskey, setSelectedPasskey] = useState<
Passkey | undefined
>();
- const router = useRouter();
+ useEffect(() => {
+ showNavBar(true);
+
+ const urlParams = new URLSearchParams(window.location.search);
+
+ const token = urlParams.get("token");
+ if (token) {
+ setToken(token);
+ } else {
+ log.error("Missing accounts token");
+ showPasskeyFetchFailedErrorDialog();
+ }
+ }, []);
+
+ useEffect(() => {
+ if (token) {
+ void refreshPasskeys();
+ }
+ }, [token]);
const refreshPasskeys = async () => {
try {
- setPasskeys(await getPasskeys());
+ setPasskeys(await getPasskeys(ensure(token)));
} catch (e) {
log.error("Failed to fetch passkeys", e);
- setDialogBoxAttributesV2({
- title: t("ERROR"),
- content: t("passkey_fetch_failed"),
- close: {},
- });
+ showPasskeyFetchFailedErrorDialog();
}
};
- useEffect(() => {
- if (!getToken()) {
- router.push("/login");
- return;
- }
-
- showNavBar(true);
- void refreshPasskeys();
- }, []);
+ const showPasskeyFetchFailedErrorDialog = () => {
+ setDialogBoxAttributesV2({
+ title: t("ERROR"),
+ content: t("passkey_fetch_failed"),
+ close: {},
+ });
+ };
const handleSelectPasskey = (passkey: Passkey) => {
setSelectedPasskey(passkey);
@@ -90,7 +102,7 @@ const Page: React.FC = () => {
resetForm: () => void,
) => {
try {
- await registerPasskey(inputValue);
+ await registerPasskey(ensure(token), inputValue);
} catch (e) {
log.error("Failed to register a new passkey", e);
// If the user cancels the operation, then an error with name
@@ -133,10 +145,12 @@ const Page: React.FC = () => {
+
>
@@ -205,6 +219,13 @@ interface ManagePasskeyDrawerProps {
open: boolean;
/** Callback to invoke when the drawer wants to be closed. */
onClose: () => void;
+ /**
+ * The token to use for authenticating with the backend when making requests
+ * for editing or deleting passkeys.
+ *
+ * It is guaranteed that this will be defined when `open` is true.
+ */
+ token: string | undefined;
/**
* The {@link Passkey} whose details should be shown in the drawer.
*
@@ -223,6 +244,7 @@ interface ManagePasskeyDrawerProps {
const ManagePasskeyDrawer: React.FC = ({
open,
onClose,
+ token,
passkey,
onUpdateOrDeletePasskey,
}) => {
@@ -232,7 +254,7 @@ const ManagePasskeyDrawer: React.FC = ({
return (
<>
- {passkey && (
+ {token && passkey && (
= ({
)}
- {passkey && (
+ {token && passkey && (
setShowRenameDialog(false)}
+ token={token}
passkey={passkey}
onRenamePasskey={() => {
setShowRenameDialog(false);
@@ -282,10 +305,11 @@ const ManagePasskeyDrawer: React.FC = ({
/>
)}
- {passkey && (
+ {token && passkey && (
setShowDeleteDialog(false)}
+ token={token}
passkey={passkey}
onDeletePasskey={() => {
setShowDeleteDialog(false);
@@ -302,6 +326,8 @@ interface RenamePasskeyDialogProps {
open: boolean;
/** Callback to invoke when the dialog wants to be closed. */
onClose: () => void;
+ /** Auth token for API requests. */
+ token: string;
/** The {@link Passkey} to rename. */
passkey: Passkey;
/** Callback to invoke when the passkey is renamed. */
@@ -311,6 +337,7 @@ interface RenamePasskeyDialogProps {
const RenamePasskeyDialog: React.FC = ({
open,
onClose,
+ token,
passkey,
onRenamePasskey,
}) => {
@@ -318,7 +345,7 @@ const RenamePasskeyDialog: React.FC = ({
const handleSubmit = async (inputValue: string) => {
try {
- await renamePasskey(passkey.id, inputValue);
+ await renamePasskey(token, passkey.id, inputValue);
onRenamePasskey();
} catch (e) {
log.error("Failed to rename passkey", e);
@@ -349,6 +376,8 @@ interface DeletePasskeyDialogProps {
open: boolean;
/** Callback to invoke when the dialog wants to be closed. */
onClose: () => void;
+ /** Auth token for API requests. */
+ token: string;
/** The {@link Passkey} to delete. */
passkey: Passkey;
/** Callback to invoke when the passkey is deleted. */
@@ -358,6 +387,7 @@ interface DeletePasskeyDialogProps {
const DeletePasskeyDialog: React.FC = ({
open,
onClose,
+ token,
passkey,
onDeletePasskey,
}) => {
@@ -367,7 +397,7 @@ const DeletePasskeyDialog: React.FC = ({
const handleConfirm = async () => {
setIsDeleting(true);
try {
- await deletePasskey(passkey.id);
+ await deletePasskey(token, passkey.id);
onDeletePasskey();
} catch (e) {
log.error("Failed to delete passkey", e);
diff --git a/web/apps/accounts/src/pages/passkeys/recover.tsx b/web/apps/accounts/src/pages/passkeys/recover.tsx
deleted file mode 100644
index 7498332043..0000000000
--- a/web/apps/accounts/src/pages/passkeys/recover.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import Page_ from "@ente/accounts/pages/two-factor/recover";
-import { useAppContext } from "../_app";
-
-// This page is for when trying to reset the passkey verification within the
-// accounts app itself. More commonly, this happens in the auth/photos app.
-//
-// See: [Note: Finish passkey flow in the requesting app]
-
-const Page = () => (
-
-);
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/passkeys/verify.tsx b/web/apps/accounts/src/pages/passkeys/verify.tsx
index a7694e57a2..26cbbc7b77 100644
--- a/web/apps/accounts/src/pages/passkeys/verify.tsx
+++ b/web/apps/accounts/src/pages/passkeys/verify.tsx
@@ -1,11 +1,9 @@
-import { setClientPackageForAuthenticatedRequests } from "@/next/http";
import log from "@/next/log";
import type { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
import { nullToUndefined } from "@/utils/transform";
import { VerticallyCentered } from "@ente/shared/components/Container";
import EnteButton from "@ente/shared/components/EnteButton";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
-import HTTPService from "@ente/shared/network/HTTPService";
import InfoIcon from "@mui/icons-material/Info";
import { Paper, Typography, styled } from "@mui/material";
import { t } from "i18next";
@@ -67,12 +65,6 @@ const Page = () => {
return;
}
- localStorage.setItem("clientPackage", clientPackage);
- setClientPackageForAuthenticatedRequests(clientPackage);
- HTTPService.setHeaders({
- "X-Client-Package": clientPackage,
- });
-
setStatus("loading");
// Extract passkeySessionID from the query params.
@@ -91,6 +83,10 @@ const Page = () => {
setStatus("waitingForUser");
+ // Safari throws "NotAllowedError: The document is not focused" if
+ // the console is open when we call `navigator.credentials.create`.
+ // Not adding any workarounds, just documenting their incompetence.
+
const credential = await signChallenge(options.publicKey);
if (!credential) {
setStatus("failed");
diff --git a/web/apps/accounts/src/pages/recover.tsx b/web/apps/accounts/src/pages/recover.tsx
deleted file mode 100644
index d825729e5e..0000000000
--- a/web/apps/accounts/src/pages/recover.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/recover";
-import { useAppContext } from "./_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/signup.tsx b/web/apps/accounts/src/pages/signup.tsx
deleted file mode 100644
index 403d3e7357..0000000000
--- a/web/apps/accounts/src/pages/signup.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/signup";
-import { useAppContext } from "./_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/two-factor/recover.tsx b/web/apps/accounts/src/pages/two-factor/recover.tsx
deleted file mode 100644
index 61414077e5..0000000000
--- a/web/apps/accounts/src/pages/two-factor/recover.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/two-factor/recover";
-import { useAppContext } from "../_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/two-factor/setup.tsx b/web/apps/accounts/src/pages/two-factor/setup.tsx
deleted file mode 100644
index 12716e2dfb..0000000000
--- a/web/apps/accounts/src/pages/two-factor/setup.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/two-factor/setup";
-import { useAppContext } from "../_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/two-factor/verify.tsx b/web/apps/accounts/src/pages/two-factor/verify.tsx
deleted file mode 100644
index 7c682b1b99..0000000000
--- a/web/apps/accounts/src/pages/two-factor/verify.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/two-factor/verify";
-import { useAppContext } from "../_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/pages/verify.tsx b/web/apps/accounts/src/pages/verify.tsx
deleted file mode 100644
index bb2dc87788..0000000000
--- a/web/apps/accounts/src/pages/verify.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import Page_ from "@ente/accounts/pages/verify";
-import { useAppContext } from "./_app";
-
-const Page = () => ;
-
-export default Page;
diff --git a/web/apps/accounts/src/services/passkey.ts b/web/apps/accounts/src/services/passkey.ts
index 58adaaa0e1..03cbd1eff8 100644
--- a/web/apps/accounts/src/services/passkey.ts
+++ b/web/apps/accounts/src/services/passkey.ts
@@ -1,5 +1,5 @@
import { isDevBuild } from "@/next/env";
-import { clientPackageHeaderIfPresent } from "@/next/http";
+import { clientPackageName } from "@/next/types/app";
import { TwoFactorAuthorizationResponse } from "@/next/types/credentials";
import { ensure } from "@/utils/ensure";
import { nullToUndefined } from "@/utils/transform";
@@ -9,7 +9,6 @@ import {
toB64URLSafeNoPaddingString,
} from "@ente/shared/crypto/internal/libsodium";
import { apiOrigin } from "@ente/shared/network/api";
-import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { z } from "zod";
/** Return true if the user's browser supports WebAuthn (Passkeys). */
@@ -19,19 +18,15 @@ export const isWebAuthnSupported = () => !!navigator.credentials;
* Variant of {@link authenticatedRequestHeaders} but for authenticated requests
* made by the accounts app.
*
- * We cannot use {@link authenticatedRequestHeaders} directly because the
- * accounts app does not save a full user and instead only saves the user's
- * token (and that token too is scoped to the accounts APIs).
+ * @param token The accounts specific auth token to use for making API requests.
*/
-const accountsAuthenticatedRequestHeaders = (): Record => {
- const token = getToken();
- if (!token) throw new Error("Missing accounts token");
- const headers: Record = { "X-Auth-Token": token };
- const clientPackage = nullToUndefined(
- localStorage.getItem("clientPackage"),
- );
- if (clientPackage) headers["X-Client-Package"] = clientPackage;
- return headers;
+const accountsAuthenticatedRequestHeaders = (
+ token: string,
+): Record => {
+ return {
+ "X-Auth-Token": token,
+ "X-Client-Package": clientPackageName.accounts,
+ };
};
const Passkey = z.object({
@@ -57,13 +52,15 @@ const GetPasskeysResponse = z.object({
/**
* Fetch the existing passkeys for the user.
*
+ * @param token The accounts specific auth token to use for making API requests.
+ *
* @returns An array of {@link Passkey}s. The array will be empty if the user
* has no passkeys.
*/
-export const getPasskeys = async () => {
+export const getPasskeys = async (token: string) => {
const url = `${apiOrigin()}/passkeys`;
const res = await fetch(url, {
- headers: accountsAuthenticatedRequestHeaders(),
+ headers: accountsAuthenticatedRequestHeaders(token),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
const { passkeys } = GetPasskeysResponse.parse(await res.json());
@@ -73,16 +70,22 @@ export const getPasskeys = async () => {
/**
* Rename one of the user's existing passkey with the given {@link id}.
*
+ * @param token The accounts specific auth token to use for making API requests.
+ *
* @param id The `id` of the existing passkey to rename.
*
* @param name The new name (a.k.a. "friendly name").
*/
-export const renamePasskey = async (id: string, name: string) => {
+export const renamePasskey = async (
+ token: string,
+ id: string,
+ name: string,
+) => {
const params = new URLSearchParams({ friendlyName: name });
const url = `${apiOrigin()}/passkeys/${id}`;
const res = await fetch(`${url}?${params.toString()}`, {
method: "PATCH",
- headers: accountsAuthenticatedRequestHeaders(),
+ headers: accountsAuthenticatedRequestHeaders(token),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
};
@@ -90,13 +93,15 @@ export const renamePasskey = async (id: string, name: string) => {
/**
* Delete one of the user's existing passkeys.
*
+ * @param token The accounts specific auth token to use for making API requests.
+ *
* @param id The `id` of the existing passkey to delete.
*/
-export const deletePasskey = async (id: string) => {
+export const deletePasskey = async (token: string, id: string) => {
const url = `${apiOrigin()}/passkeys/${id}`;
const res = await fetch(url, {
method: "DELETE",
- headers: accountsAuthenticatedRequestHeaders(),
+ headers: accountsAuthenticatedRequestHeaders(token),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
};
@@ -104,12 +109,14 @@ export const deletePasskey = async (id: string) => {
/**
* Add a new passkey as the second factor to the user's account.
*
+ * @param token The accounts specific auth token to use for making API requests.
+ *
* @param name An arbitrary name that the user wishes to label this passkey with
* (a.k.a. "friendly name").
*/
-export const registerPasskey = async (name: string) => {
+export const registerPasskey = async (token: string, name: string) => {
// Get options (and sessionID) from the backend.
- const { sessionID, options } = await beginPasskeyRegistration();
+ const { sessionID, options } = await beginPasskeyRegistration(token);
// Ask the browser to new (public key) credentials using these options.
const credential = ensure(await navigator.credentials.create(options));
@@ -117,6 +124,7 @@ export const registerPasskey = async (name: string) => {
// Finish by letting the backend know about these credentials so that it can
// save the public key for future authentication.
await finishPasskeyRegistration({
+ token,
friendlyName: name,
sessionID,
credential,
@@ -140,11 +148,11 @@ interface BeginPasskeyRegistrationResponse {
};
}
-const beginPasskeyRegistration = async () => {
+const beginPasskeyRegistration = async (token: string) => {
const url = `${apiOrigin()}/passkeys/registration/begin`;
const res = await fetch(url, {
method: "POST",
- headers: accountsAuthenticatedRequestHeaders(),
+ headers: accountsAuthenticatedRequestHeaders(token),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
@@ -262,12 +270,14 @@ const binaryToServerB64 = async (b: ArrayBuffer) => {
};
interface FinishPasskeyRegistrationOptions {
+ token: string;
sessionID: string;
friendlyName: string;
credential: Credential;
}
const finishPasskeyRegistration = async ({
+ token,
sessionID,
friendlyName,
credential,
@@ -286,7 +296,7 @@ const finishPasskeyRegistration = async ({
const url = `${apiOrigin()}/passkeys/registration/finish`;
const res = await fetch(`${url}?${params.toString()}`, {
method: "POST",
- headers: accountsAuthenticatedRequestHeaders(),
+ headers: accountsAuthenticatedRequestHeaders(token),
body: JSON.stringify({
id: credential.id,
// This is meant to be the ArrayBuffer version of the (base64
@@ -376,7 +386,6 @@ export const beginPasskeyAuthentication = async (
const url = `${apiOrigin()}/users/two-factor/passkeys/begin`;
const res = await fetch(url, {
method: "POST",
- headers: clientPackageHeaderIfPresent(),
body: JSON.stringify({ sessionID: passkeySessionID }),
});
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx
index 34f8c78dd8..d2d7aa1e83 100644
--- a/web/apps/photos/src/components/Sidebar/index.tsx
+++ b/web/apps/photos/src/components/Sidebar/index.tsx
@@ -428,7 +428,6 @@ const UtilitySection: React.FC = ({ closeSidebar }) => {
const router = useRouter();
const appContext = useContext(AppContext);
const {
- appName,
setDialogMessage,
startLoading,
watchFolderView,
@@ -473,7 +472,7 @@ const UtilitySection: React.FC = ({ closeSidebar }) => {
closeSidebar();
try {
- await openAccountsManagePasskeysPage(appName);
+ await openAccountsManagePasskeysPage();
} catch (e) {
log.error("failed to redirect to accounts page", e);
}
diff --git a/web/docs/webauthn-passkeys.md b/web/docs/webauthn-passkeys.md
index 465af26726..853a31145e 100644
--- a/web/docs/webauthn-passkeys.md
+++ b/web/docs/webauthn-passkeys.md
@@ -21,9 +21,9 @@ some operating system restrictions.
## Getting to the passkeys manager
-As of Feb 2024, Ente clients have a button to navigate to a WebView of Ente
+As of Jun 2024, Ente clients have a button to navigate to a WebView of Ente
Accounts. Ente Accounts allows users to add and manage their registered
-passkeys.
+passkeys, and later authenticate with them as a second factor.
> [!NOTE]
>
@@ -55,12 +55,10 @@ used.** This restriction is a byproduct of the enablement for automatic login.
### Automatically logging into Ente Accounts
Clients open a WebView with the URL
-`https://accounts.ente.io/passkeys/handoff?token=`. This page
-will parse the token for usage in subsequent Accounts-related API calls.
+`https://accounts.ente.io/passkeys?token=`.
-If the token is valid, the user will be automatically redirected to the passkeys
-management page. Otherwise, they will be required to login with their Ente
-credentials.
+If the token is valid, the user will be show a list of their passkeys, and they
+can edit / delete them, or add new ones.
## Registering a WebAuthn credential
@@ -128,7 +126,7 @@ func (u *PasskeyUser) WebAuthnCredentials() []webauthn.Credential {
"publicKey": {
"rp": {
"name": "Ente",
- "id": "accounts.ente.io"
+ "id": "ente.io"
},
"user": {
"name": "james@example.org",
@@ -335,16 +333,17 @@ if (passkeySessionID) {
}
```
-The client should redirect the user to Accounts with this session ID to prompt
-credential authentication. We use Accounts as the central WebAuthn hub since it
-is needed anyways to service credential authentication from mobile clients, so
-we use the same flow for other (web, desktop) clients too.
+The client should redirect the user to the Ente Accounts web app with this
+session ID to prompt credential authentication.
-```tsx
-window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${passkeySessionID}&clientPackage=io.ente.photos.web&redirect=${
- window.location.origin
-}/passkeys/finish`;
```
+https://accounts.ente.io/passkeys?
+ passkeySessionID=&clientPackage=&
+ redirect=&recover=
+```
+
+We use Ente Accounts as the central WebAuthn hub since it allows us to handle
+mobile and desktop clients too.
### Requesting publicKey options (begin)
@@ -367,7 +366,7 @@ window.location.href = `${accountsAppURL()}/passkeys/verify?passkeySessionID=${p
"publicKey": {
"challenge": "dF-mmdZSBxP6Z7OhZrmQ4h-k-BkuuX6ERnW_ckYdkvc",
"timeout": 300000,
- "rpId": "accounts.ente.io",
+ "rpId": "ente.io",
"allowCredentials": [
{
"type": "public-key",
diff --git a/web/packages/accounts/services/passkey.ts b/web/packages/accounts/services/passkey.ts
index 32884c358f..c4e2b1429e 100644
--- a/web/packages/accounts/services/passkey.ts
+++ b/web/packages/accounts/services/passkey.ts
@@ -69,11 +69,12 @@ export const redirectUserToPasskeyVerificationFlow = (
*
* @param appName The {@link AppName} of the app which is calling this function.
*/
-export const openAccountsManagePasskeysPage = async (appName: AppName) => {
- // check if the user has passkey recovery enabled
+export const openAccountsManagePasskeysPage = async () => {
+ // Check if the user has passkey recovery enabled
const recoveryEnabled = await isPasskeyRecoveryEnabled();
if (!recoveryEnabled) {
- // let's create the necessary recovery information
+ // If not, enable it for them by creating the necessary recovery
+ // information to prevent them from getting locked out.
const recoveryKey = await getRecoveryKey();
const resetSecret = await generateEncryptionKey();
@@ -91,11 +92,12 @@ export const openAccountsManagePasskeysPage = async (appName: AppName) => {
);
}
+ // Redirect to the Ente Accounts app where they can view and add and manage
+ // their passkeys.
const token = await getAccountsToken();
- const client = clientPackageName[appName];
- const params = new URLSearchParams({ token, client });
+ const params = new URLSearchParams({ token });
- window.open(`${accountsAppURL()}/passkeys/handoff?${params.toString()}`);
+ window.open(`${accountsAppURL()}/passkeys?${params.toString()}`);
};
export const isPasskeyRecoveryEnabled = async () => {
diff --git a/web/packages/next/http.ts b/web/packages/next/http.ts
index 680967c183..69327fd53d 100644
--- a/web/packages/next/http.ts
+++ b/web/packages/next/http.ts
@@ -48,13 +48,3 @@ export const authenticatedRequestHeaders = (): Record => {
if (_clientPackage) headers["X-Client-Package"] = _clientPackage;
return headers;
};
-
-/**
- * Return a headers object with client package header if we have that value
- * present in local storage.
- */
-export const clientPackageHeaderIfPresent = (): Record => {
- const headers: Record = {};
- if (_clientPackage) headers["X-Client-Package"] = _clientPackage;
- return headers;
-};