From be1d16f563436a70578f3ad0477ec0c6b5dea324 Mon Sep 17 00:00:00 2001 From: Incorbador Date: Tue, 17 Dec 2024 14:39:00 +0100 Subject: [PATCH 1/4] Update process lifetime for connect --- .../src/models/connect/connectProcess.ts | 65 ++++++++++++------- packages/web-core/src/models/connect/login.ts | 3 + .../web-core/src/services/ConnectService.ts | 65 ++++++++++--------- 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/packages/web-core/src/models/connect/connectProcess.ts b/packages/web-core/src/models/connect/connectProcess.ts index f09f2f8cf..27b466a8b 100644 --- a/packages/web-core/src/models/connect/connectProcess.ts +++ b/packages/web-core/src/models/connect/connectProcess.ts @@ -6,7 +6,6 @@ export class ConnectProcess { readonly id: string; readonly projectId: string; readonly frontendApiUrl: string; - readonly expiresAt: number; readonly loginData: ConnectLoginInitData | null; readonly appendData: ConnectAppendInitData | null; readonly manageData: ConnectManageInitData | null; @@ -14,7 +13,6 @@ export class ConnectProcess { constructor( id: string, projectId: string, - expiresAt: number, frontendApiUrl: string, loginData: ConnectLoginInitData | null, appendData: ConnectAppendInitData | null, @@ -22,7 +20,6 @@ export class ConnectProcess { ) { this.id = id; this.projectId = projectId; - this.expiresAt = expiresAt; this.frontendApiUrl = frontendApiUrl; this.loginData = loginData; this.appendData = appendData; @@ -30,26 +27,17 @@ export class ConnectProcess { } isValid(): boolean { - return this.expiresAt > Date.now() / 1000 + 10; + return true; } resetLoginData(): ConnectProcess { - return new ConnectProcess( - this.id, - this.projectId, - this.expiresAt, - this.frontendApiUrl, - null, - this.appendData, - this.manageData, - ); + return new ConnectProcess(this.id, this.projectId, this.frontendApiUrl, null, this.appendData, this.manageData); } - copyWithLoginData(loginData: ConnectLoginInitData, expiresAt: number): ConnectProcess { + copyWithLoginData(loginData: ConnectLoginInitData): ConnectProcess { return new ConnectProcess( this.id, this.projectId, - expiresAt, this.frontendApiUrl, loginData, this.appendData, @@ -57,11 +45,10 @@ export class ConnectProcess { ); } - copyWithAppendData(appendData: ConnectAppendInitData, expiresAt: number): ConnectProcess { + copyWithAppendData(appendData: ConnectAppendInitData): ConnectProcess { return new ConnectProcess( this.id, this.projectId, - expiresAt, this.frontendApiUrl, this.loginData, appendData, @@ -69,11 +56,10 @@ export class ConnectProcess { ); } - copyWithManageData(manageData: ConnectManageInitData, expiresAt: number): ConnectProcess { + copyWithManageData(manageData: ConnectManageInitData): ConnectProcess { return new ConnectProcess( this.id, this.projectId, - expiresAt, this.frontendApiUrl, this.loginData, this.appendData, @@ -81,14 +67,50 @@ export class ConnectProcess { ); } + getValidLoginData(): ConnectLoginInitData | undefined { + if (!this.loginData || !this.loginData.expiresAt) { + return; + } + + if (this.loginData.expiresAt < Date.now() / 1000) { + return; + } + + return this.loginData; + } + + getValidAppendData(): ConnectAppendInitData | undefined { + if (!this.appendData || !this.appendData.expiresAt) { + return; + } + + if (this.appendData.expiresAt < Date.now() / 1000) { + return; + } + + return this.appendData; + } + + getValidManageData(): ConnectManageInitData | undefined { + if (!this.manageData || !this.manageData.expiresAt) { + return; + } + + if (this.manageData.expiresAt < Date.now() / 1000) { + return; + } + + return this.manageData; + } + static loadFromStorage(projectId: string): ConnectProcess | undefined { const serialized = localStorage.getItem(getStorageKey(projectId)); if (!serialized) { return undefined; } - const { id, expiresAt, frontendApiUrl, loginData, appendData, manageData } = JSON.parse(serialized); - const process = new ConnectProcess(id, projectId, expiresAt, frontendApiUrl, loginData, appendData, manageData); + const { id, frontendApiUrl, loginData, appendData, manageData } = JSON.parse(serialized); + const process = new ConnectProcess(id, projectId, frontendApiUrl, loginData, appendData, manageData); if (!process.isValid()) { return undefined; } @@ -101,7 +123,6 @@ export class ConnectProcess { getStorageKey(this.projectId), JSON.stringify({ id: this.id, - expiresAt: this.expiresAt, frontendApiUrl: this.frontendApiUrl, loginData: this.loginData, appendData: this.appendData, diff --git a/packages/web-core/src/models/connect/login.ts b/packages/web-core/src/models/connect/login.ts index 034398511..9bfc1ded9 100644 --- a/packages/web-core/src/models/connect/login.ts +++ b/packages/web-core/src/models/connect/login.ts @@ -4,16 +4,19 @@ export type ConnectLoginInitData = { loginAllowed: boolean; conditionalUIChallenge: string | null; flags: Record; + expiresAt?: number; }; export interface ConnectAppendInitData { appendAllowed: boolean; flags: Record; + expiresAt?: number; } export interface ConnectManageInitData { manageAllowed: boolean; flags: Record; + expiresAt?: number; } export interface ConnectManageListData { diff --git a/packages/web-core/src/services/ConnectService.ts b/packages/web-core/src/services/ConnectService.ts index 4e8974dd3..37afab264 100644 --- a/packages/web-core/src/services/ConnectService.ts +++ b/packages/web-core/src/services/ConnectService.ts @@ -22,7 +22,6 @@ import type { ConnectManageListRsp, } from '../api/v2'; import { CorbadoConnectApi, PasskeyEventType } from '../api/v2'; -import type { AuthProcess } from '../models/authProcess'; import { ConnectFlags } from '../models/connect/connectFlags'; import { ConnectInvitation } from '../models/connect/connectInvitation'; import { ConnectLastLogin } from '../models/connect/connectLastLogin'; @@ -93,7 +92,7 @@ export class ConnectService { return out; } - #setApisV2(process?: AuthProcess): void { + #setApisV2(process?: ConnectProcess): void { let frontendApiUrl = this.#getDefaultFrontendApiUrl(); if (process?.frontendApiUrl && process?.frontendApiUrl.length > 0) { frontendApiUrl = process.frontendApiUrl; @@ -124,9 +123,11 @@ export class ConnectService { async loginInit(abortController: AbortController): Promise> { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); + const maybeLoginData = existingProcess?.getValidLoginData(); if ( - existingProcess?.loginData && - !existingProcess?.loginData.loginAllowed && + existingProcess && + maybeLoginData && + !maybeLoginData.loginAllowed && ConnectInvitation.loadFromStorage()?.token ) { existingProcess.resetLoginData().persistToStorage(); @@ -135,8 +136,8 @@ export class ConnectService { this.#setApisV2(existingProcess); // process has already been initialized - if (existingProcess?.loginData) { - return Ok(existingProcess.loginData); + if (maybeLoginData) { + return Ok(maybeLoginData); } } @@ -168,20 +169,24 @@ export class ConnectService { loginAllowed: res.val.loginAllowed, conditionalUIChallenge: res.val.conditionalUIChallenge ?? null, flags: flags.getItemsObject(), + expiresAt: res.val.expiresAt, }; - // update local state - const newProcess = new ConnectProcess( - res.val.token, - this.#projectId, - res.val.expiresAt, - res.val.frontendApiUrl, - loginData, - null, - null, - ); - this.#setApisV2(newProcess); - newProcess.persistToStorage(); + if (existingProcess && existingProcess.id === res.val.token) { + const p = existingProcess.copyWithLoginData(loginData); + p.persistToStorage(); + } else { + const newProcess = new ConnectProcess( + res.val.token, + this.#projectId, + res.val.frontendApiUrl, + loginData, + null, + null, + ); + this.#setApisV2(newProcess); + newProcess.persistToStorage(); + } // persist flags flags.persistToStorage(this.#projectId); @@ -192,7 +197,7 @@ export class ConnectService { async #getExistingProcess(generator: () => Promise>): Promise { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); if (existingProcess) { - log.debug('process found', existingProcess.expiresAt); + log.debug('process found'); return existingProcess; } @@ -287,8 +292,9 @@ export class ConnectService { this.#setApisV2(existingProcess); // process has already been initialized - if (existingProcess?.appendData) { - return Ok(existingProcess.appendData); + const maybeAppendData = existingProcess?.getValidAppendData(); + if (maybeAppendData) { + return Ok(maybeAppendData); } } @@ -311,17 +317,17 @@ export class ConnectService { const appendData: ConnectAppendInitData = { appendAllowed: res.val.appendAllowed, flags: flags.getItemsObject(), + expiresAt: res.val.expiresAt, }; // update local state with process - if (existingProcess) { - const p = existingProcess.copyWithAppendData(appendData, res.val.expiresAt); + if (existingProcess && existingProcess.id === res.val.processID) { + const p = existingProcess.copyWithAppendData(appendData); p.persistToStorage(); } else { const newProcess = new ConnectProcess( res.val.processID, this.#projectId, - res.val.expiresAt, res.val.frontendApiUrl, null, appendData, @@ -435,8 +441,9 @@ export class ConnectService { this.#setApisV2(existingProcess); // process has already been initialized - if (existingProcess?.manageData) { - return Ok(existingProcess.manageData); + const maybeManageData = existingProcess?.getValidManageData(); + if (maybeManageData) { + return Ok(maybeManageData); } } @@ -459,17 +466,17 @@ export class ConnectService { const manageData: ConnectManageInitData = { manageAllowed: res.val.manageAllowed, flags: flags.getItemsObject(), + expiresAt: res.val.expiresAt, }; // update local state with process - if (existingProcess) { - const p = existingProcess.copyWithManageData(manageData, res.val.expiresAt); + if (existingProcess && existingProcess.id === res.val.processID) { + const p = existingProcess.copyWithManageData(manageData); p.persistToStorage(); } else { const newProcess = new ConnectProcess( res.val.processID, this.#projectId, - res.val.expiresAt, res.val.frontendApiUrl, null, null, From ccb4f4ad908317db4881dab48f4a3cfc4758ade8 Mon Sep 17 00:00:00 2001 From: Incorbador Date: Wed, 18 Dec 2024 14:23:40 +0100 Subject: [PATCH 2/4] Improve error logging for connect --- .../append/AppendAfterErrorScreen.tsx | 4 +-- .../append/AppendAfterHybridLoginScreen.tsx | 4 +-- .../components/append/AppendInitScreen.tsx | 4 +-- .../components/login/LoginErrorScreenHard.tsx | 9 +++--- .../components/login/LoginErrorScreenSoft.tsx | 7 +++-- .../components/login/LoginHybridScreen.tsx | 7 +++-- .../src/components/login/LoginInitScreen.tsx | 9 ++++-- .../login/LoginPasskeyReLoginScreen.tsx | 7 +++-- .../passkeyList/PasskeyListScreen.tsx | 9 +++++- .../src/contexts/AppendProcessContext.ts | 4 +-- .../src/contexts/AppendProcessProvider.tsx | 17 +++++++---- packages/web-core/openapi/spec_v2.yaml | 4 ++- packages/web-core/src/api/v2/api.ts | 11 +++++++- .../src/models/connect/connectProcess.ts | 4 +++ .../web-core/src/services/ConnectService.ts | 28 +++++++++++++++---- playground/connect-next/app/login/actions.ts | 9 +++++- 16 files changed, 98 insertions(+), 39 deletions(-) diff --git a/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx b/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx index 86c6d2ca6..c47987bba 100644 --- a/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx +++ b/packages/connect-react/src/components/append/AppendAfterErrorScreen.tsx @@ -54,10 +54,10 @@ const AppendAfterErrorScreen = ({ attestationOptions }: { attestationOptions: st case AppendSituationCode.CtApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePostAuthenticator: - void handleErrorHard(situationCode); + void handleErrorHard(situationCode, false); break; case AppendSituationCode.ClientPasskeyOperationCancelled: - void handleErrorSoft(situationCode); + void handleErrorSoft(situationCode, true); break; case AppendSituationCode.ClientExcludeCredentialsMatch: void handleCredentialExistsError(); diff --git a/packages/connect-react/src/components/append/AppendAfterHybridLoginScreen.tsx b/packages/connect-react/src/components/append/AppendAfterHybridLoginScreen.tsx index 476aeac37..5f0e0394e 100644 --- a/packages/connect-react/src/components/append/AppendAfterHybridLoginScreen.tsx +++ b/packages/connect-react/src/components/append/AppendAfterHybridLoginScreen.tsx @@ -56,10 +56,10 @@ const AppendAfterHybridLoginScreen = ({ attestationOptions }: { attestationOptio case AppendSituationCode.CtApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePostAuthenticator: - void handleErrorHard(situationCode); + void handleErrorHard(situationCode, false); break; case AppendSituationCode.ClientPasskeyOperationCancelled: - void handleErrorSoft(situationCode); + void handleErrorSoft(situationCode, true); break; case AppendSituationCode.ClientExcludeCredentialsMatch: void handleCredentialExistsError(); diff --git a/packages/connect-react/src/components/append/AppendInitScreen.tsx b/packages/connect-react/src/components/append/AppendInitScreen.tsx index c39085607..5dfbf363f 100644 --- a/packages/connect-react/src/components/append/AppendInitScreen.tsx +++ b/packages/connect-react/src/components/append/AppendInitScreen.tsx @@ -165,12 +165,12 @@ const AppendInitScreen = () => { case AppendSituationCode.CtApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePreAuthenticator: case AppendSituationCode.CboApiNotAvailablePostAuthenticator: - void handleErrorHard(situationCode); + void handleErrorHard(situationCode, false); statefulLoader.current.finishWithError(); break; case AppendSituationCode.ClientPasskeyOperationCancelled: - void handleErrorSoft(situationCode); + void handleErrorSoft(situationCode, true); setAppendLoading(false); break; case AppendSituationCode.ClientExcludeCredentialsMatch: diff --git a/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx b/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx index 45769aaf7..c4f89d7f0 100644 --- a/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx +++ b/packages/connect-react/src/components/login/LoginErrorScreenHard.tsx @@ -48,7 +48,8 @@ const LoginErrorScreenHard = () => { }; const handleSituation = (situationCode: LoginSituationCode) => { - log.debug(`situation: ${situationCode}`); + const messageCode = `situation: ${situationCode}`; + log.debug(messageCode); const identifier = currentIdentifier; const message = getLoginErrorMessage(situationCode); @@ -58,21 +59,21 @@ const LoginErrorScreenHard = () => { case LoginSituationCode.CboApiNotAvailablePostAuthenticator: navigateToScreen(LoginScreenType.Invisible); config.onFallback(identifier, message); - void getConnectService().recordEventLoginErrorUntyped(); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); setLoading(false); break; case LoginSituationCode.ClientPasskeyOperationCancelledTooManyTimes: navigateToScreen(LoginScreenType.Invisible); config.onFallback(identifier, message); - void getConnectService().recordEventLoginError(); + void getConnectService().recordEventLoginError(messageCode); setLoading(false); break; case LoginSituationCode.ClientPasskeyOperationCancelled: setHardErrorCount(hardErrorCount + 1); - void getConnectService().recordEventLoginError(); + void getConnectService().recordEventLoginError(messageCode); setLoading(false); break; diff --git a/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx b/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx index 95cd433bb..9f83ac295 100644 --- a/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx +++ b/packages/connect-react/src/components/login/LoginErrorScreenSoft.tsx @@ -42,7 +42,8 @@ const LoginErrorScreenSoft = () => { }; const handleSituation = (situationCode: LoginSituationCode) => { - log.debug(`situation: ${situationCode}`); + const messageCode = `situation: ${situationCode}`; + log.debug(messageCode); const identifier = currentIdentifier; const message = getLoginErrorMessage(situationCode); @@ -52,14 +53,14 @@ const LoginErrorScreenSoft = () => { case LoginSituationCode.CboApiNotAvailablePostAuthenticator: navigateToScreen(LoginScreenType.Invisible); config.onFallback(identifier, message); - void getConnectService().recordEventLoginErrorUntyped(); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); setLoading(false); break; case LoginSituationCode.ClientPasskeyOperationCancelled: navigateToScreen(LoginScreenType.ErrorHard); config.onError?.(situationCode.toString()); - void getConnectService().recordEventLoginError(); + void getConnectService().recordEventLoginError(messageCode); setLoading(false); break; diff --git a/packages/connect-react/src/components/login/LoginHybridScreen.tsx b/packages/connect-react/src/components/login/LoginHybridScreen.tsx index 885b256c0..8178cb41a 100644 --- a/packages/connect-react/src/components/login/LoginHybridScreen.tsx +++ b/packages/connect-react/src/components/login/LoginHybridScreen.tsx @@ -37,7 +37,8 @@ const LoginHybridScreen = (resStart: ConnectLoginStartRsp) => { }, [getConnectService, config, navigateToScreen, currentIdentifier, loading]); const handleSituation = (situationCode: LoginSituationCode) => { - log.debug(`situation: ${situationCode}`); + const messageCode = `situation: ${situationCode}`; + log.debug(messageCode); const identifier = currentIdentifier; const message = getLoginErrorMessage(situationCode); @@ -47,14 +48,14 @@ const LoginHybridScreen = (resStart: ConnectLoginStartRsp) => { case LoginSituationCode.CboApiNotAvailablePostAuthenticator: navigateToScreen(LoginScreenType.Invisible); config.onFallback(identifier, message); - void getConnectService().recordEventLoginErrorUntyped(); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); setLoading(false); break; case LoginSituationCode.ClientPasskeyOperationCancelled: navigateToScreen(LoginScreenType.ErrorSoft); config.onError?.(situationCode.toString()); - void getConnectService().recordEventLoginError(); + void getConnectService().recordEventLoginError(messageCode); setLoading(false); break; diff --git a/packages/connect-react/src/components/login/LoginInitScreen.tsx b/packages/connect-react/src/components/login/LoginInitScreen.tsx index 37ff63be2..cea349ff3 100644 --- a/packages/connect-react/src/components/login/LoginInitScreen.tsx +++ b/packages/connect-react/src/components/login/LoginInitScreen.tsx @@ -145,7 +145,6 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { return handleSituation(LoginSituationCode.ClientPasskeyConditionalOperationCancelled); } - void getConnectService().recordEventLoginErrorUntyped(); // if a passkey has been deleted, CUI will fail => fallback with message if (res.val instanceof ConnectConditionalUIPasskeyDeleted) { return handleSituation(LoginSituationCode.PasskeyNotAvailablePostConditionalAuthenticator); @@ -214,13 +213,15 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { }; const handleSituation = (situationCode: LoginSituationCode) => { - log.debug(`situation: ${situationCode}`); + const messageCode = `situation: ${situationCode}`; + log.debug(messageCode); const message = getLoginErrorMessage(situationCode); switch (situationCode) { case LoginSituationCode.CboApiNotAvailablePreAuthenticator: fallback(identifier, message); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); statefulLoader.current.finish(); break; @@ -235,18 +236,20 @@ const LoginInitScreen: FC = ({ showFallback = false }) => { case LoginSituationCode.CtApiNotAvailablePostAuthenticator: case LoginSituationCode.CboApiNotAvailablePostAuthenticator: fallback(identifier, message); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); setIdentifierBasedLoading(false); break; case LoginSituationCode.ClientPasskeyOperationCancelled: navigateToScreen(LoginScreenType.ErrorSoft); - void getConnectService().recordEventLoginError(); + void getConnectService().recordEventLoginError(messageCode); config.onError?.(situationCode.toString()); setIdentifierBasedLoading(false); break; case LoginSituationCode.UserNotFound: setError(message ?? ''); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); setIdentifierBasedLoading(false); break; diff --git a/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx b/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx index 8631e9220..a295b07b1 100644 --- a/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx +++ b/packages/connect-react/src/components/login/LoginPasskeyReLoginScreen.tsx @@ -53,7 +53,8 @@ export const LoginPasskeyReLoginScreen = () => { }; const handleSituation = (situationCode: LoginSituationCode) => { - log.debug(`situation: ${situationCode}`); + const messageCode = `situation: ${situationCode}`; + log.debug(messageCode); const identifier = currentIdentifier; const message = getLoginErrorMessage(situationCode); @@ -64,14 +65,14 @@ export const LoginPasskeyReLoginScreen = () => { case LoginSituationCode.CboApiNotAvailablePreAuthenticator: navigateToScreen(LoginScreenType.Invisible); config.onFallback(identifier, message); - void getConnectService().recordEventLoginErrorUntyped(); + void getConnectService().recordEventLoginErrorUnexpected(messageCode); setLoading(false); break; case LoginSituationCode.ClientPasskeyOperationCancelled: navigateToScreen(LoginScreenType.ErrorSoft); config.onError?.(situationCode.toString()); - void getConnectService().recordEventLoginError(); + void getConnectService().recordEventLoginError(messageCode); setLoading(false); break; diff --git a/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx b/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx index 26a4c9d56..7e9cbcc69 100644 --- a/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx +++ b/packages/connect-react/src/components/passkeyList/PasskeyListScreen.tsx @@ -168,7 +168,8 @@ const PasskeyListScreen = () => { }; const handleSituation = (situationCode: PasskeyListSituationCode) => { - log.debug(`situation: ${situationCode}`); + const messageCode = `situation: ${situationCode}`; + log.debug(messageCode); const message = getPasskeyListErrorMessage(situationCode); switch (situationCode) { @@ -188,6 +189,8 @@ const PasskeyListScreen = () => { if (message) { setErrorMessage(message); } + + void getConnectService().recordEventManageErrorUnexpected(messageCode); break; case PasskeyListSituationCode.CtApiNotAvailablePreDelete: case PasskeyListSituationCode.CboApiNotAvailableDuringDelete: @@ -195,6 +198,8 @@ const PasskeyListScreen = () => { if (message) { setErrorMessage(message); } + + void getConnectService().recordEventManageErrorUnexpected(messageCode); break; case PasskeyListSituationCode.CtApiNotAvailablePreAuthenticator: case PasskeyListSituationCode.CboApiNotAvailablePreAuthenticator: @@ -205,6 +210,8 @@ const PasskeyListScreen = () => { if (message) { setErrorMessage(message); } + + void getConnectService().recordEventManageErrorUnexpected(messageCode); } }; diff --git a/packages/connect-react/src/contexts/AppendProcessContext.ts b/packages/connect-react/src/contexts/AppendProcessContext.ts index 05cfbda50..62f0cc2aa 100644 --- a/packages/connect-react/src/contexts/AppendProcessContext.ts +++ b/packages/connect-react/src/contexts/AppendProcessContext.ts @@ -13,8 +13,8 @@ export interface AppendProcessContextProps { currentScreenOptions: any; config: CorbadoConnectAppendConfig; navigateToScreen: (s: AppendScreenType, options?: any) => void; - handleErrorSoft: (situation: AppendSituationCode) => Promise; - handleErrorHard: (situation: AppendSituationCode, explicit?: boolean) => Promise; + handleErrorSoft: (situation: AppendSituationCode, expected: boolean) => Promise; + handleErrorHard: (situation: AppendSituationCode, expected: boolean) => Promise; handleCredentialExistsError: () => Promise; handleSkip: (situation: AppendSituationCode, explicit?: boolean) => Promise; } diff --git a/packages/connect-react/src/contexts/AppendProcessProvider.tsx b/packages/connect-react/src/contexts/AppendProcessProvider.tsx index a1537f247..fe96b02ec 100644 --- a/packages/connect-react/src/contexts/AppendProcessProvider.tsx +++ b/packages/connect-react/src/contexts/AppendProcessProvider.tsx @@ -25,19 +25,24 @@ export const AppendProcessProvider: FC> = ({ children, }, []); const handleErrorSoft = useCallback( - async (situationCode: AppendSituationCode) => { - await getConnectService().recordEventAppendError(); + async (situationCode: AppendSituationCode, expected: boolean) => { + if (expected) { + await getConnectService().recordEventAppendError(); + } else { + await getConnectService().recordEventAppendErrorUnexpected(`situation: ${situationCode}`); + } + config.onError?.(situationCode.toString()); }, [getConnectService, config], ); const handleErrorHard = useCallback( - async (situationCode: AppendSituationCode, explicit?: boolean) => { - if (explicit) { - await getConnectService().recordEventAppendExplicitAbort(); - } else { + async (situationCode: AppendSituationCode, expected: boolean) => { + if (expected) { await getConnectService().recordEventAppendError(); + } else { + await getConnectService().recordEventAppendErrorUnexpected(`situation: ${situationCode}`); } config.onError?.(situationCode.toString()); diff --git a/packages/web-core/openapi/spec_v2.yaml b/packages/web-core/openapi/spec_v2.yaml index 5e019be8d..d61284b47 100644 --- a/packages/web-core/openapi/spec_v2.yaml +++ b/packages/web-core/openapi/spec_v2.yaml @@ -1419,6 +1419,8 @@ components: properties: eventType: $ref: '#/components/schemas/passkeyEventType' + message: + type: string challenge: type: string @@ -1637,7 +1639,7 @@ components: passkeyEventType: type: string - enum: [ login-explicit-abort, login-error, login-error-untyped, login-one-tap-switch, user-append-after-cross-platform-blacklisted, user-append-after-login-error-blacklisted, append-credential-exists, append-explicit-abort, append-error ] + enum: [ login-explicit-abort, login-error, login-error-untyped, login-error-unexpected, login-one-tap-switch, user-append-after-cross-platform-blacklisted, user-append-after-login-error-blacklisted, append-credential-exists, append-explicit-abort, append-error, append-error-unexpected, manage-error-unexpected ] blockType: type: string diff --git a/packages/web-core/src/api/v2/api.ts b/packages/web-core/src/api/v2/api.ts index 403a1b7b6..1dd1b1d4e 100644 --- a/packages/web-core/src/api/v2/api.ts +++ b/packages/web-core/src/api/v2/api.ts @@ -400,6 +400,12 @@ export interface ConnectEventCreateReq { * @memberof ConnectEventCreateReq */ 'eventType': PasskeyEventType; + /** + * + * @type {string} + * @memberof ConnectEventCreateReq + */ + 'message'?: string; /** * * @type {string} @@ -1781,12 +1787,15 @@ export const PasskeyEventType = { LoginExplicitAbort: 'login-explicit-abort', LoginError: 'login-error', LoginErrorUntyped: 'login-error-untyped', + LoginErrorUnexpected: 'login-error-unexpected', LoginOneTapSwitch: 'login-one-tap-switch', UserAppendAfterCrossPlatformBlacklisted: 'user-append-after-cross-platform-blacklisted', UserAppendAfterLoginErrorBlacklisted: 'user-append-after-login-error-blacklisted', AppendCredentialExists: 'append-credential-exists', AppendExplicitAbort: 'append-explicit-abort', - AppendError: 'append-error' + AppendError: 'append-error', + AppendErrorUnexpected: 'append-error-unexpected', + ManageErrorUnexpected: 'manage-error-unexpected' } as const; export type PasskeyEventType = typeof PasskeyEventType[keyof typeof PasskeyEventType]; diff --git a/packages/web-core/src/models/connect/connectProcess.ts b/packages/web-core/src/models/connect/connectProcess.ts index 27b466a8b..3bc5ccd23 100644 --- a/packages/web-core/src/models/connect/connectProcess.ts +++ b/packages/web-core/src/models/connect/connectProcess.ts @@ -1,4 +1,5 @@ import type { ConnectAppendInitData, ConnectLoginInitData, ConnectManageInitData } from './login'; +import log from 'loglevel'; const getStorageKey = (projectId: string) => `cbo_connect_process-${projectId}`; @@ -68,10 +69,13 @@ export class ConnectProcess { } getValidLoginData(): ConnectLoginInitData | undefined { + log.debug('getValidLoginData 1', this.loginData); if (!this.loginData || !this.loginData.expiresAt) { return; } + log.debug('getValidLoginData 2'); + if (this.loginData.expiresAt < Date.now() / 1000) { return; } diff --git a/packages/web-core/src/services/ConnectService.ts b/packages/web-core/src/services/ConnectService.ts index 37afab264..fd3d95905 100644 --- a/packages/web-core/src/services/ConnectService.ts +++ b/packages/web-core/src/services/ConnectService.ts @@ -151,11 +151,12 @@ export class ConnectService { } const existingProcessFromOtherLoginInit = ConnectProcess.loadFromStorage(this.#projectId); - if (existingProcessFromOtherLoginInit?.loginData) { + const maybeExistingLoginDataFromOtherLoginInit = existingProcessFromOtherLoginInit?.getValidLoginData(); + if (maybeExistingLoginDataFromOtherLoginInit) { log.debug('process exists (after login init attempt'); this.#setApisV2(existingProcessFromOtherLoginInit); - return Ok(existingProcessFromOtherLoginInit.loginData); + return Ok(maybeExistingLoginDataFromOtherLoginInit); } // if the backend decides that a new client handle is needed, we store it in local storage @@ -173,9 +174,11 @@ export class ConnectService { }; if (existingProcess && existingProcess.id === res.val.token) { + log.debug('process exists, updating login data', loginData); const p = existingProcess.copyWithLoginData(loginData); p.persistToStorage(); } else { + log.debug('creating new process', loginData); const newProcess = new ConnectProcess( res.val.token, this.#projectId, @@ -322,9 +325,11 @@ export class ConnectService { // update local state with process if (existingProcess && existingProcess.id === res.val.processID) { + log.debug('process exists, updating append data', appendData); const p = existingProcess.copyWithAppendData(appendData); p.persistToStorage(); } else { + log.debug('creating new process', appendData); const newProcess = new ConnectProcess( res.val.processID, this.#projectId, @@ -533,8 +538,8 @@ export class ConnectService { invitation.persistToStorage(); } - recordEventLoginError() { - return this.#recordEvent(PasskeyEventType.LoginError); + recordEventLoginError(messageCode: string) { + return this.#recordEvent(PasskeyEventType.LoginError, messageCode); } recordEventLoginExplicitAbort() { @@ -565,12 +570,24 @@ export class ConnectService { return this.#recordEvent(PasskeyEventType.AppendError); } + recordEventLoginErrorUnexpected(messageCode: string) { + return this.#recordEvent(PasskeyEventType.LoginErrorUnexpected, messageCode); + } + + recordEventAppendErrorUnexpected(messageCode: string) { + return this.#recordEvent(PasskeyEventType.AppendErrorUnexpected, messageCode); + } + + recordEventManageErrorUnexpected(messageCode: string) { + return this.#recordEvent(PasskeyEventType.ManageErrorUnexpected, messageCode); + } + recordEventAppendExplicitAbort() { return this.#recordEvent(PasskeyEventType.AppendExplicitAbort); } // This function can be used to catch events that would usually not create backend interaction (e.g. when a passkey ceremony is canceled) - #recordEvent(eventType: PasskeyEventType) { + #recordEvent(eventType: PasskeyEventType, message?: string) { const existingProcess = ConnectProcess.loadFromStorage(this.#projectId); if (!existingProcess) { log.warn('No process found to record event.'); @@ -580,6 +597,7 @@ export class ConnectService { const req: ConnectEventCreateReq = { eventType, + message, }; return this.wrapWithErr(() => this.#connectApi.connectEventCreate(req)); diff --git a/playground/connect-next/app/login/actions.ts b/playground/connect-next/app/login/actions.ts index 4c7371696..2846fa5f2 100644 --- a/playground/connect-next/app/login/actions.ts +++ b/playground/connect-next/app/login/actions.ts @@ -18,6 +18,10 @@ type DecodedToken = { username: string; }; +type TokenWrapper = { + AccessToken: string; +}; + const getKey = (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => { client.getSigningKey(header.kid, (err, key) => { const signingKey = key?.getPublicKey(); @@ -45,8 +49,11 @@ const verifyToken = async (token: string): Promise => { export async function postPasskeyLogin(session: string) { // validate session try { - const decoded = await verifyToken(session); + const tokenWrapper = JSON.parse(session) as TokenWrapper; + console.log('Validating token:', tokenWrapper); + const decoded = await verifyToken(tokenWrapper.AccessToken); const username = decoded.username; + console.log('decoded:', decoded); // create client that loads profile from ~/.aws/credentials or environment variables const client = new CognitoIdentityProviderClient({ From 4edef8b546c0ad6095574424e5d683e84bc82f8f Mon Sep 17 00:00:00 2001 From: Incorbador Date: Wed, 18 Dec 2024 14:26:02 +0100 Subject: [PATCH 3/4] Cleanup for PR --- packages/web-core/src/models/connect/connectProcess.ts | 3 --- playground/connect-next/app/login/actions.ts | 2 -- 2 files changed, 5 deletions(-) diff --git a/packages/web-core/src/models/connect/connectProcess.ts b/packages/web-core/src/models/connect/connectProcess.ts index 3bc5ccd23..0ff89d993 100644 --- a/packages/web-core/src/models/connect/connectProcess.ts +++ b/packages/web-core/src/models/connect/connectProcess.ts @@ -69,13 +69,10 @@ export class ConnectProcess { } getValidLoginData(): ConnectLoginInitData | undefined { - log.debug('getValidLoginData 1', this.loginData); if (!this.loginData || !this.loginData.expiresAt) { return; } - log.debug('getValidLoginData 2'); - if (this.loginData.expiresAt < Date.now() / 1000) { return; } diff --git a/playground/connect-next/app/login/actions.ts b/playground/connect-next/app/login/actions.ts index 2846fa5f2..b8c1f5bfd 100644 --- a/playground/connect-next/app/login/actions.ts +++ b/playground/connect-next/app/login/actions.ts @@ -50,10 +50,8 @@ export async function postPasskeyLogin(session: string) { // validate session try { const tokenWrapper = JSON.parse(session) as TokenWrapper; - console.log('Validating token:', tokenWrapper); const decoded = await verifyToken(tokenWrapper.AccessToken); const username = decoded.username; - console.log('decoded:', decoded); // create client that loads profile from ~/.aws/credentials or environment variables const client = new CognitoIdentityProviderClient({ From d5ca103e42d678d239e4a0529da9fe9d3a070a22 Mon Sep 17 00:00:00 2001 From: Incorbador Date: Wed, 18 Dec 2024 14:27:22 +0100 Subject: [PATCH 4/4] Linter --- packages/web-core/src/models/connect/connectProcess.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/web-core/src/models/connect/connectProcess.ts b/packages/web-core/src/models/connect/connectProcess.ts index 0ff89d993..27b466a8b 100644 --- a/packages/web-core/src/models/connect/connectProcess.ts +++ b/packages/web-core/src/models/connect/connectProcess.ts @@ -1,5 +1,4 @@ import type { ConnectAppendInitData, ConnectLoginInitData, ConnectManageInitData } from './login'; -import log from 'loglevel'; const getStorageKey = (projectId: string) => `cbo_connect_process-${projectId}`;