Skip to content

Commit

Permalink
Merge pull request #808 from magiclabs/sherzod-PDEEXP-1711-Support-MF…
Browse files Browse the repository at this point in the history
…A-setup-whitelabel-support

White label support for MFA | Should not be merged before Mandrake
  • Loading branch information
joshuascan authored Sep 25, 2024
2 parents b534ca0 + 7390564 commit dc1f7f3
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 67 deletions.
47 changes: 41 additions & 6 deletions packages/@magic-sdk/provider/src/modules/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import {
RequestUserInfoScope,
RecoverAccountConfiguration,
ShowSettingsConfiguration,
EnableMFAConfiguration,
EnableMFAEventEmit,
EnableMFAEventHandlers,
DisableMFAConfiguration,
DisableMFAEventHandlers,
DisableMFAEventEmit,
} from '@magic-sdk/types';
import { getItem, setItem, removeItem } from '../util/storage';
import { BaseModule } from './base-module';
Expand Down Expand Up @@ -141,14 +147,43 @@ export class UserModule extends BaseModule {
this.userLoggedOutCallbacks.push(callback);
}

public enableMFA() {
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.EnableMFA);
return this.request<boolean>(requestPayload);
public enableMFA(configuration: EnableMFAConfiguration) {
const { showUI = true } = configuration;
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.EnableMFA, [{ showUI }]);
const handle = this.request<string | boolean | null, EnableMFAEventHandlers>(requestPayload);

if (!showUI && handle) {
handle.on(EnableMFAEventEmit.VerifyMFACode, (totp: string) => {
this.createIntermediaryEvent(EnableMFAEventEmit.VerifyMFACode, requestPayload.id as string)(totp);
});

handle.on(EnableMFAEventEmit.Cancel, () => {
this.createIntermediaryEvent(EnableMFAEventEmit.Cancel, requestPayload.id as string)();
});
}
return handle;
}

public disableMFA() {
const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.DisableMFA);
return this.request<boolean>(requestPayload);
public disableMFA(configuration: DisableMFAConfiguration) {
const { showUI = true } = configuration;

const requestPayload = createJsonRpcRequestPayload(MagicPayloadMethod.DisableMFA, [{ showUI }]);
const handle = this.request<string | boolean | null, DisableMFAEventHandlers>(requestPayload);

if (!showUI && handle) {
handle.on(DisableMFAEventEmit.VerifyMFACode, (totp: string) => {
this.createIntermediaryEvent(DisableMFAEventEmit.VerifyMFACode, requestPayload.id as string)(totp);
});

handle.on(DisableMFAEventEmit.LostDevice, (recoveryCode: string) => {
this.createIntermediaryEvent(DisableMFAEventEmit.LostDevice, requestPayload.id as string)(recoveryCode);
});

handle.on(DisableMFAEventEmit.Cancel, () => {
this.createIntermediaryEvent(DisableMFAEventEmit.Cancel, requestPayload.id as string)();
});
}
return handle;
}

// Private members
Expand Down
102 changes: 96 additions & 6 deletions packages/@magic-sdk/provider/test/spec/modules/user/mfa.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,113 @@
import { createMagicSDK } from '../../../factories';

test('Resolves immediately when cached magic.user.enableMFA.spec is true', () => {
test('Resolves immediately when cached magic.user.enableMFA.spec is true with ShowUI: true', async () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

magic.user.enableMFA();
await magic.user.enableMFA({ showUI: true });

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_enable_mfa_flow');
expect(requestPayload.params).toEqual([]);
expect(requestPayload.params).toEqual([{ showUI: true }]);
});

test('Resolves immediately when cached magic.user.enableMFA.spec is true', () => {
test('Resolves immediately when cached magic.user.enableMFA.spec is true with ShowUI: false', async () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

magic.user.disableMFA();
await magic.user.enableMFA({ showUI: false });

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_enable_mfa_flow');
expect(requestPayload.params).toEqual([{ showUI: false }]);
});

test('Resolves immediately when cached magic.user.enableMFA.spec is true with empty param', async () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

await magic.user.enableMFA({});

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_enable_mfa_flow');
expect(requestPayload.params).toEqual([{ showUI: true }]);
});

test('Enable MFA flow returns invalid mfa event', () => {
const magic = createMagicSDK();
magic.user.overlay.post = jest.fn().mockImplementation(() => new Promise(() => {}));
const createIntermediaryEventFn = jest.fn();
magic.user.createIntermediaryEvent = jest.fn().mockImplementation(() => createIntermediaryEventFn);

const handle = magic.user.enableMFA({ showUI: false });

const troll_otp = '123456';
handle.emit('verify-mfa-code', troll_otp);
handle.emit('cancel-mfa-setup');

const verifyEvent = magic.user.createIntermediaryEvent.mock.calls[0];
expect(verifyEvent[0]).toBe('verify-mfa-code');
expect(createIntermediaryEventFn.mock.calls[0][0]).toBe(troll_otp);

const intermediaryEventSecondMethod = magic.user.createIntermediaryEvent.mock.calls[1][0];
expect(intermediaryEventSecondMethod).toBe('cancel-mfa-setup');
});

test('Resolves immediately when cached magic.user.enableMFA.spec is true with ShowUI: true', () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

magic.user.disableMFA({ showUI: true });

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_disable_mfa_flow');
expect(requestPayload.params).toEqual([{ showUI: true }]);
});

test('Resolves immediately when cached magic.user.enableMFA.spec is true with ShowUI: false', () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

magic.user.disableMFA({ showUI: false });

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_disable_mfa_flow');
expect(requestPayload.params).toEqual([{ showUI: false }]);
});

test('Resolves immediately when cached magic.user.enableMFA.spec is true with empty object', () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

magic.user.disableMFA({});

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_disable_mfa_flow');
expect(requestPayload.params).toEqual([]);
expect(requestPayload.params).toEqual([{ showUI: true }]);
});

test('Disable MFA flow returns invalid mfa event', () => {
const magic = createMagicSDK();
magic.user.overlay.post = jest.fn().mockImplementation(() => new Promise(() => {}));
const createIntermediaryEventFn = jest.fn();
magic.user.createIntermediaryEvent = jest.fn().mockImplementation(() => createIntermediaryEventFn);

const handle = magic.user.disableMFA({ showUI: false });

const troll_otp = '123456';
handle.emit('verify-mfa-code', troll_otp);
handle.emit('cancel-mfa-disable');
handle.emit('lost-device', troll_otp);

const events = magic.user.createIntermediaryEvent.mock.calls;

const verifyEvent = events[0][0];
expect(verifyEvent).toBe('verify-mfa-code');
expect(createIntermediaryEventFn.mock.calls[0][0]).toBe(troll_otp);

const cancelEvent = events[1][0];
expect(cancelEvent).toBe('cancel-mfa-disable');

const lostDeviceEvent = events[2][0];
expect(lostDeviceEvent).toBe('lost-device');
});
14 changes: 10 additions & 4 deletions packages/@magic-sdk/types/src/modules/auth-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,9 @@ export type UpdateEmailEventHandlers = {

export type EnableMFAEventHandlers = {
// Event Received
[EnableMFAEventOnReceived.MFASecretGenerated]: (QRCode: string, key: string) => void;
[EnableMFAEventOnReceived.InvalidMFAOtp]: (error: string) => void;
[EnableMFAEventOnReceived.MFARecoveryCodes]: (recoveryCodes: string) => void;
[EnableMFAEventOnReceived.MFASecretGenerated]: ({ QRCode, key }: { QRCode: string; key: string }) => void;
[EnableMFAEventOnReceived.InvalidMFAOtp]: ({ errorCode }: { errorCode: string }) => void;
[EnableMFAEventOnReceived.MFARecoveryCodes]: ({ recoveryCode }: { recoveryCode: string }) => void;

// Event sent
[EnableMFAEventEmit.VerifyMFACode]: (totp: string) => void;
Expand All @@ -351,7 +351,13 @@ export type EnableMFAEventHandlers = {
*/

export type DisableMFAEventHandlers = {
// Event Received
[DisableMFAEventOnReceived.MFACodeRequested]: () => void;
[DisableMFAEventOnReceived.InvalidMFAOtp]: (error: string) => void;
[DisableMFAEventOnReceived.InvalidMFAOtp]: ({ errorCode }: { errorCode: string }) => void;
[DisableMFAEventOnReceived.InvalidRecoveryCode]: () => void;

// Event sent
[DisableMFAEventEmit.VerifyMFACode]: (totp: string) => void;
[DisableMFAEventEmit.LostDevice]: (recoveryCode: string) => void;
[DisableMFAEventEmit.Cancel]: () => void;
};
12 changes: 11 additions & 1 deletion packages/@magic-sdk/types/src/modules/intermediary-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
UpdateEmailEventEmit,
UpdateEmailEventOnReceived,
LoginWithSmsOTPEventOnReceived,
EnableMFAEventEmit,
EnableMFAEventOnReceived,
DisableMFAEventOnReceived,
DisableMFAEventEmit,
} from './auth-types';
import { NftCheckoutIntermediaryEvents } from './nft-types';

Expand Down Expand Up @@ -46,4 +50,10 @@ export type IntermediaryEvents =
// Farcaster Login Events
| `${FarcasterLoginEventEmit}`
// Ui Events
| `${UiEventsEmit}`;
| `${UiEventsEmit}`
// Enable MFA Events
| `${EnableMFAEventOnReceived}`
| `${EnableMFAEventEmit}`
// Disable MFA Events
| `${DisableMFAEventOnReceived}`
| `${DisableMFAEventEmit}`;
Loading

0 comments on commit dc1f7f3

Please sign in to comment.