Skip to content

Commit

Permalink
Merge pull request #72 from webitel/feature/add-2fa
Browse files Browse the repository at this point in the history
feature: add 2fa[WTEL-4430]
  • Loading branch information
dlohvinov authored May 16, 2024
2 parents 7ae470e + 4e1235b commit 75c4963
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 19 deletions.
20 changes: 20 additions & 0 deletions src/api/auth/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,25 @@ import instance, { config } from '../instance';
export const login = async (credentials) => {
const url = '/login';

try {
const response = await instance.post(url, credentials);

// [https://webitel.atlassian.net/browse/WTEL-3405]
// If two-factor authentication is enabled,
// API returns the two-factor authentication session ID instead of a token
// and saving to localStorage is not needed

if(response.accessToken) {
localStorage.setItem('access-token', response.accessToken);
return postToken();
} return response;
} catch (err) {
throw err;
}
};

export const login2fa = async (credentials) => {
const url = '/login/2fa';
try {
const response = await instance.post(url, credentials);
localStorage.setItem('access-token', response.accessToken);
Expand Down Expand Up @@ -105,6 +124,7 @@ const checkDomainExistence = async (domain) => {

const AuthAPI = {
login,
login2fa,
register,
checkCurrentSession,
loadServiceProviders,
Expand Down
58 changes: 58 additions & 0 deletions src/components/auth/login/steps/the-login-third-step.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div>
<wt-input
v-model.trim="totp"
:label="$t('auth.code')"
/>

<div class="auth-form-actions">
<wt-button
color="secondary"
@click="emit('back')"
>{{ $t('reusable.back') }}
</wt-button>

<wt-button
:disabled="v$.$invalid"
@click="emit('next')"
>{{ $t('auth.login') }}
</wt-button>
</div>
</div>
</template>

<script setup>
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import { computed, onMounted } from 'vue';
import { useStore } from 'vuex';
import { useNextOnEnter } from '../../../../composables/useNextOnEnter.js';
const emit = defineEmits(['back', 'next']);
const store = useStore();
const totp = computed({
get: () => store.state.auth.totp,
set: (value) => setProp({ prop: 'totp', value })
});
const v$ = useVuelidate(
computed(() => ({
totp: {
required,
},
})),
{ totp },
{ $autoDirty: true },
);
onMounted(() => { v$.value.$touch() });
useNextOnEnter(() => !v$.value.$invalid && emit('next'));
async function setProp(payload) {
return store.dispatch('auth/SET_PROPERTY', payload);
}
</script>
41 changes: 35 additions & 6 deletions src/components/auth/login/the-login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,23 @@
@back="backPrevStep"
@next="goNextStep"
></second-step>

<third-step
v-if="activeStep === 3"
@back="backPrevStep"
@next="goNextStep"
></third-step>
</div>

</template>
</wt-stepper>
</template>

<script>
import { mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import FirstStep from '../login/steps/the-login-first-step.vue';
import SecondStep from '../login/steps/the-login-second-step.vue';
import ThirdStep from './steps/the-login-third-step.vue';
export default {
name: 'the-login',
data: () => ({
Expand All @@ -41,11 +47,16 @@ export default {
components: {
FirstStep,
SecondStep,
ThirdStep,
},
computed: {
...mapState('auth', {
enabledTfa: (state) => state.enabledTfa,
}),
steps() {
return [
const steps = [
{
name: this.$t('reusable.step', { count: 1 }),
description: this.$t('auth.enterDomain'),
Expand All @@ -55,6 +66,13 @@ export default {
description: this.$t('auth.enterUsername'),
},
];
if (this.enabledTfa) steps.push({
name: this.$t('reusable.step', { count: 3 }),
description: this.$t('auth.enterAuthenticationCode'),
});
return steps;
},
},
Expand All @@ -63,6 +81,7 @@ export default {
setProp: 'SET_PROPERTY',
resetState: 'RESET_STATE',
checkDomain: 'CHECK_DOMAIN',
get2faSessionId: 'GET_2FA_SESSION_ID',
}),
backPrevStep() {
Expand All @@ -71,6 +90,14 @@ export default {
} else {
this.activeStep = this.activeStep - 1;
}
if (this.activeStep === 2) {
this.setProp({ prop: 'password', value: '' });
}
if (this.activeStep === 3) {
this.setProp({ prop: 'totp', value: '' });
}
},
async goNextStep() {
Expand All @@ -85,11 +112,12 @@ export default {
}
}
if (this.activeStep === 2) {
await this.setProp({ prop: 'password', value: '' });
this.activeStep = this.activeStep + 1;
if (this.activeStep === 3) {
await this.get2faSessionId();
}
this.activeStep = this.activeStep + 1;
} else {
this.$emit('submit');
}
Expand All @@ -99,6 +127,7 @@ export default {
unmounted() {
this.resetState();
},
};
</script>

Expand Down
2 changes: 2 additions & 0 deletions src/locale/en/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default {
signIn: 'Sign in',
login: 'Log in',
remember: 'Remember',
code: 'Code',
enterAuthenticationCode: 'Enter your two-factor authentication code',
carousel: {
title1: 'Cloud vs. On-Site',
text1: 'Security policy does not allow to store data and use cloud services? With Webitel, you can build a contact center on your site!',
Expand Down
2 changes: 2 additions & 0 deletions src/locale/ru/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ export default {
signIn: 'Войти',
login: 'Войти',
remember: 'Запомнить',
code: 'Код',
enterAuthenticationCode: 'Введите ваш код двухфакторной аутентификации',
},
};
2 changes: 2 additions & 0 deletions src/locale/ua/ua.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ export default {
signIn: 'Увійти',
login: 'Увійти',
remember: 'Запам\'ятати',
code: 'Код',
enterAuthenticationCode: 'Введіть ваш код двофакторної автентифікації',
},
};
48 changes: 35 additions & 13 deletions src/store/modules/auth/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,31 @@ const state = {
...defaultState(),
rememberCredentials: localStorage.getItem('auth/rememberCredentials') === 'true',
loginProviders: {},
enabledTfa: false,
totp: '',
sessionId: '', // it's necessary for two-factor authentication
};

const getters = {};

const actions = {
SUBMIT_AUTH: async (context, action) => {
let accessToken;
switch (action) {
case 'login':
accessToken = await context.dispatch('LOGIN');
break;
case 'register':
accessToken = await context.dispatch('REGISTER');
break;
default:
throw new Error(`Invalid action: ${action}`);
}

if(action === 'login') {
if(context.state.sessionId) {
accessToken = await context.dispatch('LOGIN_2FA');
}
accessToken = await context.dispatch('LOGIN');
} else if(action === 'register') {
accessToken = await context.dispatch('REGISTER');
} else throw new Error(`Invalid action: ${action}`);

return context.dispatch('ON_AUTH_SUCCESS', { accessToken });
},

LOGIN: (context) => {
return AuthAPI.login({
LOGIN: async (context) => {
return await AuthAPI.login({
username: context.state.username,
password: context.state.password,
domain: context.state.domain,
Expand All @@ -52,11 +54,31 @@ const actions = {
});
},

LOGIN_2FA: (context) => {
return AuthAPI.login2fa({
id: context.state.sessionId,
totp: context.state.totp,
})
},

GET_2FA_SESSION_ID: async (context) => {
const { id } = await AuthAPI.login({
username: context.state.username,
password: context.state.password,
domain: context.state.domain,
});

if(id) {
await context.dispatch('SET_PROPERTY', { prop: 'sessionId', value: id });
}
},

LOAD_SERVICE_PROVIDERS: async (context) => {
const domain = context.state.domain;
const response = await AuthAPI.loadServiceProviders({ domain });
const { federation = {} } = response;
const { federation = {}, enabledTfa } = response;
context.commit('SET_SERVICE_PROVIDERS', federation);
await context.dispatch('SET_PROPERTY', { prop: 'enabledTfa', value: enabledTfa });
},

EXECUTE_PROVIDER: (context, { ticket }) => {
Expand Down

0 comments on commit 75c4963

Please sign in to comment.