mirror of
https://github.com/ente-io/ente.git
synced 2025-07-03 05:56:17 +00:00
119 lines
4.7 KiB
TypeScript
119 lines
4.7 KiB
TypeScript
import { authenticatedRequestHeaders, HTTPError } from "@/base/http";
|
|
import { ensureLocalUser } from "@/base/local-user";
|
|
import { apiURL } from "@/base/origins";
|
|
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
|
|
import type { KeyAttributes } from "@ente/shared/user/types";
|
|
import type { SRPAttributes } from "./srp-remote";
|
|
import { getSRPAttributes } from "./srp-remote";
|
|
|
|
type SessionValidity =
|
|
| { status: "invalid" }
|
|
| { status: "valid" }
|
|
| {
|
|
status: "validButPasswordChanged";
|
|
updatedKeyAttributes: KeyAttributes;
|
|
updatedSRPAttributes: SRPAttributes;
|
|
};
|
|
|
|
/**
|
|
* Check if the local token and/or key attributes we have are still valid.
|
|
*
|
|
* This function does not take any parameters because it reads the current state
|
|
* (key attributes) from local storage.
|
|
*
|
|
* @returns
|
|
*
|
|
* - status "invalid" if the current token has been invalidated,
|
|
* - status "valid" normally
|
|
* - status "validButPasswordChanged" we detected that user changed their
|
|
* password on a different device (without choosing the option to log out of
|
|
* all existing sessions).
|
|
*
|
|
* ---
|
|
*
|
|
* [Note: Handling password changes]
|
|
*
|
|
* If the user changes their password on a different device, then we need to
|
|
* update our local state so that we use the latest password for verification.
|
|
*
|
|
* There is a straightforward way of doing this by always making a blocking API
|
|
* call before showing the password unlock page, however that would add latency
|
|
* to the 99% user experience (of normal unlocks) for the 1% case (they've
|
|
* changed their password elsewhere).
|
|
*
|
|
* The approach we instead use is to make an non-blocking `/session-validity`
|
|
* API call when this page is loaded. Usually this'll complete well before the
|
|
* password enters their password and presses "Sign in", but we also
|
|
* transparently await for this API call to finish before initiating the actual
|
|
* verification. Thus there is no extra latency in the happy paths.
|
|
*
|
|
* The `/session-validity` API call tells us:
|
|
*
|
|
* 1. Whether or not the session has been invalidated (by the user choosing to
|
|
* log out from all devices elsewhere).
|
|
*
|
|
* 2. What are their latest key attributes.
|
|
*
|
|
* If the session has been invalidated, we log them out here too.
|
|
*
|
|
* If the key attributes we get are different from the ones we have locally, we
|
|
* regenerate new ones locally, and then use those for verifying the password
|
|
* subsequently.
|
|
*/
|
|
export const checkSessionValidity = async (): Promise<SessionValidity> => {
|
|
const res = await fetch(await apiURL("/users/session-validity/v2"), {
|
|
headers: await authenticatedRequestHeaders(),
|
|
});
|
|
if (!res.ok) {
|
|
if (res.status == 401)
|
|
return { status: "invalid" }; /* session is no longer valid */
|
|
else throw new HTTPError(res);
|
|
}
|
|
|
|
// See if the response contains keyAttributes (they might not for older
|
|
// deployments).
|
|
const json = await res.json();
|
|
if (
|
|
"keyAttributes" in json &&
|
|
typeof json.keyAttributes == "object" &&
|
|
json.keyAttributes !== null
|
|
) {
|
|
// Assume it is a `KeyAttributes`.
|
|
//
|
|
// Enhancement: Convert this to a zod validation.
|
|
const remoteKeyAttributes = json.keyAttributes as KeyAttributes;
|
|
|
|
// We should have these values locally if we reach here.
|
|
const email = ensureLocalUser().email;
|
|
const localSRPAttributes = getData(LS_KEYS.SRP_ATTRIBUTES)!;
|
|
|
|
// Fetch the remote SRP attributes.
|
|
//
|
|
// The key attributes we have saved locally are the locally generated
|
|
// ones for interactive usage, and thus they'll always differ from the
|
|
// ones we get from remote. To detect if the user changed their
|
|
// password, we also need to fetch their SRP attributes (which will be
|
|
// identical between remote and us if the password is the same).
|
|
const remoteSRPAttributes = await getSRPAttributes(email);
|
|
|
|
// If we get something (and usually we should),
|
|
if (remoteSRPAttributes) {
|
|
// See if it is different from the one we have locally. Use the kekSalt
|
|
// as a proxy for comparing the entire object (The kekSalt will be
|
|
// different if a new password has been generated).
|
|
if (remoteSRPAttributes.kekSalt != localSRPAttributes.kekSalt) {
|
|
// The token is still valid, but the key and SRP attributes have
|
|
// changed.
|
|
return {
|
|
status: "validButPasswordChanged",
|
|
updatedKeyAttributes: remoteKeyAttributes,
|
|
updatedSRPAttributes: remoteSRPAttributes,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// The token is still valid (to the best of our ascertainable knowledge).
|
|
return { status: "valid" };
|
|
};
|