feat(core): Add MFA (#4767)

https://linear.app/n8n/issue/ADO-947/sync-branch-with-master-and-fix-fe-e2e-tets

---------

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Ricardo Espinoza
2023-08-23 22:59:16 -04:00
committed by GitHub
parent a01c3fbc19
commit 2b7ba6fdf1
61 changed files with 2301 additions and 105 deletions

View File

@@ -1,29 +1,43 @@
<template>
<AuthView
:form="FORM_CONFIG"
:formLoading="loading"
:with-sso="true"
data-test-id="signin-form"
@submit="onSubmit"
/>
<div>
<AuthView
v-if="!showMfaView"
:form="FORM_CONFIG"
:formLoading="loading"
:with-sso="true"
data-test-id="signin-form"
@submit="onEmailPasswordSubmitted"
/>
<MfaView
v-if="showMfaView"
@submit="onMFASubmitted"
@onBackClick="onBackClick"
@onFormChanged="onFormChanged"
:reportError="reportError"
/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AuthView from './AuthView.vue';
import MfaView from './MfaView.vue';
import { useToast } from '@/composables';
import type { IFormBoxConfig } from '@/Interface';
import { VIEWS } from '@/constants';
import { MFA_AUTHENTICATION_REQUIRED_ERROR_CODE, VIEWS } from '@/constants';
import { mapStores } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useCloudPlanStore, useUIStore } from '@/stores';
import { genericHelpers } from '@/mixins/genericHelpers';
import { FORM } from './MfaView.vue';
export default defineComponent({
name: 'SigninView',
mixins: [genericHelpers],
components: {
AuthView,
MfaView,
},
setup() {
return {
@@ -34,10 +48,17 @@ export default defineComponent({
return {
FORM_CONFIG: {} as IFormBoxConfig,
loading: false,
showMfaView: false,
email: '',
password: '',
reportError: false,
};
},
computed: {
...mapStores(useUsersStore, useSettingsStore, useUIStore, useCloudPlanStore),
userHasMfaEnabled() {
return !!this.usersStore.currentUser?.mfaEnabled;
},
},
mounted() {
let emailLabel = this.$locale.baseText('auth.email');
@@ -84,31 +105,94 @@ export default defineComponent({
}
},
methods: {
async onSubmit(values: { [key: string]: string }) {
async onMFASubmitted(form: { token?: string; recoveryCode?: string }) {
await this.login({
email: this.email,
password: this.password,
token: form.token,
recoveryCode: form.recoveryCode,
});
},
async onEmailPasswordSubmitted(form: { email: string; password: string }) {
await this.login(form);
},
async login(form: { email: string; password: string; token?: string; recoveryCode?: string }) {
try {
this.loading = true;
await this.usersStore.loginWithCreds(values as { email: string; password: string });
await this.usersStore.loginWithCreds({
email: form.email,
password: form.password,
mfaToken: form.token,
mfaRecoveryCode: form.recoveryCode,
});
this.loading = false;
await this.cloudPlanStore.checkForCloudPlanData();
await this.uiStore.initBanners();
this.clearAllStickyNotifications();
this.loading = false;
this.checkRecoveryCodesLeft();
if (typeof this.$route.query.redirect === 'string') {
const redirect = decodeURIComponent(this.$route.query.redirect);
if (redirect.startsWith('/')) {
// protect against phishing
void this.$router.push(redirect);
this.$telemetry.track('User attempted to login', {
result: this.showMfaView ? 'mfa_success' : 'success',
});
return;
}
if (this.isRedirectSafe()) {
const redirect = this.getRedirectQueryParameter();
void this.$router.push(redirect);
return;
}
await this.$router.push({ name: VIEWS.HOMEPAGE });
} catch (error) {
this.showError(error, this.$locale.baseText('auth.signin.error'));
if (error.errorCode === MFA_AUTHENTICATION_REQUIRED_ERROR_CODE) {
this.showMfaView = true;
this.cacheCredentials(form);
return;
}
this.$telemetry.track('User attempted to login', {
result: this.showMfaView ? 'mfa_token_rejected' : 'credentials_error',
});
if (!this.showMfaView) {
this.showError(error, this.$locale.baseText('auth.signin.error'));
this.loading = false;
return;
}
this.reportError = true;
}
},
onBackClick(fromForm: string) {
this.reportError = false;
if (fromForm === FORM.MFA_TOKEN) {
this.showMfaView = false;
this.loading = false;
}
},
onFormChanged(toForm: string) {
if (toForm === FORM.MFA_RECOVERY_CODE) {
this.reportError = false;
}
},
cacheCredentials(form: { email: string; password: string }) {
this.email = form.email;
this.password = form.password;
},
checkRecoveryCodesLeft() {
if (this.usersStore.currentUser) {
const { hasRecoveryCodesLeft, mfaEnabled } = this.usersStore.currentUser;
if (mfaEnabled && !hasRecoveryCodesLeft) {
this.showToast({
title: this.$locale.baseText('settings.mfa.toast.noRecoveryCodeLeft.title'),
message: this.$locale.baseText('settings.mfa.toast.noRecoveryCodeLeft.message'),
type: 'info',
duration: 0,
dangerouslyUseHTMLString: true,
});
}
}
},
},
});
</script>