This is an automated email from the ASF dual-hosted git repository. harikrishna pushed a commit to branch 2FA in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 7bbad688ef79a8366ce83bd7fdb59a3cb42f5710 Author: Harikrishna Patnala <[email protected]> AuthorDate: Thu Nov 24 09:11:33 2022 +0530 Adding setup 2FA at login page --- .../org/apache/cloudstack/api/ApiConstants.java | 1 + .../cloudstack/api/response/LoginCmdResponse.java | 12 ++ server/src/main/java/com/cloud/api/ApiServer.java | 4 + .../api/auth/APIAuthenticationManagerImpl.java | 2 - .../main/java/com/cloud/user/AccountManager.java | 14 +- .../java/com/cloud/user/AccountManagerImpl.java | 9 +- ui/src/config/router.js | 6 +- ui/src/permission.js | 4 +- ui/src/store/getters.js | 1 + ui/src/store/modules/user.js | 8 +- ui/src/views/auth/Login.vue | 4 +- ui/src/views/dashboard/Dashboard.vue | 4 +- ui/src/views/dashboard/TwoFa.vue | 172 --------------------- 13 files changed, 50 insertions(+), 191 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 786b0ddbbad..71bb1d6252c 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -909,6 +909,7 @@ public class ApiConstants { public static final String ADMINS_ONLY = "adminsonly"; public static final String ANNOTATION_FILTER = "annotationfilter"; public static final String TWOFACTORAUTHENTICATIONCODE = "2facode"; + public static final String TWOFACTORAUTHENTICATIONPROVIDER = "twofaprovider"; public static final String SECRET_CODE = "secretcode"; public static final String LOGIN = "login"; public static final String LOGOUT = "logout"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java index 94cf380cb05..3c7e35eaf99 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java @@ -78,6 +78,10 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { @Param(description = "Is two factor authentication verified") private String is2FAverified; + @SerializedName(value = ApiConstants.TWOFACTORAUTHENTICATIONPROVIDER) + @Param(description = "Two factor authentication provider") + private String twoFAprovider; + public String getUsername() { return username; } @@ -187,4 +191,12 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { public void set2FAverfied(String is2FAverified) { this.is2FAverified = is2FAverified; } + + public String get2FAprovider() { + return twoFAprovider; + } + + public void set2FAprovider(String twoFAprovider) { + this.twoFAprovider = twoFAprovider; + } } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 2979639aabe..9b4a617d49e 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -1075,6 +1075,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer if (ApiConstants.IS_2FA_VERIFIED.equalsIgnoreCase(attrName)) { response.set2FAverfied(attrObj.toString()); } + if (ApiConstants.TWOFACTORAUTHENTICATIONPROVIDER.equalsIgnoreCase(attrName)) { + response.set2FAprovider(attrObj.toString()); + } } } response.setResponseName("loginresponse"); @@ -1140,6 +1143,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer session.setAttribute(ApiConstants.IS_2FA_ENABLED, Boolean.toString(userAcct.isTwoFactorAuthenticationEnabled())); session.setAttribute(ApiConstants.IS_2FA_VERIFIED, false); + session.setAttribute(ApiConstants.TWOFACTORAUTHENTICATIONPROVIDER, userAcct.getUser2faProvider()); // (bug 5483) generate a session key that the user must submit on every request to prevent CSRF, add that // to the login response so that session-based authenticators know to send the key back diff --git a/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java b/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java index d847e5c6135..6ec9ff9c1ce 100644 --- a/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java +++ b/server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java @@ -32,8 +32,6 @@ import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; import com.cloud.utils.component.ComponentContext; import com.cloud.utils.component.ManagerBase; -import static com.cloud.user.AccountManager.enable2FA; - @SuppressWarnings("unchecked") public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager { public static final Logger s_logger = Logger.getLogger(APIAuthenticationManagerImpl.class.getName()); diff --git a/server/src/main/java/com/cloud/user/AccountManager.java b/server/src/main/java/com/cloud/user/AccountManager.java index 46936fb47f3..7990a826a97 100644 --- a/server/src/main/java/com/cloud/user/AccountManager.java +++ b/server/src/main/java/com/cloud/user/AccountManager.java @@ -190,11 +190,19 @@ public interface AccountManager extends AccountService, Configurable { ConfigKey<Boolean> UseSecretKeyInResponse = new ConfigKey<Boolean>("Advanced", Boolean.class, "use.secret.key.in.response", "false", "This parameter allows the users to enable or disable of showing secret key as a part of response for various APIs. By default it is set to false.", true); - ConfigKey<Boolean> enable2FA = new ConfigKey<Boolean>("Advanced", + ConfigKey<Boolean> enableUserTwoFactorAuthentication = new ConfigKey<Boolean>("Advanced", Boolean.class, - "enable.two.factor.authentication", + "enable.user.two.factor.authentication", "false", - "Determines whether two factor authentication is enabled or not. This can be done at domain level as well", + "Determines whether two factor authentication is enabled or not. This can be configured at domain level also", + false, + ConfigKey.Scope.Domain); + + ConfigKey<Boolean> mandateUserTwoFactorAuthentication = new ConfigKey<Boolean>("Advanced", + Boolean.class, + "mandate.user.two.factor.authentication", + "false", + "Determines whether to make the two factor authentication mandatory or not. This can be configured at domain level also", false, ConfigKey.Scope.Domain); diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index c7fb0737427..b6483c20fa2 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -2386,7 +2386,10 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M if (userUUID == null) { userUUID = UUID.randomUUID().toString(); } - UserVO user = _userDao.persist(new UserVO(accountId, userName, encodedPassword, firstName, lastName, email, timezone, userUUID, source)); + + UserVO userVO = new UserVO(accountId, userName, encodedPassword, firstName, lastName, email, timezone, userUUID, source); + userVO.setTwoFactorAuthenticationEnabled(mandateUserTwoFactorAuthentication.valueIn(getAccount(accountId).getDomainId())); + UserVO user = _userDao.persist(userVO); CallContext.current().putContextParameter(User.class, user.getUuid()); return user; } @@ -3143,7 +3146,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M @Override public ConfigKey<?>[] getConfigKeys() { - return new ConfigKey<?>[] {UseSecretKeyInResponse, enable2FA, userTwoFactorAuthenticationProviderPlugin}; + return new ConfigKey<?>[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication, userTwoFactorAuthenticationProviderPlugin}; } public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticationProviders() { @@ -3204,7 +3207,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M UserAccountVO userAccount = _userAccountDao.findById(userId); UserVO userVO = _userDao.findById(userId); - if (!enable2FA.valueIn(userAccount.getDomainId())) { + if (!enableUserTwoFactorAuthentication.valueIn(userAccount.getDomainId())) { throw new CloudRuntimeException("2FA is not enabled for this domain or at global level"); } diff --git a/ui/src/config/router.js b/ui/src/config/router.js index 349e5da7d71..a1d5be7522d 100644 --- a/ui/src/config/router.js +++ b/ui/src/config/router.js @@ -308,13 +308,13 @@ export const constantRouterMap = [ ] }, { - path: '/2FA', - name: 'TwoFa', + path: '/verify2FA', + name: 'VerifyTwoFa', meta: { title: 'label.two.factor.authentication', hidden: true }, - component: () => import('@/views/dashboard/TwoFa') + component: () => import('@/views/dashboard/VerifyTwoFa') }, { path: '/403', diff --git a/ui/src/permission.js b/ui/src/permission.js index 834d0e098be..8dbed2e950e 100644 --- a/ui/src/permission.js +++ b/ui/src/permission.js @@ -57,10 +57,9 @@ router.beforeEach((to, from, next) => { const validLogin = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('userid') || Cookies.get('userid', { path: '/client' }) if (validLogin) { if (to.path === '/user/login') { - console.log('hari3') next({ path: '/dashboard' }) NProgress.done() - } else if (to.path === '/2FA') { + } else if (to.path === '/verify2FA') { if (store.getters.twoFaEnabled && !store.getters.loginFlag) { console.log('Do Two-factor authentication') next() @@ -69,7 +68,6 @@ router.beforeEach((to, from, next) => { NProgress.done() } } else { - console.log('hari4') if (Object.keys(store.getters.apis).length === 0) { const cachedApis = vueProps.$localStorage.get(APIS, {}) if (Object.keys(cachedApis).length > 0) { diff --git a/ui/src/store/getters.js b/ui/src/store/getters.js index 5e4393fa7aa..7c17195dbdc 100644 --- a/ui/src/store/getters.js +++ b/ui/src/store/getters.js @@ -45,6 +45,7 @@ const getters = { customColumns: state => state.user.customColumns, logoutFlag: state => state.user.logoutFlag, twoFaEnabled: state => state.user.twoFaEnabled, + twoFaProvider: state => state.user.twoFaProvider, loginFlag: state => state.user.loginFlag } diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 42c7db72ca3..222688d9185 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -61,7 +61,8 @@ const user = { loginFlag: false, logoutFlag: false, customColumns: {}, - twoFaEnabled: false + twoFaEnabled: false, + twoFaProvider: '' }, mutations: { @@ -137,6 +138,9 @@ const user = { SET_2FA_ENABLED: (state, flag) => { state.twoFaEnabled = flag }, + SET_2FA_PROVIDER: (state, flag) => { + state.twoFaProvider = flag + }, SET_LOGIN_FLAG: (state, flag) => { state.loginFlag = flag } @@ -181,6 +185,7 @@ const user = { commit('SET_DOMAIN_STORE', {}) commit('SET_LOGOUT_FLAG', false) commit('SET_2FA_ENABLED', (result.is2faenabled === 'true')) + commit('SET_2FA_PROVIDER', result.twofaprovider) commit('SET_LOGIN_FLAG', false) notification.destroy() @@ -310,6 +315,7 @@ const user = { commit('SET_DOMAIN_STORE', {}) commit('SET_LOGOUT_FLAG', true) commit('SET_2FA_ENABLED', false) + commit('SET_2FA_PROVIDER', '') commit('SET_LOGIN_FLAG', false) vueProps.$localStorage.remove(CURRENT_PROJECT) vueProps.$localStorage.remove(ACCESS_TOKEN) diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue index 4b1f773ddb0..0a64fbe3925 100644 --- a/ui/src/views/auth/Login.vue +++ b/ui/src/views/auth/Login.vue @@ -298,8 +298,8 @@ export default { loginSuccess (res) { this.$notification.destroy() this.$store.commit('SET_COUNT_NOTIFY', 0) - if (store.getters.twoFaEnabled === true) { - this.$router.push({ path: '/2FA' }).catch(() => {}) + if (store.getters.twoFaEnabled === true && store.getters.twoFaProvider !== '') { + this.$router.push({ path: '/verify2FA' }).catch(() => {}) } else { this.$store.commit('SET_LOGIN_FLAG', true) this.$router.push({ path: '/dashboard' }).catch(() => {}) diff --git a/ui/src/views/dashboard/Dashboard.vue b/ui/src/views/dashboard/Dashboard.vue index e07b7735bdd..e5bab748fb6 100644 --- a/ui/src/views/dashboard/Dashboard.vue +++ b/ui/src/views/dashboard/Dashboard.vue @@ -35,7 +35,7 @@ import store from '@/store' import CapacityDashboard from './CapacityDashboard' import UsageDashboard from './UsageDashboard' import OnboardingDashboard from './OnboardingDashboard' -import TwoFa from './TwoFa' +import VerifyTwoFa from './VerifyTwoFa' export default { name: 'Dashboard', @@ -43,7 +43,7 @@ export default { CapacityDashboard, UsageDashboard, OnboardingDashboard, - TwoFa + VerifyTwoFa }, provide: function () { return { diff --git a/ui/src/views/dashboard/TwoFa.vue b/ui/src/views/dashboard/TwoFa.vue deleted file mode 100644 index acbd87170a5..00000000000 --- a/ui/src/views/dashboard/TwoFa.vue +++ /dev/null @@ -1,172 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -<template> - <a-form> - <img - v-if="$config.banner" - :style="{ - width: $config.theme['@banner-width'], - height: $config.theme['@banner-height'] - }" - :src="$config.banner" - class="user-layout-logo" - alt="logo"> - <h1 style="text-align: center; font-size: 24px; color: gray"> {{ $t('label.two.factor.authentication') }} </h1> - <br /> - <br /> - <a-form - :ref="formRef" - :model="form" - :rules="rules" - @finish="handleSubmit" - layout="vertical"> - <a-form-item name="code" ref="code"> - <a-input - class="center-align" - style="width: 400px" - v-model:value="form.code" - placeholder="xxxxxxx" /> - </a-form-item> - <div :span="24" class="center-align top-padding"> - <a-button - :loading="loading" - ref="submit" - type="primary" - class="center-align" - @click="handleSubmit">{{ $t('label.verify') }} - </a-button> - </div> - <p style="text-align: center" v-html="$t('message.two.fa.auth')"></p> - </a-form> - </a-form> -</template> -<script> - -import { api } from '@/api' -import { ref, reactive, toRaw } from 'vue' - -export default { - name: 'TwoFa', - data () { - return { - twoFAresponse: false - } - }, - created () { - this.initForm() - }, - methods: { - initForm () { - this.formRef = ref() - this.form = reactive({}) - this.rules = reactive({ - code: [{ required: true, message: this.$t('message.error.authentication.code') }] - }) - }, - handleSubmit () { - this.formRef.value.validate().then(() => { - const values = toRaw(this.form) - api('validateUserTwoFactorAuthenticationCode', { '2facode': values.code }).then(response => { - this.twoFAresponse = true - if (this.twoFAresponse) { - this.$notification.destroy() - this.$store.commit('SET_COUNT_NOTIFY', 0) - this.$store.commit('SET_LOGIN_FLAG', true) - this.$router.push({ path: '/dashboard' }).catch(() => {}) - - this.$message.success({ - content: `${this.$t('label.action.enable.two.factor.authentication')}`, - duration: 2 - }) - this.$emit('refresh-data') - } - console.log(response) - }).catch(error => { - this.$notification.error({ - message: this.$t('message.request.failed'), - description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message - }) - }) - }) - } - } -} -</script> -<style lang="less" scoped> - .center-align { - display: block; - margin-left: auto; - margin-right: auto; - } - .top-padding { - padding-top: 35px; - } - .note { - text-align: center; - color: grey; - padding-top: 10px; - } - - .user-layout { - height: 100%; - - &-container { - padding: 3rem 0; - width: 100%; - - @media (min-height:600px) { - padding: 0; - position: relative; - top: 50%; - transform: translateY(-50%); - margin-top: -50px; - } - } - - &-logo { - border-style: none; - margin: 0 auto 2rem; - display: block; - - .mobile & { - max-width: 300px; - margin-bottom: 1rem; - } - } - - &-footer { - display: flex; - flex-direction: column; - position: absolute; - bottom: 20px; - text-align: center; - width: 100%; - - @media (max-height: 600px) { - position: relative; - margin-top: 50px; - } - - label { - width: 368px; - font-weight: 500; - margin: 0 auto; - } - } - } -</style>
