2024-09-07 13:27:23 +05:30

117 lines
4.0 KiB
TypeScript

import { fromB64URLSafeNoPaddingString } from "@/base/crypto/libsodium";
import log from "@/base/log";
import { nullToUndefined } from "@/utils/transform";
import { VerticallyCentered } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import {
LS_KEYS,
getData,
setData,
setLSUser,
} from "@ente/shared/storage/localStorage";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { PAGES } from "../../constants/pages";
import { unstashRedirect } from "../../services/redirect";
import type { PageProps } from "../../types/page";
/**
* [Note: Finish passkey flow in the requesting app]
*
* The passkey finish step needs to happen in the context of the client which
* invoked the passkey flow since it needs to save the obtained credentials
* in local storage (which is tied to the current origin).
*/
const Page: React.FC<PageProps> = () => {
const router = useRouter();
useEffect(() => {
// Extract response from query params
const searchParams = new URLSearchParams(window.location.search);
const passkeySessionID = searchParams.get("passkeySessionID");
const response = searchParams.get("response");
if (!passkeySessionID || !response) return;
saveCredentialsAndNavigateTo(passkeySessionID, response).then(
(slug: string) => {
router.push(slug);
},
);
}, []);
return (
<VerticallyCentered>
<EnteSpinner />
</VerticallyCentered>
);
};
export default Page;
/**
* Extract credentials from a successful passkey flow "response" query parameter
* and save them to local storage for use by subsequent steps (or normal
* functioning) of the app.
*
* @param passkeySessionID The string that is passed as the "passkeySessionID"
* query parameter to us.
*
* @param response The string that is passed as the "response" query parameter to
* us (we're the final "finish" page in the passkey flow).
*
* @returns the slug that we should navigate to now.
*/
const saveCredentialsAndNavigateTo = async (
passkeySessionID: string,
response: string,
) => {
const inflightPasskeySessionID = nullToUndefined(
sessionStorage.getItem("inflightPasskeySessionID"),
);
if (
!inflightPasskeySessionID ||
passkeySessionID != inflightPasskeySessionID
) {
// This is not the princess we were looking for. However, we have
// already entered this castle. Redirect back to home without changing
// any state, hopefully this will get the user back to where they were.
log.info(
`Ignoring redirect for unexpected passkeySessionID ${passkeySessionID}`,
);
return "/";
}
sessionStorage.removeItem("inflightPasskeySessionID");
// Decode response string (inverse of the steps we perform in
// `passkeyAuthenticationSuccessRedirectURL`).
const decodedResponse = JSON.parse(
await fromB64URLSafeNoPaddingString(response),
);
// Only one of `encryptedToken` or `token` will be present depending on the
// account's lifetime:
//
// - The plaintext "token" will be passed during fresh signups, where we
// don't yet have keys to encrypt it, the account itself is being created
// as we go through this flow.
// TODO(MR): Conceptually this cannot happen. During a _real_ fresh signup
// we'll never enter the passkey verification flow. Remove this code after
// making sure that it doesn't get triggered in cases where an existing
// user goes through the new user flow.
//
// - The encrypted `encryptedToken` will be present otherwise (i.e. if the
// user is signing into an existing account).
const { keyAttributes, encryptedToken, token, id } = decodedResponse;
await setLSUser({
...getData(LS_KEYS.USER),
token,
encryptedToken,
id,
});
setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
return unstashRedirect() ?? PAGES.CREDENTIALS;
};