You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by ha...@apache.org on 2022/11/24 03:43:44 UTC

[cloudstack] 03/03: Adding setup 2FA at login page

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 <ha...@gmail.com>
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>