From 065158400047ae6f60b2ebab8498a2114acf930f Mon Sep 17 00:00:00 2001 From: blake duncan Date: Tue, 25 Mar 2025 16:48:19 -0400 Subject: [PATCH 1/3] feat: test using new auth method --- account-kit/signer/src/base.ts | 132 ++++++++++++++++++++++++++++++- account-kit/signer/src/signer.ts | 19 +++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/account-kit/signer/src/base.ts b/account-kit/signer/src/base.ts index eed50078b5..a44be306f3 100644 --- a/account-kit/signer/src/base.ts +++ b/account-kit/signer/src/base.ts @@ -40,7 +40,7 @@ import { type SessionManagerParams, } from "./session/manager.js"; import type { SessionManagerEvents } from "./session/types"; -import type { AuthParams } from "./signer"; +import type { AuthParams, AuthStepResult } from "./signer"; import { SolanaSigner } from "./solanaSigner.js"; import { AlchemySignerStatus, @@ -312,6 +312,34 @@ export abstract class BaseAlchemySigner } ); + authenticateStep: (params: AuthParams) => Promise = + SignerLogger.profiled( + "BaseAlchemySigner.authenticateStep", + async (params) => { + const { type } = params; + const result = (() => { + switch (type) { + case "email": + return this.authenticateWithEmailNew(params); + case "otp": + return this.authenticateWithOtpNew(params); + default: + throw new Error("type not implemented"); + } + })(); + + this.trackAuthenticateType(params); + + return result.catch((error) => { + this.store.setState({ + error: toErrorInfo(error), + status: AlchemySignerStatus.DISCONNECTED, + }); + throw error; + }); + } + ); + private trackAuthenticateType = (params: AuthParams) => { const { type } = params; switch (type) { @@ -836,6 +864,53 @@ export abstract class BaseAlchemySigner return new SolanaSigner(this.inner); }; + private authenticateWithEmailNew = async ( + params: Extract + ): Promise => { + if ("bundle" in params) { + const user = await this.completeEmailAuth(params); + return { + status: AlchemySignerStatus.CONNECTED, + user, + }; + } + + if (!("email" in params)) { + throw new Error("Email is required"); + } + + const { orgId, otpId, multiFactors, isNewUser } = + await this.initOrCreateEmailUser( + params.email, + params.emailMode ?? "otp", + params.multiFactors, + params.redirectParams + ); + + const isMfaRequired = multiFactors ? multiFactors?.length > 0 : false; + + this.sessionManager.setTemporarySession({ + orgId, + isNewUser, + isMfaRequired, + }); + + this.store.setState({ + status: AlchemySignerStatus.AWAITING_EMAIL_AUTH, + otpId, + error: null, + mfaStatus: { + mfaRequired: isMfaRequired, + mfaFactorId: multiFactors?.[0]?.multiFactorId, + }, + }); + + return { + status: AlchemySignerStatus.AWAITING_EMAIL_AUTH, + user: undefined, + }; + }; + private authenticateWithEmail = async ( params: Extract ): Promise => { @@ -993,6 +1068,61 @@ export abstract class BaseAlchemySigner return user; }; + private authenticateWithOtpNew = async ( + args: Extract + ): Promise => { + const tempSession = this.sessionManager.getTemporarySession(); + const { orgId, isNewUser, isMfaRequired } = tempSession ?? {}; + const { otpId } = this.store.getState(); + if (!orgId) { + throw new Error("orgId not found in session"); + } + if (!otpId) { + throw new Error("otpId not found in session"); + } + if (isMfaRequired && !args.multiFactors) { + throw new Error(`MFA is required.`); + } + + const response = await this.inner.submitOtpCode({ + orgId, + otpId, + otpCode: args.otpCode, + expirationSeconds: this.getExpirationSeconds(), + multiFactors: args.multiFactors, + }); + + if (response.mfaRequired) { + this.handleMfaRequired(response.encryptedPayload, response.multiFactors); + + return { + status: AlchemySignerStatus.AWAITING_MFA_AUTH, + user: undefined, + multiFactors: response.multiFactors, + }; + } + + const user = await this.inner.completeAuthWithBundle({ + bundle: response.bundle, + orgId, + connectedEventName: "connectedOtp", + authenticatingType: "otp", + }); + + this.emitNewUserEvent(isNewUser); + if (tempSession) { + this.sessionManager.setTemporarySession({ + ...tempSession, + isNewUser: false, + }); + } + + return { + user, + status: AlchemySignerStatus.CONNECTED, + }; + }; + private handleOauthReturn = ({ bundle, orgId, diff --git a/account-kit/signer/src/signer.ts b/account-kit/signer/src/signer.ts index 3267934050..69b58a4c08 100644 --- a/account-kit/signer/src/signer.ts +++ b/account-kit/signer/src/signer.ts @@ -7,8 +7,27 @@ import { import type { CredentialCreationOptionOverrides, VerifyMfaParams, + User, + MfaFactor, } from "./client/types.js"; import { SessionManagerParamsSchema } from "./session/manager.js"; +import type { AlchemySignerStatus } from "./types.js"; + +export type AuthStepResult = + | { + status: AlchemySignerStatus.CONNECTED; + user: User; + } + | { + status: AlchemySignerStatus.AWAITING_EMAIL_AUTH; + user: undefined; + } + | { + status: AlchemySignerStatus.AWAITING_MFA_AUTH; + user: undefined; + multiFactors: MfaFactor[]; + // encryptedPayload: string; // I don't think we want to expose this to the user + }; export type AuthParams = | { From 2fb518e9f90fe8a669c1d34a840926539e4e2e10 Mon Sep 17 00:00:00 2001 From: blake duncan Date: Wed, 26 Mar 2025 08:48:43 -0400 Subject: [PATCH 2/3] feat: pr feedback --- account-kit/signer/src/base.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/account-kit/signer/src/base.ts b/account-kit/signer/src/base.ts index a44be306f3..78e834d62a 100644 --- a/account-kit/signer/src/base.ts +++ b/account-kit/signer/src/base.ts @@ -323,8 +323,12 @@ export abstract class BaseAlchemySigner return this.authenticateWithEmailNew(params); case "otp": return this.authenticateWithOtpNew(params); + case "passkey": + case "oauth": + case "oauthReturn": + throw new Error(`Unsupported auth step type: ${type}`); default: - throw new Error("type not implemented"); + assertNever(type, `Unknown auth step type: ${type}`); } })(); @@ -875,10 +879,6 @@ export abstract class BaseAlchemySigner }; } - if (!("email" in params)) { - throw new Error("Email is required"); - } - const { orgId, otpId, multiFactors, isNewUser } = await this.initOrCreateEmailUser( params.email, @@ -930,7 +930,7 @@ export abstract class BaseAlchemySigner params.redirectParams ); - const isMfaRequired = multiFactors ? multiFactors?.length > 0 : false; + const isMfaRequired = multiFactors ? multiFactors.length > 0 : false; this.sessionManager.setTemporarySession({ orgId, From e37d6a7f6826b65e801b2d1d8e3adca13af407b6 Mon Sep 17 00:00:00 2001 From: blake duncan Date: Wed, 26 Mar 2025 09:56:37 -0400 Subject: [PATCH 3/3] feat: have validate mfa flow through the authenticateStep function --- account-kit/signer/src/base.ts | 16 +++++++++++++--- account-kit/signer/src/signer.ts | 6 ++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/account-kit/signer/src/base.ts b/account-kit/signer/src/base.ts index 78e834d62a..678a634f4a 100644 --- a/account-kit/signer/src/base.ts +++ b/account-kit/signer/src/base.ts @@ -287,6 +287,10 @@ export abstract class BaseAlchemySigner return this.handleOauthReturn(params); case "otp": return this.authenticateWithOtp(params); + case "mfa": + throw new Error( + `Unsupported auth step type: ${type}, please use the authenticateStep method.` + ); default: assertNever(type, `Unknown auth type: ${type}`); } @@ -323,10 +327,14 @@ export abstract class BaseAlchemySigner return this.authenticateWithEmailNew(params); case "otp": return this.authenticateWithOtpNew(params); + case "mfa": + return this.validateMultiFactors(params); case "passkey": case "oauth": case "oauthReturn": - throw new Error(`Unsupported auth step type: ${type}`); + throw new Error( + `Unsupported auth step type: ${type}, please use the authenticate method.` + ); default: assertNever(type, `Unknown auth step type: ${type}`); } @@ -383,6 +391,8 @@ export abstract class BaseAlchemySigner data: { authType: "otp" }, }); break; + case "mfa": + break; default: assertNever(type, `Unknown auth type: ${type}`); } @@ -1475,7 +1485,7 @@ export abstract class BaseAlchemySigner */ public async validateMultiFactors( params: ValidateMultiFactorsParams - ): Promise { + ): Promise { // Get MFA context from temporary session const tempSession = this.sessionManager.getTemporarySession(); if ( @@ -1530,7 +1540,7 @@ export abstract class BaseAlchemySigner }, }); - return user; + return { user, status: AlchemySignerStatus.CONNECTED }; } protected initConfig = async (): Promise => { diff --git a/account-kit/signer/src/signer.ts b/account-kit/signer/src/signer.ts index 69b58a4c08..1be736fe86 100644 --- a/account-kit/signer/src/signer.ts +++ b/account-kit/signer/src/signer.ts @@ -72,6 +72,12 @@ export type AuthParams = type: "otp"; otpCode: string; multiFactors?: VerifyMfaParams[]; + } + | { + type: "mfa"; + mode: "totp"; + multiFactorId: string; + multiFactorCode: string; }; export type OauthProviderConfig =