From bc59cd5c410ae8537af2963f7f3f565800570ca5 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 19 Jul 2024 12:44:54 -0600 Subject: [PATCH] cache the passkey JWANT access token for multiple signatures --- src/db/tables/settings.ts | 5 +- src/libs/crypto/index.ts | 2 +- src/libs/endorserServer.ts | 52 ++++++- src/libs/util.ts | 16 +- src/views/AccountViewView.vue | 250 +++++++++++++------------------ src/views/ContactAmountsView.vue | 15 +- src/views/GiftedDetails.vue | 10 +- src/views/NewEditProjectView.vue | 24 +-- src/views/ProjectViewView.vue | 6 - src/views/ProjectsView.vue | 28 ++-- src/views/SharedPhotoView.vue | 7 +- 11 files changed, 196 insertions(+), 219 deletions(-) diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index ac5cde95b..35c428f93 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -26,6 +26,7 @@ export type Settings = { lastName?: string; // deprecated - put all names in firstName lastNotifiedClaimId?: string; lastViewedClaimId?: string; + passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes profileImageUrl?: string; reminderTime?: number; // Time in milliseconds since UNIX epoch for reminders reminderOn?: boolean; // Toggle to enable or disable reminders @@ -46,7 +47,7 @@ export type Settings = { }; export function isAnyFeedFilterOn(settings: Settings): boolean { - return !!(settings.filterFeedByNearby || settings.filterFeedByVisible); + return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible); } /** @@ -60,3 +61,5 @@ export const SettingsSchema = { * Constants. */ export const MASTER_SETTINGS_KEY = 1; + +export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15; diff --git a/src/libs/crypto/index.ts b/src/libs/crypto/index.ts index 862ed5416..2ac8aef79 100644 --- a/src/libs/crypto/index.ts +++ b/src/libs/crypto/index.ts @@ -85,7 +85,7 @@ export const generateSeed = (): string => { }; /** - * Retreive an access token + * Retrieve an access token, or "" if no DID is provided. * * @return {*} */ diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 26954cd69..754766b6e 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -5,9 +5,10 @@ import * as R from "ramda"; import { DEFAULT_IMAGE_API_SERVER } from "@/constants/app"; import { Contact } from "@/db/tables/contacts"; import { accessToken } from "@/libs/crypto"; -import { NonsensitiveDexie } from "@/db/index"; -import { getAccount } from "@/libs/util"; +import { db, NonsensitiveDexie } from "@/db/index"; +import { getAccount, getPasskeyExpirationSeconds } from "@/libs/util"; import { createEndorserJwtForKey, KeyMeta } from "@/libs/crypto/vc"; +import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; export const SCHEMA_ORG_CONTEXT = "https://schema.org"; // the object in RegisterAction claims @@ -447,12 +448,57 @@ export function didInfo( return didInfoForContact(did, activeDid, contact, allMyDids).displayName; } +let passkeyAccessToken: string = ""; +let passkeyTokenExpirationEpochSeconds: number = 0; + +export function clearPasskeyToken() { + passkeyAccessToken = ""; + passkeyTokenExpirationEpochSeconds = 0; +} + +export function tokenExpiryTimeDescription() { + if ( + !passkeyAccessToken || + passkeyTokenExpirationEpochSeconds < new Date().getTime() / 1000 + ) { + return "Token has expired"; + } else { + return ( + "Token expires at " + + new Date(passkeyTokenExpirationEpochSeconds * 1000).toLocaleString() + ); + } +} + +/** + * Get the headers for a request, potentially including Authorization + */ export async function getHeaders(did?: string) { const headers: { "Content-Type": string; Authorization?: string } = { "Content-Type": "application/json", }; if (did) { - const token = await accessToken(did); + let token; + const account = await getAccount(did); + if (account?.passkeyCredIdHex) { + if ( + passkeyAccessToken && + passkeyTokenExpirationEpochSeconds > Date.now() / 1000 + ) { + // there's an active current passkey token + token = passkeyAccessToken; + } else { + // there's no current passkey token or it's expired + token = await accessToken(did); + + passkeyAccessToken = token; + const passkeyExpirationSeconds = await getPasskeyExpirationSeconds(); + passkeyTokenExpirationEpochSeconds = + Date.now() / 1000 + passkeyExpirationSeconds; + } + } else { + token = await accessToken(did); + } headers["Authorization"] = "Bearer " + token; } else { // it's often OK to request without auth; we assume necessary checks are done earlier diff --git a/src/libs/util.ts b/src/libs/util.ts index c0292ac07..c65268362 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -1,21 +1,20 @@ // many of these are also found in endorser-mobile utility.ts import axios, { AxiosResponse } from "axios"; -import { IIdentifier } from "@veramo/core"; import { useClipboard } from "@vueuse/core"; import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { Account } from "@/db/tables/accounts"; -import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; +import {DEFAULT_PASSKEY_EXPIRATION_MINUTES, MASTER_SETTINGS_KEY} from "@/db/tables/settings"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { GenericCredWrapper, containsHiddenDid } from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer"; import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; import { Buffer } from "buffer"; -import {KeyMeta} from "@/libs/crypto/vc"; -import {createPeerDid} from "@/libs/crypto/vc/didPeer"; +import { KeyMeta } from "@/libs/crypto/vc"; +import { createPeerDid } from "@/libs/crypto/vc/didPeer"; export const PRIVACY_MESSAGE = "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; @@ -273,6 +272,15 @@ export const registerSaveAndActivatePasskey = async ( return account; }; +export const getPasskeyExpirationSeconds = async (): Promise => { + await db.open(); + const settings = await db.settings.get(MASTER_SETTINGS_KEY); + const passkeyExpirationSeconds = + (settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) * + 60; + return passkeyExpirationSeconds; +}; + export const sendTestThroughPushServer = async ( subscriptionJSON: PushSubscriptionJSON, skipFilter: boolean, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index d32267eae..2f997910b 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -152,7 +152,7 @@
- Activity + Your Activity
@@ -216,7 +216,6 @@
Location
Set Search Area… @@ -622,6 +621,26 @@ +
+ + + Passkey Expiration Minutes + +
+ + {{ passkeyExpirationDescription }} + +
+
+ +
+
+