Skip to content

Conversation

blakecduncan
Copy link
Collaborator

@blakecduncan blakecduncan commented Mar 25, 2025

Pull Request Checklist


PR-Codex overview

This PR focuses on enhancing the authentication flow by introducing support for multi-factor authentication (MFA) and refining the handling of different authentication steps in the BaseAlchemySigner class.

Detailed summary

  • Added AuthStepResult type to represent various authentication states.
  • Extended AuthParams type to include MFA parameters.
  • Implemented authenticateStep method to handle different auth types, including MFA.
  • Created authenticateWithOtpNew and authenticateWithEmailNew methods for improved OTP and email handling.
  • Updated error handling for unsupported auth step types.
  • Modified validateMultiFactors to return AuthStepResult instead of User.

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

Goal:

We’ve updated our API to improve multi-factor authentication (MFA) flows by adding a new validate-mfa endpoint. Previously, you had to submit all MFA credentials (e.g., TOTP) with your primary credentials (like email + otp) in a single call, which meant you couldn’t provide incremental feedback to the user if one step failed.

Now that we have a new endpoint for MFA, it makes sense to refine the SDK’s authentication flow by introducing a authenticateStep method that returns a status after each step—rather than always returning a final User or throwing an error. This is especially important for multi-step flows like email + otp + totp.

Currently, the existing authenticate method returns a User or throws an error. This approach can lead to unresolved Promises across multiple steps in an MFA flow, forcing developers to rely on events to figure out which step the user is on.

Possible MFA flow with the current authenticate method

// 1) Returns a promise that won’t resolve until the user eventually enters the OTP and TOTP:
const user1 = signer.authenticate({ type: "email", email });
// The developer must listen for an event to know we’re “awaiting OTP.” The promise remains unresolved.

// 2) Similarly, returns a promise that only resolves after TOTP is submitted:
const user2 = signer.authenticate({ type: "otp", otpCode });
// Developer must again rely on events to detect “awaiting MFA.”

// 3) Finally resolves with a user:
const user3 = signer.authenticate({ type: "mfa", mode: "totp", multiFactorCode: "asdf" });
// At the end, user1, user2, and user3 all resolve to the same final user.

This sequence can be confusing because each call to authenticate returns a promise that waits for future steps, rather than returning partial information at each step.

Proposed Flow with authenticateStep

// 1) Immediate return with `AWAITING_EMAIL_AUTH` but no final user:
const { status, user } = signer.authenticateStep({
  type: "email",
  email,
});
// status = AWAITING_EMAIL_AUTH
// user = undefined or "partial" if you choose

// 2) Next step returns `AWAITING_MFA_AUTH` along with the allowed MFA factors:
const { status, user, multiFactors } = signer.authenticateStep({
  type: "otp",
  otpCode,
});
// status = AWAITING_MFA_AUTH
// multiFactors = e.g., totp
// user = undefined or partial

// 3) Finally returns a fully authenticated user:
const { status, user } = signer.authenticateStep({
  type: "mfa",
  mode: "totp",
  multiFactorCode: "asdf",
});
// status = CONNECTED
// user = the final, fully authenticated user

Each call returns immediate feedback about the current status (AWAITING_EMAIL_AUTH, AWAITING_MFA_AUTH, etc.) rather than suspending the promise until all steps are complete. This means you can handle errors or partial progress more cleanly.

Note: We won’t break backward compatibility. The existing authenticate method continues to work as before, but we recommend switching to authenticateStep for multi-step flows.

Copy link

vercel bot commented Mar 25, 2025

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

Name Status Preview Comments Updated (UTC)
aa-sdk-site ❌ Failed (Inspect) Mar 26, 2025 2:05pm
aa-sdk-ui-demo ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 26, 2025 2:05pm

Copy link
Contributor

@dphilipson dphilipson left a comment

Choose a reason for hiding this comment

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

This looks great! Left some nitpicky comments.

params.redirectParams
);

const isMfaRequired = multiFactors ? multiFactors?.length > 0 : false;
Copy link
Contributor

Choose a reason for hiding this comment

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

(Very nit) No need for the ?., we know multiFactors is present in that branch.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh yeah good catch

case "otp":
return this.authenticateWithOtpNew(params);
default:
throw new Error("type not implemented");
Copy link
Contributor

Choose a reason for hiding this comment

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

(Minor) I'd recommend using assertNever to check that all types are handled, which also means explicitly writing out the other cases. That way, if we add a new type of auth step the type system will make us explicitly acknowledge it here. I'm thinking something like:

          switch (type) {
            case "email":
              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:
              assertNever(type, `Unknown auth step type: ${type}`);
          }

Over time we've had a number of issues in the sdk where we added a new union arm and forgot to add handling for it somewhere, so IMO it's worth the extra hassle.

(I also suggest including the type in the error message to make debugging easier if this happens.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh yeah good call, will updated 👍

};
}

if (!("email" in params)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This check isn't strictly necessary because the type system should supposedly enforce that email is present in this branch, but doesn't hurt I suppose.

}
);

authenticateStep: (params: AuthParams) => Promise<AuthStepResult> =
Copy link
Contributor

Choose a reason for hiding this comment

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

nit because this also starts and progresses the flow should the name be handleAuth, authenticateOrAdvance, or progressAuth?

Should we break up starting and progressing authentication into two calls? Such as login and verify?

Base automatically changed from blake/new-verify-mfa-api to mfa/demo March 27, 2025 18:42
@blakecduncan
Copy link
Collaborator Author

Closing as I plan to further investigate the different approaches here

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.

3 participants