/** * Copyright Amazon.com, Inc. and its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You * may not use this file except in compliance with the License. A copy of * the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF * ANY KIND, either express or implied. See the License for the specific * language governing permissions and limitations under the License. */ import { configure } from "../config.js"; import React, { useState, useEffect } from "react"; import { usePasswordless, useAwaitableState, useLocalUserCache, } from "./hooks.js"; import { timeAgo } from "../util.js"; interface CustomBrand { backgroundImageUrl?: string; customerName?: string; customerLogoUrl?: string; } const FlexContainer = (props: { children: React.ReactNode; brand?: CustomBrand; }) => { return (
{props.brand?.backgroundImageUrl && ( )}
{props.brand?.customerLogoUrl && ( )} {props.brand?.customerName && (
{props.brand.customerName}
)} {props.children}
); }; export const Passwordless = ({ brand, children, }: { brand?: CustomBrand; children?: React.ReactNode; }) => { const { requestSignInLink, lastError, authenticateWithFido2, busy, signInStatus, signingInStatus, tokens, tokensParsed, signOut, toggleShowAuthenticatorManager, showAuthenticatorManager, } = usePasswordless(); const [newUsername, setNewUsername] = useState(""); const [showUsernameInput, setShowUsernameInput] = useState(); const { lastSignedInUsers } = useLocalUserCache(); const { fido2 } = configure(); if (!fido2) { throw new Error("Missing Fido2 config"); } useEffect(() => { if (lastSignedInUsers !== undefined) { setShowUsernameInput(lastSignedInUsers.length === 0); } }, [lastSignedInUsers]); function signInWithMagicLinkOrFido2(username: string) { requestSignInLink(username).signInLinkRequested.catch((err) => { if ( err instanceof Error && err.message.toLowerCase().includes("you must sign-in with fido2") ) { return authenticateWithFido2({ username, }).catch(() => undefined); } throw err; }); } if (signInStatus === "REFRESHING_SIGN_IN" && children) { return <>{children}; } if ( signInStatus === "CHECKING" || signInStatus === "REFRESHING_SIGN_IN" || !lastSignedInUsers ) { return (
Checking your sign-in status...
); } if (signingInStatus === "SIGNING_IN_WITH_LINK") { return (
Checking the sign-in link...
); } if (signingInStatus === "SIGNING_OUT") { return (
Signing out, please wait...
); } if (signingInStatus === "SIGNIN_LINK_REQUESTED") { return (
Please check your email.
We've emailed you a secret sign-in link
); } if (signInStatus === "SIGNED_IN" && tokens) { if (children) return <>{children}; return (
You're currently signed-in as:{" "} {tokensParsed?.idToken.email as string}
); } const user = lastSignedInUsers.at(0); const { email, credentials, useFido } = user ?? {}; return ( {signInStatus === "NOT_SIGNED_IN" && email && (
{email}

{useFido === "YES" && ( )}

{showUsernameInput === false ? ( <>
setShowUsernameInput(true)} > Sign-in as another user
) : ( <>
{ e.preventDefault(); signInWithMagicLinkOrFido2(newUsername); return false; }} > setNewUsername(e.target.value)} placeholder="e-mail" type="email" disabled={busy} autoFocus />
)}
)} {signInStatus === "NOT_SIGNED_IN" && !lastSignedInUsers.length && (
{ e.preventDefault(); signInWithMagicLinkOrFido2(newUsername); return false; }} > setNewUsername(e.target.value)} placeholder="E-mail" type="email" disabled={busy} autoFocus />
)}
{signingInStatus === "SIGNIN_LINK_EXPIRED" && (
Authentication error.
The sign-in link you tried to use is no longer valid
)} {signingInStatus === "REQUESTING_SIGNIN_LINK" && ( <>
Starting sign-in...
)} {signingInStatus === "STARTING_SIGN_IN_WITH_FIDO2" && ( <>
Starting sign-in...
)} {signingInStatus === "COMPLETING_SIGN_IN_WITH_FIDO2" && ( <>
Completing your sign-in...
)} {lastError && (
{lastError.message}
)}
); }; function Fido2Recommendation() { const { fido2CreateCredential, showAuthenticatorManager, signInStatus } = usePasswordless(); const { currentUser, updateFidoPreference } = useLocalUserCache(); const [error, setError] = useState(); const [status, setStatus] = useState< "IDLE" | "STARTING" | "INPUT_NAME" | "COMPLETING" | "COMPLETED" >("IDLE"); useEffect(() => { if (status !== "COMPLETED") return; const i = setTimeout(reset, 10000); return () => clearTimeout(i); }, [status]); const [friendlyName, setFriendlyName] = useState(""); const { awaitable: awaitableFriendlyName, resolve: resolveFriendlyName } = useAwaitableState(friendlyName); const mobileDeviceName = determineMobileDeviceName(); function reset() { setError(undefined); setStatus("IDLE"); setFriendlyName(""); } useEffect(() => { if (showAuthenticatorManager) { reset(); } }, [showAuthenticatorManager]); if (showAuthenticatorManager) return null; const show = signInStatus === "SIGNED_IN" && currentUser && (currentUser.useFido === "ASK" || status === "COMPLETED"); if (!show) return null; return (
{(status === "IDLE" || status === "STARTING") && ( <>
We recommend increasing the security of your account by adding face or touch unlock for this website.
{ updateFidoPreference({ useFido: "NO" }); reset(); }} className="passwordless-link" > close
)} {(status === "INPUT_NAME" || status === "COMPLETING") && (
{ e.preventDefault(); resolveFriendlyName(); setStatus("COMPLETING"); return false; }} >
Provide a name for this authenticator, so you can recognize it easily later
setFriendlyName(e.target.value)} />
{ updateFidoPreference({ useFido: "NO" }); reset(); }} > cancel
)} {status === "COMPLETED" && ( <> {" "}
{error ? `Failed to activate face or touch unlock: ${error.message}` : "Face or touch unlock activated successfully"}
close
)}
); } function AuthenticatorsManager() { const { fido2CreateCredential, fido2Credentials, showAuthenticatorManager, toggleShowAuthenticatorManager, signInStatus, } = usePasswordless(); const { updateFidoPreference } = useLocalUserCache(); const [error, setError] = useState(); const [addingAuthenticatorStatus, setAddingAuthenticatorStatus] = useState< "IDLE" | "STARTING" | "INPUT_NAME" | "COMPLETING" >("IDLE"); const [confirmDeleteRowIndex, setConfirmDeleteRowIndex] = useState(-1); const [friendlyName, setFriendlyName] = useState(""); const [editFriendlyNameRowIndex, setEditFriendlyNameRowIndex] = useState(-1); const [editedFriendlyName, setEditedFriendlyName] = useState(""); const { awaitable: awaitableFriendlyName, resolve: resolveFriendlyName } = useAwaitableState(friendlyName); const mobileDeviceName = determineMobileDeviceName(); const [time, setTime] = useState(new Date()); function reset() { setError(undefined); setConfirmDeleteRowIndex(-1); setEditFriendlyNameRowIndex(-1); setAddingAuthenticatorStatus("IDLE"); setFriendlyName(""); setEditedFriendlyName(""); } useEffect(() => { if (showAuthenticatorManager) { reset(); } }, [showAuthenticatorManager]); useEffect(() => { if (showAuthenticatorManager && signInStatus === "NOT_SIGNED_IN") { toggleShowAuthenticatorManager(); } }, [signInStatus, showAuthenticatorManager, toggleShowAuthenticatorManager]); useEffect(() => { const intervalId = setInterval(() => setTime(new Date()), 1000); return () => clearInterval(intervalId); }, []); if (!showAuthenticatorManager) return null; const status = { isAddingAuthenticator: addingAuthenticatorStatus !== "IDLE", isDeletingAuthenticator: confirmDeleteRowIndex !== -1, isEditingAuthenticator: editFriendlyNameRowIndex !== -1, }; return (
{(addingAuthenticatorStatus === "IDLE" || addingAuthenticatorStatus === "STARTING") && ( <> {fido2Credentials?.length === 0 && (
You don't have any authenticators yet. Press the button{" "} "Register new authenticator" to get started.
)} {!!fido2Credentials?.length && ( {fido2Credentials.map((credential, index) => editFriendlyNameRowIndex === index ? ( ) : confirmDeleteRowIndex === index ? ( ) : ( ) )}
Last sign-in Created at
{ e.preventDefault(); setError(undefined); credential .update({ friendlyName: editedFriendlyName, }) .then(reset) .catch(setError); return false; }} > setEditedFriendlyName(e.currentTarget.value) } />
{" "} Are you sure you want to delete your device named{" "} "{credential.friendlyName}" ?{" "}
{timeAgo(time, credential.lastSignIn) || "Never"} {timeAgo(time, credential.createdAt) || "Unknown"}
)} )}
{(addingAuthenticatorStatus === "IDLE" || addingAuthenticatorStatus === "STARTING") && ( )} {(addingAuthenticatorStatus === "INPUT_NAME" || addingAuthenticatorStatus === "COMPLETING") && (
{ e.preventDefault(); resolveFriendlyName(); setAddingAuthenticatorStatus("COMPLETING"); return false; }} >
Provide a name for this authenticator, so you can recognize it easily later
setFriendlyName(e.target.value)} />
)}
toggleShowAuthenticatorManager()} className="passwordless-link" > close
{error && (
{error.message}
)}
); } export function Fido2Toast() { return ( <> ); } function determineMobileDeviceName() { const mobileDevices = [ "Android", "webOS", "iPhone", "iPad", "iPod", "BlackBerry", "Windows Phone", ] as const; return mobileDevices.find((dev) => // eslint-disable-next-line security/detect-non-literal-regexp navigator.userAgent.match(new RegExp(dev, "i")) ); }