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

Add enable/manage MFA functionality #276

Merged
merged 11 commits into from
Jan 9, 2024
18 changes: 9 additions & 9 deletions examples/vue-example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 43 additions & 6 deletions examples/vue-example/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
<div class="flex flex-col sm:flex-row gap-4 bottom-gutter">
<button class="btn" @click="getEd25519Key">Get Ed25519Key</button>
</div>
<div class="flex flex-col sm:flex-row gap-4 bottom-gutter">
<button v-if="!isMFAEnabled" class="btn" @click="enableMFA">Enable MFA</button>
<button v-else class="btn" @click="manageMFA">Manage MFA</button>
</div>
<p class="btn-label">Signing</p>
<div class="flex flex-col sm:flex-row gap-4 bottom-gutter">
<button class="btn" :disabled="!ethereumPrivateKeyProvider?.provider" @click="signMessage">Sign test Eth Message</button>
Expand Down Expand Up @@ -177,10 +181,14 @@ export default defineComponent({
};
},
async created() {
const openlogin = this.openloginInstance;
await openlogin.init();
if (openlogin.privKey) {
this.privKey = openlogin.privKey;
await this.openloginInstance.init();
if (this.openloginInstance.state.factorKey) {
this.useMpc = true;
this.openloginInstance.options.useMpc = true;
await this.openloginInstance.init();
}
if (this.openloginInstance.privKey || this.openloginInstance.state.factorKey) {
this.privKey = this.openloginInstance.privKey || this.openloginInstance.state.factorKey as string;
await this.setProvider(this.privKey);
}
this.loading = false;
Expand Down Expand Up @@ -226,12 +234,14 @@ export default defineComponent({
}
: undefined,
});
op.init();
return op;
},
showEmailFlow(): boolean {
return this.selectedLoginProvider === LOGIN_PROVIDER.EMAIL_PASSWORDLESS;
},
isMFAEnabled(): boolean {
return this.openloginInstance.state.userInfo?.isMfaEnabled || false;
}
},
methods: {
async login() {
Expand All @@ -257,6 +267,7 @@ export default defineComponent({
const openLoginObj: LoginParams = {
loginProvider: this.selectedLoginProvider,
mfaLevel: "optional",

// pass empty string '' as loginProvider to open default torus modal
// with all default supported login providers or you can pass specific
// login provider from available list to set as default.
Expand Down Expand Up @@ -300,7 +311,7 @@ export default defineComponent({

async setProvider(privKey: string) {
if (this.useMpc) {
const { factorKey, tssPubKey, tssShareIndex, userInfo, tssShare, tssNonce, signatures } = this.openloginInstance.state;
const { factorKey, tssPubKey, tssShareIndex, userInfo, tssShare, tssNonce, signatures } = this.openloginInstance.state;
this.ethereumPrivateKeyProvider = new EthMpcPrivKeyProvider({
config: {
chainConfig: {
Expand Down Expand Up @@ -395,6 +406,32 @@ export default defineComponent({
this.printToConsole("User Info", userInfo);
},

async enableMFA() {
if (!this.openloginInstance || !this.openloginInstance.sessionId) {
throw new Error("User not logged in")
}
await this.openloginInstance.enableMFA({
loginProvider: this.selectedLoginProvider,
extraLoginOptions: {
login_hint: this.openloginInstance.getUserInfo().email,
flow_type: this.emailFlowType,
}
});
},

async manageMFA() {
if (!this.openloginInstance || !this.openloginInstance.sessionId) {
throw new Error("User not logged in")
}
await this.openloginInstance.manageMFA({
loginProvider: this.selectedLoginProvider,
extraLoginOptions: {
login_hint: this.openloginInstance.getUserInfo().email,
flow_type: this.emailFlowType,
}
});
},

async getOpenloginState() {
if (!this.openloginInstance) {
throw new Error("Openlogin is not available.");
Expand Down
3 changes: 2 additions & 1 deletion packages/openlogin-utils/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ export const MFA_LEVELS = {
export const OPENLOGIN_ACTIONS = {
LOGIN: "login",
ENABLE_MFA: "enable_mfa",
MODIFY_MFA: "modify_mfa",
MANAGE_MFA: "manage_mfa",
MODIFY_SOCIAL_FACTOR: "modify_social_factor",
} as const;

export const BUILD_ENV = {
Expand Down
15 changes: 15 additions & 0 deletions packages/openlogin-utils/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ export type LoginParams = BaseRedirectParams & {
curve?: SUPPORTED_KEY_CURVES_TYPE;
};

export type ManageMFAParams = LoginParams & {
/**
* Allows the dapp to set a custom redirect url for the manage mfa flow.
*
*/
dappUrl?: string;
};

export type SocialMfaModParams = {
/**
* loginProvider sets the oauth login method to be used.
Expand Down Expand Up @@ -627,6 +635,13 @@ export type OpenLoginOptions = {
*/
sdkUrl?: string;

/**
* dashboardUrl is for internal development use only and is used to override the
* `buildEnv` parameter.
* @internal
*/
dashboardUrl?: string;

/**
* options for whitelabling default openlogin modal.
*/
Expand Down
57 changes: 50 additions & 7 deletions packages/openlogin/src/OpenLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
BUILD_ENV,
jsonToBase64,
LoginParams,
ManageMFAParams,
OPENLOGIN_ACTIONS,
OPENLOGIN_NETWORK,
OpenLoginOptions,
Expand Down Expand Up @@ -46,18 +47,22 @@ class OpenLogin {
if (!options.sdkUrl && !options.useMpc) {
if (options.buildEnv === BUILD_ENV.DEVELOPMENT) {
options.sdkUrl = "http://localhost:3000";
options.dashboardUrl = "http://localhost:5173/wallet/account";
} else if (options.buildEnv === BUILD_ENV.STAGING) {
options.sdkUrl = "https://staging-auth.web3auth.io";
options.dashboardUrl = "https://staging-account.web3auth.io/wallet/account";
} else if (options.buildEnv === BUILD_ENV.TESTING) {
options.sdkUrl = "https://develop-auth.web3auth.io";
options.dashboardUrl = "https://develop-account.web3auth.io/wallet/account";
} else {
options.sdkUrl = "https://auth.web3auth.io";
options.dashboardUrl = "https://account.web3auth.io/wallet/account";
}
}

if (options.useMpc && !options.sdkUrl) {
if (Object.values(TORUS_LEGACY_NETWORK).includes(options.network as TORUS_LEGACY_NETWORK_TYPE))
throw new Error("MPC is not supported on legacy networks");
throw new Error("MPC is not supported on legacy networks, please use sapphire_devnet or sapphire_mainnet.");
if (options.buildEnv === BUILD_ENV.DEVELOPMENT) {
options.sdkUrl = "http://localhost:3000";
} else if (options.buildEnv === BUILD_ENV.STAGING) {
Expand Down Expand Up @@ -250,9 +255,9 @@ class OpenLogin {
this.currentStorage.set("sessionId", "");
}

async setupMFA(params: Partial<BaseRedirectParams>): Promise<boolean> {
async enableMFA(params: Partial<LoginParams>): Promise<boolean> {
if (!this.sessionId) throw LoginError.userNotLoggedIn();

if (this.state.userInfo.isMfaEnabled) throw LoginError.mfaAlreadyEnabled();
// in case of redirect mode, redirect url will be dapp specified
// in case of popup mode, redirect url will be sdk specified
const defaultParams: BaseRedirectParams = {
Expand All @@ -265,17 +270,55 @@ class OpenLogin {
params: {
...defaultParams,
...params,
mfaLevel: "mandatory",
},
sessionId: this.sessionId,
};

const result = await this.openloginHandler(`${this.baseUrl}/start`, dataObject);
if (this.options.uxMode === UX_MODE.REDIRECT) return undefined;
const result = await this.openloginHandler(`${this.baseUrl}/start`, dataObject, getTimeout(params.loginProvider));
if (this.options.uxMode === UX_MODE.REDIRECT) return null;
if (result.error) {
this.dappState = result.state;
throw LoginError.loginFailed(result.error);
}
this.sessionManager.sessionId = result.sessionId;
this.options.sessionNamespace = result.sessionNamespace;
this.currentStorage.set("sessionId", result.sessionId);
await this.rehydrateSession();
return true;
return Boolean(this.state.userInfo?.isMfaEnabled);
}

async manageMFA(params: Partial<ManageMFAParams>): Promise<void> {
if (!this.sessionId) throw LoginError.userNotLoggedIn();
if (!this.state.userInfo.isMfaEnabled) throw LoginError.mfaNotEnabled();

// in case of redirect mode, redirect url will be dapp specified
// in case of popup mode, redirect url will be sdk specified
const defaultParams = {
redirectUrl: this.options.dashboardUrl,
dappUrl: `${window.location.origin}${window.location.pathname}`,
};

const dataObject: OpenloginSessionConfig = {
actionType: OPENLOGIN_ACTIONS.MANAGE_MFA,
options: this.options,
params: {
...defaultParams,
...params,
},
};

const loginId = await this.getLoginId(dataObject);
const configParams: BaseLoginParams = {
loginId,
sessionNamespace: this.options.network,
};
const loginUrl = constructURL({
baseURL: `${this.baseUrl}/start`,
hash: { b64Params: jsonToBase64(configParams) },
});

window.open(loginUrl, "_blank");
}

async changeSocialFactor(params: SocialMfaModParams & Partial<BaseRedirectParams>): Promise<boolean> {
Expand All @@ -288,7 +331,7 @@ class OpenLogin {
};

const dataObject: OpenloginSessionConfig = {
actionType: OPENLOGIN_ACTIONS.MODIFY_MFA,
actionType: OPENLOGIN_ACTIONS.MODIFY_SOCIAL_FACTOR,
options: this.options,
params: {
...defaultParams,
Expand Down
10 changes: 10 additions & 0 deletions packages/openlogin/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export class LoginError extends OpenloginError {
5113: "login popup has been closed by the user",
5114: "Login failed",
5115: "Popup was blocked. Please call this function as soon as user clicks button or use redirect mode",
5116: "MFA already enabled",
5117: "MFA not yet enabled. Please call `enableMFA` first",
};

public constructor(code: number, message?: string) {
Expand Down Expand Up @@ -111,4 +113,12 @@ export class LoginError extends OpenloginError {
public static popupBlocked(extraMessage = ""): OpenloginError {
return LoginError.fromCode(5115, extraMessage);
}

public static mfaAlreadyEnabled(extraMessage = ""): OpenloginError {
return LoginError.fromCode(5116, extraMessage);
}

public static mfaNotEnabled(extraMessage = ""): OpenloginError {
return LoginError.fromCode(5117, extraMessage);
}
}