Skip to content

Commit

Permalink
Merge pull request #4920 from cloudforet-io/feature-mfa-ms-authenticator
Browse files Browse the repository at this point in the history
feat: apply mfa (ms authenticator)
  • Loading branch information
skdud4659 authored Oct 29, 2024
2 parents 471d54e + 4a5e8c2 commit 2518f97
Show file tree
Hide file tree
Showing 55 changed files with 2,728 additions and 1,255 deletions.
6 changes: 4 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@vercel/edge-config": "^0.2.1",
"@vueuse/components": "^9.0.2",
"@vueuse/core": "^10.7.2",
"@vueuse/integrations": "^11.1.0",
"@vvo/tzdb": "^6.4.1",
"animated-number-vue": "^1.0.0",
"axios": "^1.7.4",
Expand All @@ -57,7 +58,7 @@
"html-to-image": "^1.9.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jwt-decode": "^3.1.2",
"jwt-decode": "^4.0.0",
"keycloak-js": "^10.0.2",
"lodash": "^4.17.21",
"numeral": "^2.0.6",
Expand All @@ -80,7 +81,8 @@
"vue-selecto": "^1.6.4",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"webfontloader": "^1.6.28"
"webfontloader": "^1.6.28",
"qrcode": "^1.5.4"
},
"devDependencies": {
"@babel/preset-env": "^7.21.4",
Expand Down
71 changes: 0 additions & 71 deletions apps/web/src/common/modules/button/verify-button/VerifyButton.vue

This file was deleted.

21 changes: 18 additions & 3 deletions apps/web/src/lib/helper/multi-factor-auth-helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { TranslateResult } from 'vue-i18n';

import { SpaceConnector } from '@cloudforet/core-lib/space-connector';

import type { UserProfileConfirmMfaParameters } from '@/schema/identity/user-profile/api-verbs/confirm-mfa';
import type { UserProfileEnableMfaParameters } from '@/schema/identity/user-profile/api-verbs/enable-mfa';
import { MULTI_FACTOR_AUTH_TYPE } from '@/schema/identity/user-profile/constant';
import type { UserDisableMfaParameters } from '@/schema/identity/user/api-verbs/disable-mfa';
import { store } from '@/store';
import { i18n } from '@/translations';
Expand All @@ -18,7 +21,9 @@ export const postEnableMfa = async (body: UserProfileEnableMfaParameters, setUse
if (setUser) {
await store.dispatch('user/setUser', response);
}
showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_SENT_EMAIL'), '');
if (response.mfa.mfa_type === MULTI_FACTOR_AUTH_TYPE.EMAIL) {
showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_SENT_EMAIL'), '');
}
return response;
} catch (e: any) {
showErrorMessage(e.message, e);
Expand All @@ -30,7 +35,9 @@ export const postEnableMfa = async (body: UserProfileEnableMfaParameters, setUse
export const postUserProfileDisableMfa = async (): Promise<UserState|Error> => {
try {
const response = await SpaceConnector.clientV2.identity.userProfile.disableMfa<undefined, UserState>();
showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_SENT_EMAIL'), '');
if (response.mfa.mfa_type === MULTI_FACTOR_AUTH_TYPE.EMAIL) {
showSuccessMessage(i18n.t('COMMON.MFA_MODAL.ALT_S_SENT_EMAIL'), '');
}
return response;
} catch (e: any) {
showErrorMessage(e.message, e);
Expand All @@ -41,7 +48,15 @@ export const postUserProfileDisableMfa = async (): Promise<UserState|Error> => {

export const postValidationMfaCode = async (body: UserProfileConfirmMfaParameters): Promise<void|Error> => {
try {
return SpaceConnector.clientV2.identity.userProfile.confirmMfa<UserProfileConfirmMfaParameters>(body);
const userInfo = await SpaceConnector.clientV2.identity.userProfile.confirmMfa<UserProfileConfirmMfaParameters>(body);
let successMessage: TranslateResult;
if (userInfo.mfa.state === 'ENABLED') {
successMessage = i18n.t('COMMON.MFA_MODAL.ALT_S_ENABLED');
} else {
successMessage = i18n.t('COMMON.MFA_MODAL.ALT_S_DISABLED');
}
showSuccessMessage(successMessage, '');
return userInfo;
} catch (e: any) {
ErrorHandler.handleError(e);
throw e;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/router/helpers/route-helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Route, NavigationGuardNext } from 'vue-router';

import type { JwtPayload } from 'jwt-decode';
import jwtDecode from 'jwt-decode';
import { jwtDecode } from 'jwt-decode';

import { SpaceConnector } from '@cloudforet/core-lib/space-connector';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { MultiFactorAuthType } from '@/schema/identity/user-profile/type';

export interface UserProfileEnableMfaParameters {
mfa_type: string;
mfa_type: MultiFactorAuthType;
options: Record<string, any>;
}
4 changes: 4 additions & 0 deletions apps/web/src/schema/identity/user-profile/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const MULTI_FACTOR_AUTH_TYPE = {
OTP: 'OTP',
EMAIL: 'EMAIL',
} as const;
3 changes: 3 additions & 0 deletions apps/web/src/schema/identity/user-profile/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { MULTI_FACTOR_AUTH_TYPE } from '@/schema/identity/user-profile/constant';

export type MultiFactorAuthType = typeof MULTI_FACTOR_AUTH_TYPE[keyof typeof MULTI_FACTOR_AUTH_TYPE];
8 changes: 5 additions & 3 deletions apps/web/src/schema/identity/user/model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Tags } from '@/schema/_common/model';
import type { RoleType } from '@/schema/identity/role/type';
import type { MultiFactorAuthType } from '@/schema/identity/user-profile/type';
import type {
AuthType, UserState, UserMfaState, UserMfaType,
AuthType, UserState, UserMfaState,
} from '@/schema/identity/user/type';

export interface UserModel {
Expand All @@ -26,9 +27,10 @@ export interface UserModel {

export interface UserMfa {
state: UserMfaState,
mfa_type: UserMfaType,
mfa_type: MultiFactorAuthType,
options: {
email: string,
email?: string,
user_secret_id?: string,
}
}

Expand Down
1 change: 0 additions & 1 deletion apps/web/src/schema/identity/user/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ export type UserState = 'ENABLED' | 'DISABLED' | 'PENDING';
export type AuthType = 'EXTERNAL' | 'LOCAL';
export type Action = 'UPDATE_PASSWORD';
export type UserMfaState = 'ENABLED' | 'DISABLED';
export type UserMfaType = 'EMAIL';
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ const onErrorSignIn = (e, token) => {
state.token = token;
if (e.message.includes('MFA')) {
const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
const userEmail = e.message.match(emailRegex);
const mfaTypeRegex = /mfa_type\s*=\s*(\w+)/;
router.push({
name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH._NAME,
params: {
accessToken: state.token,
mfaEmail: userEmail[0],
mfaEmail: e.message.match(emailRegex)[0],
mfaType: e.message.match(mfaTypeRegex)[1],
userId: state.userId?.trim() as string,
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ const signIn = async () => {
} catch (e: any) {
if (e.message.includes('MFA')) {
const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
const userEmail = e.message.match(emailRegex);
const mfaTypeRegex = /mfa_type\s*=\s*(\w+)/;
await router.push({
name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH._NAME,
params: {
password: credentials.password,
mfaEmail: userEmail[0],
mfaEmail: e.message.match(emailRegex)[0],
mfaType: e.message.match(mfaTypeRegex)[1],
userId: state.userId?.trim() as string,
},
});
Expand Down
83 changes: 83 additions & 0 deletions apps/web/src/services/auth/components/CollapsibleContentsOTP.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { PLink, PDataLoader } from '@cloudforet/mirinae';
interface Props {
loading: boolean
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
});
</script>

<template>
<p-data-loader class="collapsible-contents-wrapper"
:loading="props.loading"
>
<div class="contents-item">
<p class="title">
{{ $t('AUTH.COLLAPSED.OTP_EXTENSION_TITLE_1') }}
</p>
<ul class="list">
<li>{{ $t('AUTH.COLLAPSED.OTP_EXTENSION_REASON_1') }}</li>
<li>{{ $t('AUTH.COLLAPSED.OTP_EXTENSION_REASON_2') }}</li>
</ul>
</div>
<div class="contents-item">
<p class="title">
{{ $t('AUTH.COLLAPSED.OTP_EXTENSION_TITLE_2') }}
</p>
<p>
<i18n path="AUTH.COLLAPSED.OTP_EXTENSION_DESC_1">
<template #guide>
<p-link href="https://support.microsoft.com/en-us/account-billing/about-microsoft-authenticator-9783c865-0308-42fb-a519-8cf666fe0acc"
new-tab
:text="$t('AUTH.COLLAPSED.GOOGLE_GUIDE')"
highlight
/>
</template>
<template #contact>
<span class="contact-help-text">
{{ $t('AUTH.COLLAPSED.EXTENSION_CONTACT') }}
</span>
</template>
</i18n>
</p>
</div>
</p-data-loader>
</template>

<style lang="postcss" scoped>
.collapsible-contents-wrapper {
@apply text-paragraph-sm;
max-width: 23rem;
.contents-item {
.title {
@apply font-bold;
}
.contact-help-text {
@apply text-violet-600;
}
li {
@apply relative;
padding-left: 1.25rem;
&::before {
@apply absolute;
content: '·';
top: 0;
left: 0.5rem;
}
.re-send-button {
@apply inline-block text-paragraph-sm text-blue-700;
padding: 0;
}
}
& + .contents-item {
margin-top: 1.625rem;
}
a {
@apply text-blue-700;
}
}
}
</style>
15 changes: 11 additions & 4 deletions apps/web/src/services/auth/components/SignInContainer.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
<script lang="ts" setup>
import { useWindowSize } from '@vueuse/core/index';
import { computed, reactive } from 'vue';
import { useRoute } from 'vue-router/composables';
import { screens } from '@cloudforet/mirinae';
import ConsoleLogo from '@/services/auth/components/ConsoleLogo.vue';
import SignInLeftContainer from '@/services/auth/components/SignInLeftContainer.vue';
const route = useRoute();
const { width } = useWindowSize();
const state = reactive({
isMobileSize: computed<boolean>(() => width.value < screens.mobile.max),
isCentered: computed<boolean>(() => route.meta?.isCentered),
});
</script>

<template>
<div class="sign-in-container">
<console-logo v-if="!state.isMobileSize" />
<div class="contents-wrapper">
<sign-in-left-container />
<div class="contents-wrapper"
:class="{'is-centered':state.isCentered }"
>
<sign-in-left-container v-if="!state.isCentered" />
<router-view />
</div>
</div>
Expand All @@ -29,12 +33,15 @@ const state = reactive({
<style lang="postcss" scoped>
.sign-in-container {
.contents-wrapper {
@apply flex absolute bg-white;
@apply flex absolute;
width: 100%;
height: 100%;
top: 0;
bottom: 0;
background-image: url('@/assets/images/img_blurred-background-min.png');
&:not(.is-centered) {
@apply bg-white;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/services/auth/pages/KeycloakPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ const onErrorSignIn = (e, token) => {
state.token = token;
if (e.message.includes('MFA')) {
const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g;
const userEmail = e.message.match(emailRegex);
const mfaTypeRegex = /mfa_type\s*=\s*(\w+)/;
router.push({
name: AUTH_ROUTE.SIGN_IN.MULTI_FACTOR_AUTH._NAME,
params: {
accessToken: state.token,
mfaEmail: userEmail[0],
mfaEmail: e.message.match(emailRegex)[0],
mfaType: e.message.match(mfaTypeRegex)[1],
userId: state.beforeUser,
},
});
Expand Down
Loading

0 comments on commit 2518f97

Please sign in to comment.