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/27 17:18:41 UTC

[cloudstack] 02/03: Fixed 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 9018e8841556f4d6ca75a51d256d3262ebce03bf
Author: Harikrishna Patnala <ha...@gmail.com>
AuthorDate: Fri Nov 25 16:03:05 2022 +0530

    Fixed 2FA at login page
---
 ui/src/views/auth/Login.vue                  |   4 +-
 ui/src/views/dashboard/SetupTwoFaAtLogin.vue | 248 +++++++++++++++++++++++++++
 2 files changed, 250 insertions(+), 2 deletions(-)

diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue
index e4c52eaded9..34f5a7040d5 100644
--- a/ui/src/views/auth/Login.vue
+++ b/ui/src/views/auth/Login.vue
@@ -298,9 +298,9 @@ export default {
     loginSuccess (res) {
       this.$notification.destroy()
       this.$store.commit('SET_COUNT_NOTIFY', 0)
-      if (store.getters.twoFaEnabled === true && store.getters.twoFaProvider !== '') {
+      if (store.getters.twoFaEnabled === true && store.getters.twoFaProvider !== '' && store.getters.twoFaProvider !== undefined) {
         this.$router.push({ path: '/verify2FA' }).catch(() => {})
-      } else if (store.getters.twoFaEnabled === true && store.getters.twoFaProvider === '') {
+      } else if (store.getters.twoFaEnabled === true && (store.getters.twoFaProvider === '' || store.getters.twoFaProvider === undefined)) {
         this.$router.push({ path: '/setup2FA' }).catch(() => {})
       } else {
         this.$store.commit('SET_LOGIN_FLAG', true)
diff --git a/ui/src/views/dashboard/SetupTwoFaAtLogin.vue b/ui/src/views/dashboard/SetupTwoFaAtLogin.vue
new file mode 100644
index 00000000000..0c1d8202974
--- /dev/null
+++ b/ui/src/views/dashboard/SetupTwoFaAtLogin.vue
@@ -0,0 +1,248 @@
+// 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>
+  <div class="center">
+    <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>
+    <h3> {{ $t('label.select.2fa.provider') }} </h3>
+    <a-form
+      :rules="rules"
+      layout="vertical">
+      <div class="form-layout form-align" v-ctrl-enter="submitPin">
+         <a-select
+          :disabled="twoFAenabled === true"
+          v-model:value="selectedProvider"
+          optionFilterProp="label"
+          :filterOption="(input, option) => {
+            return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+          }"
+          style="width: 100%"
+          @change="val => { handleSelectChange(val) }">
+          <a-select-option
+            v-for="(opt) in providers"
+            :key="opt.name"
+            :disabled="opt.enabled === false">
+              {{ opt.name }}
+          </a-select-option>
+        </a-select>
+        <div :span="24" v-if="selectedProvider">
+          <a-button ref="submit" type="primary" @click="setup2FAProvider">{{ $t('label.setup') }}</a-button>
+        </div>
+      </div>
+      <div v-if="twoFAenabled">
+        <div v-if="selectedProvider !== 'staticpin'">
+          <br />
+          <p v-html="$t('message.two.fa.register.account')"></p>
+          <vue-qrious
+            class="center-align"
+            :value="googleUrl"
+            @change="onDataUrlChange"
+          />
+          <div style="text-align: center"> <a @click="showConfiguredPin"> {{ $t('message.two.fa.view.setup.key') }}</a></div>
+        </div>
+        <div v-if="selectedProvider === 'staticpin'">
+          <br>
+          <p v-html="$t('message.two.fa.staticpin')"></p>
+          <br>
+          <div> <a @click="showConfiguredPin"> {{ $t('message.two.fa.view.static.pin') }}</a></div>
+        </div>
+        <div v-if="selectedProvider">
+          <br />
+          <h3> {{ $t('label.enter.code') }} </h3>
+          <a-form @finish="submitPin" v-ctrl-enter="submitPin" class="container">
+            <a-input v-model:value="code" />
+            <div :span="24">
+              <a-button ref="submit" type="primary" @click="submitPin">{{ $t('label.verify') }}</a-button>
+            </div>
+          </a-form>
+        </div>
+
+        <a-modal
+          v-if="showPin"
+          :visible="showPin"
+          :title="$t(selectedProvider === 'staticpin'? 'label.two.factor.authentication.static.pin' : 'label.two.factor.authentication.secret.key')"
+          :closable="true"
+          :footer="null"
+          @cancel="onCloseModal"
+          centered
+          width="450px">
+          <div> {{ pin }} </div>
+        </a-modal>
+      </div>
+    </a-form>
+  </div>
+</template>
+<script>
+
+import { api } from '@/api'
+import VueQrious from 'vue-qrious'
+import eventBus from '@/config/eventBus'
+export default {
+  name: 'RegisterTwoFactorAuth',
+  props: {
+    resource: {
+      type: Object,
+      required: true
+    }
+  },
+  components: {
+    VueQrious
+  },
+  data () {
+    return {
+      googleUrl: '',
+      dataUrl: '',
+      pin: '',
+      code: '',
+      showPin: false,
+      twoFAenabled: false,
+      twoFAverified: false,
+      providers: [],
+      selectedProvider: null
+    }
+  },
+  mounted () {
+    this.list2FAProviders()
+  },
+  created () {
+    eventBus.on('action-closing', (args) => {
+      if (args.action.api === 'setupUserTwoFactorAuthentication' && this.twoFAenabled && !this.twoFAverified) {
+        this.disable2FAProvider()
+      }
+    })
+  },
+  methods: {
+    onDataUrlChange (dataUrl) {
+      this.dataUrl = dataUrl
+    },
+    handleSelectChange (val) {
+      this.selectedProvider = val
+    },
+    setup2FAProvider () {
+      if (!this.twoFAenabled) {
+        api('setupUserTwoFactorAuthentication', { provider: this.selectedProvider }).then(response => {
+          console.log(response)
+          this.pin = response.setupusertwofactorauthenticationresponse.setup2fa.secretcode
+          if (this.selectedProvider === 'google') {
+            this.username = response.setupusertwofactorauthenticationresponse.setup2fa.username
+            this.googleUrl = 'otpauth://totp/CloudStack:' + this.username + '?secret=' + this.pin + '&issuer=CloudStack'
+            this.showPin = false
+          }
+          if (this.selectedProvider === 'staticpin') {
+            this.showPin = true
+          }
+          this.twoFAenabled = true
+          this.twoFAverified = false
+        }).catch(error => {
+          this.$notification.error({
+            message: this.$t('message.request.failed'),
+            description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
+          })
+        })
+      }
+    },
+    disable2FAProvider () {
+      api('setupUserTwoFactorAuthentication', { enable: false }).then(response => {
+        this.showPin = false
+        this.twoFAenabled = false
+        this.twoFAverified = false
+      }).catch(error => {
+        this.$notification.error({
+          message: this.$t('message.request.failed'),
+          description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
+        })
+      })
+    },
+    list2FAProviders () {
+      api('listUserTwoFactorAuthenticatorProviders', {}).then(response => {
+        this.providers = response.listusertwofactorauthenticatorprovidersresponse.providers || []
+      })
+    },
+    submitPin () {
+      api('validateUserTwoFactorAuthenticationCode', { '2facode': this.code }).then(response => {
+        this.$message.success({
+          content: `${this.$t('label.action.enable.two.factor.authentication')}`,
+          duration: 2
+        })
+        this.$notification.destroy()
+        this.$store.commit('SET_COUNT_NOTIFY', 0)
+        this.$store.commit('SET_LOGIN_FLAG', true)
+        this.$router.push({ path: '/dashboard' }).catch(() => {})
+
+        this.twoFAverified = true
+        this.$emit('refresh-data')
+      }).catch(error => {
+        this.$notification.error({
+          message: this.$t('message.request.failed'),
+          description: (error.response && error.response.headers && error.response.headers['x-description']) || error.message
+        })
+      })
+      this.closeAction()
+    },
+    closeAction () {
+      this.$emit('close-action')
+    },
+    showConfiguredPin () {
+      this.showPin = true
+    },
+    onCloseModal () {
+      this.showPin = false
+    }
+  }
+}
+</script>
+
+<style scoped>
+  .center {
+    position: fixed;
+    top: 42.5%;
+    left: 50%;
+    -webkit-transform: translate(-50%, -50%);
+
+    background-color: #D3D3D3;
+    padding: 70px 50px 70px 50px;
+    z-index: 100;
+  }
+  .center-align {
+    display: block;
+    margin-left: auto;
+    margin-right: auto;
+  }
+  .form-align {
+    display: flex;
+    flex-direction: row;
+  }
+  .top-padding {
+    padding-top: 35px;
+  }
+  .container {
+    display: flex;
+  }
+
+</style>