mirror of
https://github.com/ente-io/ente.git
synced 2025-08-07 23:18:10 +00:00
177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
import log from "@/next/log";
|
|
import type { AppName } from "@/next/types/app";
|
|
import { clientPackageName } from "@/next/types/app";
|
|
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
|
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
|
|
import {
|
|
encryptToB64,
|
|
generateEncryptionKey,
|
|
} from "@ente/shared/crypto/internal/libsodium";
|
|
import { CustomError } from "@ente/shared/error";
|
|
import HTTPService from "@ente/shared/network/HTTPService";
|
|
import { accountsAppURL, apiOrigin } from "@ente/shared/network/api";
|
|
import { getToken } from "@ente/shared/storage/localStorage/helpers";
|
|
|
|
/**
|
|
* Return a URL that can be passed to the accounts app to serve as the redirect
|
|
* back to us on successful passkey authentication.
|
|
*
|
|
* The returned URL begins with `window.location.origin` and will work both when
|
|
* we're running in a web browser (of course), but also in the desktop app
|
|
* (See: [Note: Using deeplinks to navigate in desktop app]).
|
|
*/
|
|
export const passkeyAuthenticationFinishRedirect = () =>
|
|
`${window.location.origin}/passkeys/finish`;
|
|
|
|
/**
|
|
* Redirect user to Ente accounts app to authenticate using their second factor,
|
|
* a passkey they've configured.
|
|
*
|
|
* On successful verification, the accounts app will redirect back to our
|
|
* `/passkeys/finish` page.
|
|
*
|
|
* @param appName The {@link AppName} of the app which is calling this function.
|
|
*
|
|
* @param passkeySessionID An identifier provided by museum for this passkey
|
|
* verification session.
|
|
*/
|
|
export const redirectUserToPasskeyVerificationFlow = (
|
|
appName: AppName,
|
|
passkeySessionID: string,
|
|
) => {
|
|
const clientPackage = clientPackageName[appName];
|
|
const redirect = passkeyAuthenticationFinishRedirect();
|
|
const params = new URLSearchParams({
|
|
clientPackage,
|
|
passkeySessionID,
|
|
redirect,
|
|
});
|
|
const url = `${accountsAppURL()}/passkeys/verify?${params.toString()}`;
|
|
// [Note: Passkey verification in the desktop app]
|
|
//
|
|
// Our desktop app bundles the web app and serves it over a custom protocol.
|
|
// Passkeys are tied to origins, and will not work with this custom protocol
|
|
// even if we move the passkey creation and authentication inline to within
|
|
// the Photos web app.
|
|
//
|
|
// Thus, passkey creation and authentication in the desktop app works the
|
|
// same way it works in the mobile app - the system browser is invoked to
|
|
// open accounts.ente.io.
|
|
//
|
|
// - For passkey creation, this is a one-way open. Passkeys get created at
|
|
// accounts.ente.io, and that's it.
|
|
//
|
|
// - For passkey verification, the flow is two-way. We register a custom
|
|
// protocol and provide that as a return path redirect. Passkey
|
|
// authentication happens at accounts.ente.io, and on success there is
|
|
// redirected back to the desktop app.
|
|
if (globalThis.electron) window.open(url);
|
|
else window.location.href = url;
|
|
};
|
|
|
|
/**
|
|
* Open a new window showing a page on the Ente accounts app where the user can
|
|
* see and their manage their passkeys.
|
|
*
|
|
* @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
|
|
const recoveryEnabled = await isPasskeyRecoveryEnabled();
|
|
if (!recoveryEnabled) {
|
|
// let's create the necessary recovery information
|
|
const recoveryKey = await getRecoveryKey();
|
|
|
|
const resetSecret = await generateEncryptionKey();
|
|
|
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
|
const encryptionResult = await encryptToB64(
|
|
resetSecret,
|
|
await cryptoWorker.fromHex(recoveryKey),
|
|
);
|
|
|
|
await configurePasskeyRecovery(
|
|
resetSecret,
|
|
encryptionResult.encryptedData,
|
|
encryptionResult.nonce,
|
|
);
|
|
}
|
|
|
|
const token = await getAccountsToken();
|
|
const client = clientPackageName[appName];
|
|
const params = new URLSearchParams({ token, client });
|
|
|
|
window.open(`${accountsAppURL()}/passkeys/handoff?${params.toString()}`);
|
|
};
|
|
|
|
export const isPasskeyRecoveryEnabled = async () => {
|
|
try {
|
|
const token = getToken();
|
|
|
|
const resp = await HTTPService.get(
|
|
`${apiOrigin()}/users/two-factor/recovery-status`,
|
|
{},
|
|
{
|
|
"X-Auth-Token": token,
|
|
},
|
|
);
|
|
|
|
if (typeof resp.data === "undefined") {
|
|
throw Error(CustomError.REQUEST_FAILED);
|
|
}
|
|
|
|
return resp.data["isPasskeyRecoveryEnabled"] as boolean;
|
|
} catch (e) {
|
|
log.error("failed to get passkey recovery status", e);
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
const configurePasskeyRecovery = async (
|
|
secret: string,
|
|
userSecretCipher: string,
|
|
userSecretNonce: string,
|
|
) => {
|
|
try {
|
|
const token = getToken();
|
|
|
|
const resp = await HTTPService.post(
|
|
`${apiOrigin()}/users/two-factor/passkeys/configure-recovery`,
|
|
{
|
|
secret,
|
|
userSecretCipher,
|
|
userSecretNonce,
|
|
},
|
|
undefined,
|
|
{
|
|
"X-Auth-Token": token,
|
|
},
|
|
);
|
|
|
|
if (typeof resp.data === "undefined") {
|
|
throw Error(CustomError.REQUEST_FAILED);
|
|
}
|
|
} catch (e) {
|
|
log.error("failed to configure passkey recovery", e);
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Fetch an Ente Accounts specific JWT token.
|
|
*
|
|
* This token can be used to authenticate with the Ente accounts app.
|
|
*/
|
|
const getAccountsToken = async () => {
|
|
const token = getToken();
|
|
|
|
const resp = await HTTPService.get(
|
|
`${apiOrigin()}/users/accounts-token`,
|
|
undefined,
|
|
{
|
|
"X-Auth-Token": token,
|
|
},
|
|
);
|
|
return resp.data["accountsToken"];
|
|
};
|