diff --git a/frontend/package.json b/frontend/package.json
index dec25024e..5aacece55 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,7 +23,8 @@
"vue-gettext": "^2.1.12",
"vue-router": "^3.2.0",
"vuetify": "^2.6.10",
- "vuex": "^3.4.0"
+ "vuex": "^3.4.0",
+ "qrcode.vue": "~1.7.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",
diff --git a/frontend/src/api/account.js b/frontend/src/api/account.js
index 07e631b57..e9617d813 100644
--- a/frontend/src/api/account.js
+++ b/frontend/src/api/account.js
@@ -16,8 +16,8 @@ export default {
startTFASetup () {
return repository.post(`${resource}/tfa/setup/`)
},
- getQRCodeForTFASetup () {
- return repository.get(`${resource}/tfa/setup/qr_code/`)
+ getKeyForTFASetup () {
+ return repository.get(`${resource}/tfa/setup/key/`)
},
finalizeTFASetup (pinCode) {
const payload = { pin_code: pinCode }
diff --git a/frontend/src/components/user/TwoFactorAuthForm.vue b/frontend/src/components/user/TwoFactorAuthForm.vue
index 7bf933c34..48d66b4ec 100644
--- a/frontend/src/components/user/TwoFactorAuthForm.vue
+++ b/frontend/src/components/user/TwoFactorAuthForm.vue
@@ -5,10 +5,29 @@
Two factor authentication
-
+
-
+
+
+
+
+
+ Click here to copy the key
+
+ mdi-check-all
+
+
+
@@ -95,10 +114,12 @@
import account from '@/api/account'
import Cookies from 'js-cookie'
import RecoveryCodesResetDialog from './RecoveryCodesResetDialog'
+import QrcodeVue from 'qrcode.vue'
export default {
components: {
- RecoveryCodesResetDialog
+ RecoveryCodesResetDialog,
+ QrcodeVue
},
props: {
account: Object
@@ -107,20 +128,27 @@ export default {
return {
newTokens: [],
pinCode: null,
- qrCode: null,
+ key: null,
+ qrURL: null,
+ clicked: false,
showCodesResetDialog: false,
tokens: []
}
},
methods: {
- getQRCode () {
- account.getQRCodeForTFASetup().then(resp => {
- this.qrCode = resp.data
+ getKey () {
+ account.getKeyForTFASetup().then(resp => {
+ this.key = resp.data.key
+ this.qrURL = resp.data.url
})
},
+ copyKey () {
+ navigator.clipboard.writeText(this.key)
+ this.clicked = true
+ },
startTFASetup () {
account.startTFASetup().then(resp => {
- this.getQRCode()
+ this.getKey()
})
},
async finalizeTFASetup () {
@@ -129,7 +157,8 @@ export default {
return
}
const resp = await account.finalizeTFASetup(this.pinCode)
- this.qrCode = null
+ this.key = null
+ this.qrURL = null
this.tokens = resp.data.tokens
Cookies.set('token', resp.data.access, { sameSite: 'strict' })
Cookies.set('refreshToken', resp.data.refresh, { sameSite: 'strict' })
@@ -153,8 +182,18 @@ export default {
},
created () {
if (!this.account.tfa_enabled) {
- this.getQRCode()
+ this.getKey()
}
}
}
+
+
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 5d405fa40..9515e1bee 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -6350,6 +6350,11 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+qrcode.vue@~1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/qrcode.vue/-/qrcode.vue-1.7.0.tgz#c54b2934f0650c10d92785d08aaad36c55e0fc56"
+ integrity sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==
+
qs@6.10.3:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py
index 24ba9adee..1732ef67d 100644
--- a/modoboa/core/api/v2/viewsets.py
+++ b/modoboa/core/api/v2/viewsets.py
@@ -1,10 +1,5 @@
"""Core API v2 viewsets."""
-import io
-
-import qrcode
-import qrcode.image.svg
-
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from drf_spectacular.utils import extend_schema
from rest_framework import filters, permissions, response, viewsets
@@ -102,19 +97,16 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
"access": str(refresh.access_token)
})
- @action(methods=["get"], detail=False, url_path="tfa/setup/qr_code")
- def tfa_setup_get_qr_code(self, request):
+ @action(methods=["get"], detail=False, url_path="tfa/setup/key")
+ def tfa_setup_get_key(self, request):
"""Get a QR code to finalize the setup process."""
if request.user.tfa_enabled:
return response.Response(status=404)
device = request.user.totpdevice_set.first()
if not device:
return response.Response(status=404)
- factory = qrcode.image.svg.SvgPathImage
- img = qrcode.make(device.config_url, image_factory=factory)
- buf = io.BytesIO()
- img.save(buf)
- return response.Response(buf.getvalue(), content_type="application/xml")
+ return response.Response({"key": device.key, "url":device.config_url},
+ content_type="application/json")
@extend_schema(
request=core_v1_serializers.CheckTFASetupSerializer