Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mfa totp to the email otp flow #1389

Merged
merged 16 commits into from
Feb 27, 2025
Merged

Conversation

blakecduncan
Copy link
Collaborator

@blakecduncan blakecduncan commented Feb 25, 2025

Pull Request Checklist


New api

// Setting up MFA

const client = new AlchemySignerWebClient(...)

// Get existing MFA factors
const { factors } = await client.getMfaFactors();

// 1. User already signed in

// Enable TOTP MFA
// Returns factor details including ID and setup information
const result = await client.enableMfa({
  factorType: "totp"
});

// result contains:
// {
//   factorId: "factor-id-123",
//   factorType: "totp",
//   factorTotpUrl: "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example"
// }

// Verify the MFA setup by providing the factor ID and verification code
await client.verifyMfa({
  factorId: result.factorId,
  factorCode: "123456"
});

// Disable MFA by providing an array of factor IDs to remove
await client.disableMfa({
  factors: [result.factorId]
});

Authenticating (email + otp)

// Create a webclient
const client = new AlchemySignerWebClient(...)

// Get the signer
const signer = await client?.account.getSigner();

signer.on("mfaStatusChanged", async (status) => {
  if (status === AlchemyMfaStatus.REQUIRED) {
    const factors = await client.getMfaFactors();
    console.log("MFA is required with factors:", factors);
    // Store factors for later use
  }
});

await signer.authenticate({
  type: "email",
  emailMode: "otp",
  email: "[email protected]",
});
 
if (signer.getMfaStatus() === AlchemyMfaStatus.REQUIRED) {
  // Prompt for totp

  // later once the user has entered the code from their email
  await signer.authenticate({
    type: "otp",
    otpCode: "123456",
    multiFactor: {
      factorId: "factor-id-123",
      challange: { code },
    }
  });
} else {
  // Regular OTP flow
  await signer.authenticate({
    type: "otp",
    otpCode: "123456"
  });
}

Authenticating (email + magic link)

// send the email and inclue mfa challenge
await signer.authenticate({
  type: "email",
  emailMode: "magicLink",
  email: "[email protected]",
  multiFactor: {
     factorId: "factor-id-123",
     challange: { code },
   }
});
 
// later once the user has clicked the link
const url = new URL(window.location.href);
const bundle = url.searchParams.get("bundle");
if (!bundle) {
  throw new Error("No bundle found in URL");
}
 
await signer.authenticate({
  type: "email",
  bundle,
});

PR-Codex overview

This PR primarily focuses on enhancing the multi-factor authentication (MFA) functionality in the @account-kit/signer package. It introduces new types, methods, and documentation related to MFA, ensuring better handling and management of MFA factors.

Detailed summary

  • Added isMfaRequired to TemporarySession type.
  • Introduced AlchemyMfaStatus enum with values NOT_REQUIRED and REQUIRED.
  • Added methods for managing MFA: getMfaFactors, addMfa, verifyMfa, and removeMfa in BaseSignerClient and AlchemySignerWebClient.
  • Updated RNSignerClient to override MFA methods with errors if not implemented.
  • Enhanced documentation for MFA-related methods across multiple .mdx files.
  • Updated state management to incorporate MFA status in the BaseAlchemySigner.

✨ Ask PR-Codex anything about this PR by commenting with /codex {your question}

Copy link

vercel bot commented Feb 25, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
aa-sdk-site ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 27, 2025 6:56pm
aa-sdk-ui-demo ✅ Ready (Inspect) Visit Preview 💬 Add feedback Feb 27, 2025 6:56pm

@blakecduncan blakecduncan marked this pull request as draft February 25, 2025 16:57
expirationSeconds,
redirectParams: params.redirectParams,
});
if (existingUser) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole method should probably be broken up into multiple methods but I figured I could do that as a separate task if we want to circle back to it

@@ -68,6 +73,12 @@ export class RNSignerClient extends BaseSignerClient<undefined> {
targetPublicKey: publicKey,
});

if (!credentialBundle) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -21,6 +22,11 @@ export enum AlchemySignerStatus {
AWAITING_OTP_AUTH = "AWAITING_OTP_AUTH",
}

export enum AlchemyMfaStatus {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a 3rd state of "completed"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess looking at some of Josh's PRs it might also need a "disabled"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two separate states at play here. The state of the mfa factor and whether or not a user requires mfa on login.

I was thinking that AlchemyMfaStatus would be the User level required/not required at sign in state.


export type MfaState = {
factors?: MfaFactor[];
multiFactorState: "required" | "not_required";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just AlchemyMfaStatus?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good catch

organizationId: this.user.orgId,
});

return this.request("/v1/auth-list-multi-factors", {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth gut checking to see if this request already includes the jwt in the headers? https://github.com/alchemyplatform/aa-sdk/blob/008812b0/account-kit/signer/src/client/base.ts#L374 We might not even need to have the stamped request object in there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked and when the api call is made, the jwt is undefined


switch (params.factorType) {
case "totp":
return this.request("/v1/auth-request-multi-factor", {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On thing we may have forgetten is specifying which type of multifactor @0xfourzerofour. We might want that as a parameter here just for the sake of future proofing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants