Skip to content

Commit

Permalink
Merge pull request #73 from near/disable-auto-delete-lak
Browse files Browse the repository at this point in the history
Disable auto delete lak, decoupling FAK, LAK on firestore, improvements on Devices page
  • Loading branch information
hcho112 authored Oct 5, 2023
2 parents 19b313d + 85c90cd commit 6d14279
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 50 deletions.
7 changes: 5 additions & 2 deletions src/components/AddDevice/AddDevice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { openToast } from '../../lib/Toast';
import { useAuthState } from '../../lib/useAuthState';
import { decodeIfTruthy, inIframe } from '../../utils';
import { basePath } from '../../utils/config';
import { checkFirestoreReady, firebaseAuth } from '../../utils/firebase';
import { checkFirestoreReady, firebaseAuth, getDomain } from '../../utils/firebase';
import { isValidEmail } from '../../utils/form-validation';

const StyledContainer = styled.div`
Expand Down Expand Up @@ -228,9 +228,12 @@ function SignInPage() {
// @ts-ignore
oidcToken: user.accessToken,
});

// Since FAK is already added, we only add LAK
return window.firestoreController.addDeviceCollection({
fakPublicKey: publicKeyFak,
fakPublicKey: null,
lakPublicKey: public_key,
gateway: success_url,
})
.then(() => {
const parsedUrl = new URL(success_url || window.location.origin);
Expand Down
61 changes: 42 additions & 19 deletions src/components/AuthCallback/AuthCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import styled from 'styled-components';
import FastAuthController from '../../lib/controller';
import FirestoreController from '../../lib/firestoreController';
import { openToast } from '../../lib/Toast';
import { decodeIfTruthy } from '../../utils';
import { decodeIfTruthy, inIframe } from '../../utils';
import { network, networkId } from '../../utils/config';
import { checkFirestoreReady, firebaseAuth } from '../../utils/firebase';
import { checkFirestoreReady, firebaseAuth, getDomain } from '../../utils/firebase';
import {
CLAIM, getAddKeyAction, getUserCredentialsFrpSignature, getDeleteKeysAction
CLAIM, getAddKeyAction, getUserCredentialsFrpSignature, getDeleteKeysAction, getAddLAKAction
} from '../../utils/mpc-service';

const StyledStatusMessage = styled.div`
Expand All @@ -36,6 +36,7 @@ const onCreateAccount = async ({
success_url,
setStatusMessage,
email,
gateway,
}) => {
const CLAIM_SALT = CLAIM + 2;
const signature = getUserCredentialsFrpSignature({
Expand Down Expand Up @@ -78,10 +79,14 @@ const onCreateAccount = async ({
if (!response?.ok) {
throw new Error('Network response was not ok');
}
if (!window.firestoreController) {
(window as any).firestoreController = new FirestoreController();
}
// Add device
await window.firestoreController.addDeviceCollection({
fakPublicKey: publicKeyFak,
lakPublicKey: public_key_lak,
gateway,
});

setStatusMessage('Account created successfully!');
Expand Down Expand Up @@ -118,6 +123,8 @@ export const onSignIn = async ({
email,
searchParams,
navigate,
onlyAddLak = false,
gateway,
}) => {
const recoveryPK = await window.fastAuthController.getUserCredential(accessToken);
const accountIds = await fetch(`${network.fastAuth.authHelperUrl}/publicKey/${recoveryPK}/accounts`)
Expand All @@ -127,25 +134,32 @@ export const onSignIn = async ({
throw new Error('Unable to retrieve account Id');
});

const existingDevice = await window.firestoreController.getDeviceCollection(publicKeyFak);

// delete old lak key attached to webAuthN public Key
const deleteKeyActions = existingDevice
? getDeleteKeysAction(existingDevice.publicKeys.filter((key) => key !== publicKeyFak)) : [];

const addKeyActions = getAddKeyAction({
publicKeyLak: public_key_lak,
webAuthNPublicKey: publicKeyFak,
contractId: contract_id,
methodNames,
allowance: new BN('250000000000000'),
});
// TODO: If we want to remove old LAK automatically, use below code and add deleteKeyActions to signAndSendActionsWithRecoveryKey
// const existingDevice = await window.firestoreController.getDeviceCollection(publicKeyFak);
// // delete old lak key attached to webAuthN public Key
// const deleteKeyActions = existingDevice
// ? getDeleteKeysAction(existingDevice.publicKeys.filter((key) => key !== publicKeyFak)) : [];

// onlyAddLak will be true if current browser already has a FAK with passkey
const addKeyActions = onlyAddLak
? getAddLAKAction({
publicKeyLak: public_key_lak,
contractId: contract_id,
methodNames,
allowance: new BN('250000000000000'),
}) : getAddKeyAction({
publicKeyLak: public_key_lak,
webAuthNPublicKey: publicKeyFak,
contractId: contract_id,
methodNames,
allowance: new BN('250000000000000'),
});

return (window as any).fastAuthController.signAndSendActionsWithRecoveryKey({
oidcToken: accessToken,
accountId: accountIds[0],
recoveryPK,
actions: [...deleteKeyActions, ...addKeyActions]
actions: addKeyActions
})
.then((res) => res.json())
.then(async (res) => {
Expand All @@ -155,9 +169,13 @@ export const onSignIn = async ({
navigate(`/devices?${searchParams.toString()}`);
} else {
await checkFirestoreReady();
if (!window.firestoreController) {
(window as any).firestoreController = new FirestoreController();
}
await window.firestoreController.addDeviceCollection({
fakPublicKey: publicKeyFak,
fakPublicKey: onlyAddLak ? null : publicKeyFak,
lakPublicKey: public_key_lak,
gateway,
});

setStatusMessage('Account recovered successfully!');
Expand All @@ -172,7 +190,11 @@ export const onSignIn = async ({
parsedUrl.searchParams.set('public_key', public_key_lak);
parsedUrl.searchParams.set('all_keys', [public_key_lak, publicKeyFak].join(','));

window.location.replace(parsedUrl.href);
if (inIframe()) {
window.open(parsedUrl.href, '_parent');
} else {
window.location.replace(parsedUrl.href);
}
}
});
};
Expand Down Expand Up @@ -252,6 +274,7 @@ function AuthCallbackPage() {
email,
navigate,
searchParams,
gateway: success_url,
});
} catch (e) {
console.log('error:', e);
Expand Down
61 changes: 46 additions & 15 deletions src/components/Devices/Devices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ import { useAuthState } from '../../lib/useAuthState';
import { decodeIfTruthy } from '../../utils';
import { networkId } from '../../utils/config';
import { onSignIn } from '../AuthCallback/AuthCallback';
import { getDomain } from '../../utils/firebase';

const Title = styled.h1`
padding-bottom: 20px;
`;

const Description = styled.p`
font-size: 16px;
font-weight: bold;
`;

const StyledCheckbox = styled.input`
width: 20px;
Expand All @@ -17,11 +27,21 @@ const StyledCheckbox = styled.input`
border: initial;
appearance: auto;
cursor: pointer;
margin-top: 0;
margin-right: 10px;
`;

const Row = styled.div`
display: flex;
align-items: center;
padding-bottom: 10px;
`;

const DeleteButton = styled.button`
width: 100%;
margin-top: 30px;
outline: none;
font-size: 20px;
`;

function Devices() {
Expand Down Expand Up @@ -49,15 +69,17 @@ function Devices() {
const getCollection = async () => {
setIsLoaded(true);
const privateKey = window.localStorage.getItem(`temp_fastauthflow_${publicKeyFak}`);
const accountId = await controller.getAccountIdFromOidcToken();
if (privateKey) {
const accountId = await controller.getAccountIdFromOidcToken();

// claim the oidc token
(window as any).fastAuthController = new FastAuthController({
accountId,
networkId
});
const keypair = new KeyPairEd25519(privateKey.split(':')[1]);
await window.fastAuthController.setKey(keypair);
// claim the oidc token
(window as any).fastAuthController = new FastAuthController({
accountId,
networkId
});
const keypair = new KeyPairEd25519(privateKey.split(':')[1]);
await window.fastAuthController.setKey(keypair);
}

const deviceCollections = await controller.listDevices();
setIsLoaded(false);
Expand Down Expand Up @@ -94,15 +116,17 @@ function Devices() {

await onSignIn({
accessToken: oidcToken,
publicKeyFak,
public_key_lak,
publicKeyFak: publicKeyFak !== 'null' ? publicKeyFak : await window.fastAuthController.getPublicKey(),
public_key_lak: public_key_lak !== 'null' ? public_key_lak : decodeIfTruthy(searchParams.get('public_key')),
contract_id,
methodNames,
setStatusMessage: () => null,
success_url,
email,
searchParams,
navigate,
onlyAddLak: publicKeyFak === 'null',
gateway: success_url,
});
setIsAddingKey(false);
}).catch((err) => {
Expand All @@ -113,21 +137,28 @@ function Devices() {

return (
<>
<div>Devices route</div>
<Title>Devices with Keys</Title>

{isLoaded && <div>Loading...</div>}

{collections.length > 0 && (
<Description>
You have reached maximum number of keys. Please delete some keys to add new keys.
</Description>
)}
{
collections.map((collection) => (
<Row key={collection.id}>
<StyledCheckbox type="checkbox" id={collection.id} onChange={() => onClick(collection.id)} checked={deleteCollections.includes(collection.id)} />
<label htmlFor={collection.id}>{collection.label}</label>
<label htmlFor={collection.id} title={`Created At: ${collection.createdAt}`}>{collection.label}</label>
</Row>
))
}
{
collections.length > 0 && (
<button type="button" onClick={onDeleteCollections} disabled={!deleteCollections.length || isDeleted}>
Delete collections
</button>
<DeleteButton type="button" onClick={onDeleteCollections} disabled={!deleteCollections.length || isDeleted}>
{`Delete key${deleteCollections.length > 1 ? 's' : ''}`}
</DeleteButton>
)
}
{isDeleted && <div>Deleting...</div>}
Expand Down
52 changes: 38 additions & 14 deletions src/lib/firestoreController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,40 @@ class FirestoreController {
async addDeviceCollection({
fakPublicKey,
lakPublicKey,
gateway,
}) {
const parser = new UAParser();
const device = parser.getDevice();
const os = parser.getOS();
const browser = parser.getBrowser();

const dateTime = new Date().toISOString();
// setDoc will overwrite existing document or create new if not exist
return setDoc(doc(this.firestore, `/users/${this.userUid}/devices`, fakPublicKey), {
device: `${device.vendor} ${device.model}`,
os: `${os.name} ${os.version}`,
browser: `${browser.name} ${browser.version}`,
publicKeys: [fakPublicKey, lakPublicKey],
uid: this.userUid,
}, { merge: true }).catch((err) => {
return Promise.all([
...(fakPublicKey ? [
setDoc(doc(this.firestore, `/users/${this.userUid}/devices`, fakPublicKey), {
device: `${device.vendor} ${device.model}`,
os: `${os.name} ${os.version}`,
browser: `${browser.name} ${browser.version}`,
publicKeys: [fakPublicKey],
uid: this.userUid,
gateway: gateway || 'Unknown Gateway',
dateTime,
keyType: 'fak',
}, { merge: true })
] : []),
...(lakPublicKey ? [
setDoc(doc(this.firestore, `/users/${this.userUid}/devices`, lakPublicKey), {
device: `${device.vendor} ${device.model}`,
os: `${os.name} ${os.version}`,
browser: `${browser.name} ${browser.version}`,
publicKeys: [lakPublicKey],
uid: this.userUid,
gateway: gateway || 'Unknown Gateway',
dateTime,
keyType: 'lak',
}, { merge: true })
] : [])
]).catch((err) => {
console.log('fail to add device collection, ', err);
throw new Error('fail to add device collection');
});
Expand Down Expand Up @@ -92,7 +112,8 @@ class FirestoreController {
...data,
firebaseId: document.id,
id: data.publicKeys[0],
label: `${data.device} - ${data.browser} - ${data.os}`,
label: `${data.gateway || 'Unknown Gateway'} (${data.keyType || 'Unknown Key Type'}) ${data.device} - ${data.browser} - ${data.os}`,
createdAt: data.dateTime ? new Date(data.dateTime) : 'Unknown',
});
});
// claim oidc token before getting all access keys
Expand Down Expand Up @@ -123,6 +144,7 @@ class FirestoreController {
id: key,
firebaseId: null,
label: 'Unknown Device',
createdAt: 'Unknown',
publicKeys: [key],
}
];
Expand All @@ -143,11 +165,13 @@ class FirestoreController {
const firestoreIds = list
.map(({ firebaseId }) => firebaseId)
.filter((id) => id);
// delete all records except the one that has LAK
firestoreIds.forEach((id) => {
batch.delete(doc(this.firestore, `/users/${this.userUid}/devices`, id));
});
await batch.commit();
if (firestoreIds.length) {
// delete all records except the one that has LAK
firestoreIds.forEach((id) => {
batch.delete(doc(this.firestore, `/users/${this.userUid}/devices`, id));
});
await batch.commit();
}
} catch (err) {
console.log('Fail to delete firestore records', err);
throw new Error(err);
Expand Down
10 changes: 10 additions & 0 deletions src/utils/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ export const checkFirestoreReady = async () => firebaseAuth.authStateReady()
}
return false;
});

export const getDomain = (url) => {
let urlObj;
try {
urlObj = new URL(url);
} catch {
return false;
}
return urlObj.hostname.replace('www.', '');
};
3 changes: 3 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export type Device = {
browser: string;
publicKeys: string[];
uid: string;
gateway: string | null;
dateTime: Date;
keyType: string;
};

export type DeleteDevice = {
Expand Down

0 comments on commit 6d14279

Please sign in to comment.