From 767f2479af00156017983e64459ac85404be0932 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 20 Jun 2024 15:44:00 +0530 Subject: [PATCH] wip --- web/packages/accounts/pages/credentials.tsx | 52 --------- web/packages/accounts/services/session.ts | 118 ++++++++++++++++++++ 2 files changed, 118 insertions(+), 52 deletions(-) create mode 100644 web/packages/accounts/services/session.ts diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index 311152dac9..9465c530e1 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -368,55 +368,3 @@ const Page: React.FC = ({ appContext }) => { }; export default Page; - -/** - * If the user changes their password on a different device, then we need to log - * them out here. - * - * There is a straightforward way of doing this by always making a blocking API - * call before showing this page, however that would add latency to the 99% user - * experience (of normal unlocks) for the 1% cases (they've changed their - * password elsewhere). - * - * If we don't do anything though, the behaviour is confusing: - * - * 1. The data on this device is encrypted with their old password, so entering - * their old password will successfully log them in. This appears as a bug to - * the user. - * - * 2. However, more critically, if they try to enter their new password, it does - * not get accepted (since the data on this device is encrypted with their - * old password). This causes user alarm. - * - * As a way to handle primarily case 2 (but also case 1), without adding latency - * to the normal unlocks, we do an non-blocking API call to get the user's SRP - * attributes when they enter this page. SRP attributes change when the password - * is changed, and thus when we compare the server's response with what is - * present locally, we'll find that the SRP attributes have changed. In such - * cases, we invalidate their session on this device and ask them to login - * afresh. - * - * @param email The user's email. - * - * @param localSRPAttributes The local SRP attributes. - */ -const didPasswordChangeElsewhere = async ( - email: string, - localSRPAttributes: SRPAttributes, -) => { - try { - const serverAttributes = await getSRPAttributes(email); - // (Arbitrarily) compare the salt to figure out if something changed - // (salt will always change on password changes). - if (serverAttributes?.kekSalt !== localSRPAttributes.kekSalt) - return true; /* password indeed did change */ - return false; - } catch (e) { - // Ignore errors here. In rare cases, the stars may align and cause the - // API calls to fail in that 1 case where the user indeed changed their - // password, but we also don't want to start logging people out for - // harmless transient issues like network errors. - log.error("Failed to compare SRP attributes", e); - return false; - } -}; diff --git a/web/packages/accounts/services/session.ts b/web/packages/accounts/services/session.ts new file mode 100644 index 0000000000..98107200f9 --- /dev/null +++ b/web/packages/accounts/services/session.ts @@ -0,0 +1,118 @@ +import { authenticatedRequestHeaders } from "@/next/http"; +import { apiOrigin } from "@ente/shared/network/api"; +import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; +import type { KeyAttributes } from "@ente/shared/user/types"; + +const getSessionValidity = async () => { + const url = `${apiOrigin()}/users/session-validity/v2`; + const res = await fetch(url, { + headers: authenticatedRequestHeaders() + }); + if (!res.ok) { + if (res.status == 401) return false; /* session is no longer valid */ + else throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); + } + + + export const isTokenValid = async (token: string) => { + try { + const resp = await HTTPService.get( + `${ENDPOINT}/users/session-validity/v2`, + null, + { + "X-Auth-Token": token, + }, + ); + try { + if (resp.data[HAS_SET_KEYS] === undefined) { + throw Error("resp.data.hasSetKey undefined"); + } + if (!resp.data["hasSetKeys"]) { + try { + await putAttributes( + token, + getData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES), + ); + } catch (e) { + log.error("put attribute failed", e); + } + } + } catch (e) { + log.error("hasSetKeys not set in session validity response", e); + } + return true; + } catch (e) { + log.error("session-validity api call failed", e); + if ( + e instanceof ApiError && + e.httpStatusCode === HttpStatusCode.Unauthorized + ) { + return false; + } else { + return true; + } + } + }; + +} +/** + * [Note: Handle 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 this page, however that would add latency to the 99% user + * experience (of normal unlocks) for the 1% case (they've changed their + * password elsewhere). + * + * Another alternative would be to non-blockingly check if their password has + * changed (e.g. by comparing the remote and local SRP attributes), and if so, + * log them out. This would be a robust solution, except users might've chosen + * the "Don't log me out of other devices" option when changing their password + * from the mobile app. + * + * The approach we instead use is to make an non-blocking /session-validity API + * call when this page is loaded. This 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. + * + * It does not take any parameters because it reads the current state (key + * attributes) from local storage. + * + * @returns true if the session is valid, false if the session is invalid, and + * the (new) remote {@link KeyAttributes} if they've changed. + * + * @throws Exceptions if something goes wrong (it doesn't attempt to swallow + * failures, it is upto the caller to decide how to deal with failures in + * determining session validity). + */ +const checkSessionValidity = async (): boolean | KeyAttributes => { + const user = getData(LS_KEYS.USER); + if (!user?.email) return "invalid"; + + try { + const serverAttributes = await getSRPAttributes(email); + // (Arbitrarily) compare the salt to figure out if something changed + // (salt will always change on password changes). + if (serverAttributes?.kekSalt !== localSRPAttributes.kekSalt) + return true; /* password indeed did change */ + return false; + } catch (e) { + // Ignore errors here. In rare cases, the stars may align and cause the + // API calls to fail in that 1 case where the user indeed changed their + // password, but we also don't want to start logging people out for + // harmless transient issues like network errors. + log.error("Failed to compare SRP attributes", e); + return false; + } +}; +