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; -};