Allow to enable only webauthn

This commit is contained in:
Spitfireap
2024-06-22 19:45:58 +02:00
parent 0d8e0690c5
commit b1bf3faf07
12 changed files with 261 additions and 164 deletions
+1 -1
View File
@@ -24,5 +24,5 @@ export default {
},
deleteFido(id) {
return repository.delete(`/fido/${id}/`)
}
},
}
@@ -0,0 +1,116 @@
<template>
<div>
<v-card flat>
<v-card-title>
<span class="text-h6">
{{ $gettext('Backup codes') }}
</span>
</v-card-title>
<v-card-text>
<template v-if="tfa_enabled">
<v-alert type="info" class="mb-2">
{{
$gettext(
"You already have generated backup codes for this account. If you don't remember them or have used them all, you generate new ones."
)
}}
</v-alert>
<v-form ref="editTFAForm">
<label class="m-label">{{ $gettext('Password') }} </label>
<v-text-field
v-model="password"
autocomplete="new-password"
type="password"
variant="outlined"
:rules="[rules.required]"
:error-messages="passwordError"
/>
<div class="mt-5">
<v-btn
:loading="loadingReset"
color="success"
@click="resetRecoveryCodes"
>
{{ $gettext('Reset recovery codes') }}
</v-btn>
</div>
</v-form>
</template>
<template v-else>
<div tag="p" class="my-4">
{{
$gettext(
'No two-factor authentificator is yet set for your account.'
)
}}
</div>
</template>
<v-dialog v-model="showCodesResetDialog" max-width="800px" persistent>
<RecoveryCodesResetDialog
:tokens="newTokens"
@close="closeRecoveryCodesResetDialog"
/>
</v-dialog>
</v-card-text>
</v-card>
</div>
</template>
<script setup lang="js">
import accountApi from '@/api/account'
import RecoveryCodesResetDialog from './RecoveryCodesResetDialog'
import { useGettext } from 'vue3-gettext'
import { useBusStore, useAuthStore } from '@/stores'
import rules from '@/plugins/rules'
import { ref, computed } from 'vue'
const { $gettext } = useGettext()
const busStore = useBusStore()
const authStore = useAuthStore()
const tfa_enabled = computed(() => authStore.authUser.tfa_enabled)
const loadingReset = ref(false)
const editTFAForm = ref()
const password = ref('')
const newTokens = ref([])
const passwordError = ref()
const showCodesResetDialog = ref(false)
async function resetRecoveryCodes() {
const isValid = await editTFAForm.value.validate()
if (!isValid) {
return
}
const payload = {
password: password.value,
}
loadingReset.value = true
accountApi
.resetRecoveryCodes(payload)
.then((response) => {
newTokens.value = response.data.tokens
showCodesResetDialog.value = true
})
.catch((error) => {
if (error.response.status === 400) {
passwordError.value = error.response.data.password
} else {
busStore.displayNotification({
msg: $gettext(error.response.data),
type: 'error',
})
}
})
.finally(() => {
loadingReset.value = false
editTFAForm.value.reset()
})
}
function closeRecoveryCodesResetDialog() {
newTokens.value = []
showCodesResetDialog.value = false
}
</script>
@@ -16,33 +16,8 @@
)
}}
</div>
<template v-if="browserCapable">
<v-form ref="fidoForm" @submit.prevent="startFidoRegistration">
<v-text-field
v-model="name"
:label="$gettext('New device name')"
:rules="[rules.required]"
/>
<v-btn
color="success"
type="submit"
:loading="registrationLoading"
>
{{ $gettext('Setup device') }}
</v-btn>
</v-form>
</template>
<template v-else>
<v-alert type="error">
{{
$gettext(
'Your browser does not seem compatible with WebAuthN'
)
}}
</v-alert>
</template>
</v-col>
<v-col v-if="fidoCreds.length" cols="8">
<v-col v-else cols="8">
<h3>{{ $gettext('Registered Webauthn devices') }}</h3>
<v-data-table-virtual
:headers="headers"
@@ -74,6 +49,38 @@
</template>
</v-data-table-virtual>
</v-col>
<v-col cols="4">
<template v-if="browserCapable">
<h3>
{{
$gettext('Enter a name to register a new webauthn device.')
}}
</h3>
<v-form ref="fidoForm" @submit.prevent="startFidoRegistration">
<v-text-field
v-model="name"
:label="$gettext('New device name')"
:rules="[rules.required]"
/>
<v-btn
color="success"
type="submit"
:loading="registrationLoading"
>
{{ $gettext('Setup device') }}
</v-btn>
</v-form>
</template>
<template v-else>
<v-alert type="error">
{{
$gettext(
'Your browser does not seem compatible with WebAuthN'
)
}}
</v-alert>
</template>
</v-col>
</v-row>
</v-card-text>
</v-card>
@@ -95,7 +102,7 @@
</ConfirmDialog>
<v-dialog v-model="showBackupTokenDialog" max-width="800px" persistent>
<RecoveryCodesResetDialog
:tokens="newTokens"
:tokens="tokens"
@close="closeRecoveryCodesResetDialog"
/>
</v-dialog>
@@ -153,16 +160,21 @@ async function startFidoRegistration() {
const creationOption = await authApi.beginFidoRegistration()
if (creationOption) {
const options = parseCreationOptionsFromJSON(creationOption.data)
const device = await create(options)
const result = device.toJSON()
result.name = name.value
authStore.addFidoCred(result).then((res) => {
if (res.status === 200) {
tokens.value = res.data.tokens
showBackupTokenDialog.value = true
}
})
fidoForm.value.reset()
try {
const device = await create(options)
const result = device.toJSON()
result.name = name.value
authStore.addFidoCred(result).then((res) => {
if (res.status === 200) {
tokens.value = res.data.tokens
showBackupTokenDialog.value = true
}
})
fidoForm.value.reset()
} catch {
registrationLoading.value = false
return
}
}
registrationLoading.value = false
}
@@ -47,7 +47,7 @@
</v-col>
</v-row>
</template>
<template v-else-if="tokens.length">
<template v-else-if="successSetup">
<v-alert type="success">
{{
$gettext(
@@ -55,22 +55,24 @@
)
}}
</v-alert>
<p class="mt-4">
{{
$gettext(
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
)
}}
</p>
<v-table density="compact">
<tbody>
<tr v-for="token in tokens" :key="token">
<td>{{ token }}</td>
</tr>
</tbody>
</v-table>
<template v-if="tokens.length">
<p class="mt-4">
{{
$gettext(
"The following recovery codes can be used one time each to let you regain access to your account, in case you lose your phone for example. Make sure to save them in a safe place, otherwise you won't be able to access your account anymore."
)
}}
</p>
<v-table density="compact">
<tbody>
<tr v-for="token in tokens" :key="token">
<td>{{ token }}</td>
</tr>
</tbody>
</v-table>
</template>
</template>
<template v-else-if="tfa_enabled">
<template v-else-if="totp_enabled">
<v-alert type="info" class="mb-2">
{{
$gettext(
@@ -97,9 +99,6 @@
>
{{ $gettext('Disable 2FA') }}
</v-btn>
<v-btn :loading="loadingReset" @click="resetRecoveryCodes">
{{ $gettext('Reset recovery codes') }}
</v-btn>
</div>
</v-form>
</template>
@@ -117,18 +116,11 @@
</template>
</v-card-text>
</v-card>
<v-dialog v-model="showCodesResetDialog" max-width="800px" persistent>
<RecoveryCodesResetDialog
:tokens="newTokens"
@close="closeRecoveryCodesResetDialog"
/>
</v-dialog>
</div>
</template>
<script setup lang="js">
import accountApi from '@/api/account'
import RecoveryCodesResetDialog from './RecoveryCodesResetDialog'
import QrcodeVue from 'qrcode.vue'
import { useBusStore, useAuthStore } from '@/stores'
import { ref, computed } from 'vue'
@@ -140,21 +132,18 @@ const busStore = useBusStore()
const authStore = useAuthStore()
const editTFAForm = ref()
const newTokens = ref([])
const pinCode = ref('')
const pinCodeErrors = ref([])
const key = ref(null)
const qrURL = ref(null)
const clicked = ref(false)
const showCodesResetDialog = ref(false)
const loadingDisable = ref(false)
const loadingReset = ref(false)
const password = ref('')
const passwordError = ref([])
const tokens = ref([])
const successSetup = ref(false)
const tfa_enabled = computed(() => authStore.authUser.tfa_enabled)
const totp_enabled = computed(() => authStore.authUser.totp_enabled)
function getKey() {
accountApi.getKeyForTFASetup().then((resp) => {
@@ -180,7 +169,10 @@ function finalizeTFASetup() {
.then((response) => {
key.value = null
qrURL.value = null
tokens.value = response.data.tokens
if (response.status === 200) {
tokens.value = response.data.tokens
}
successSetup.value = true
})
.catch((error) => {
if (error.response.status === 400) {
@@ -218,46 +210,6 @@ async function disableTFA() {
editTFAForm.value.reset()
})
}
async function resetRecoveryCodes() {
const isValid = await editTFAForm.value.validate()
if (!isValid) {
return
}
const payload = {
password: password.value,
}
loadingReset.value = true
accountApi
.resetRecoveryCodes(payload)
.then((response) => {
newTokens.value = response.data.tokens
showCodesResetDialog.value = true
})
.catch((error) => {
if (error.response.status === 400) {
passwordError.value = error.response.data.password
} else {
busStore.displayNotification({
msg: $gettext(error.response.data),
type: 'error',
})
}
})
.finally(() => {
loadingReset.value = false
editTFAForm.value.reset()
})
}
function closeRecoveryCodesResetDialog() {
newTokens.value = []
showCodesResetDialog.value = false
}
if (!tfa_enabled.value) {
getKey()
}
</script>
<style>
@@ -36,10 +36,10 @@ const { $gettext } = useGettext()
const props = defineProps({
// the callback should return true or false, returning false inhibits the close event
callback_agree : {
callback_agree: {
type: Function,
default: null
}
default: null,
},
})
const dialog = ref(false)
@@ -70,7 +70,7 @@ defineExpose({
})
async function agree() {
if (props.callback_agree !== null && ! (await props.callback_agree())) {
if (props.callback_agree !== null && !(await props.callback_agree())) {
return
}
storedResolve(true)
+12 -2
View File
@@ -46,18 +46,28 @@ export const useAuthStore = defineStore('auth', () => {
async function getFidoCreds() {
return authApi.getAllFidoRegistred().then((resp) => {
fidoCreds.value = resp.data
if (fidoCreds.value.length > 0) {
authUser.value.tfa_enabled = true
authUser.value.webauthn_enabled = true
}
return resp
})
}
async function addFidoCred(result) {
return authApi.endFidoRegistration(result).then(() => {
return authApi.endFidoRegistration(result).then((res) => {
getFidoCreds()
return res
})
}
async function deleteFidoCreds(id) {
return authApi.deleteFido(id).then(() => {
return authApi.deleteFido(id).then((res) => {
fidoCreds.value = fidoCreds.value.filter((cred) => cred.id !== id)
authUser.value.tfa_enabled = res.data.tfa_enabled
if (!res.data.tfa_enabled) {
authUser.value.webauthn_enabled = false
}
})
}
+4 -2
View File
@@ -25,7 +25,8 @@
<ProfileForm />
</v-window-item>
<v-window-item value="security">
<TwoFactorAuthForm />
<TotpAuthForm />
<BackupCodeAuthForm />
<FidoAuthForm />
</v-window-item>
<v-window-item v-if="authStore.userHasMailbox" value="forward">
@@ -47,7 +48,8 @@
<script setup>
import { ref } from 'vue'
import ProfileForm from '@/components/account/ProfileForm.vue'
import TwoFactorAuthForm from '@/components/account/TwoFactorAuthForm.vue'
import TotpAuthForm from '@/components/account/TotpAuthForm.vue'
import BackupCodeAuthForm from '@/components/account/BackupCodeAuthForm.vue'
import ForwardForm from '@/components/account/ForwardForm.vue'
import AutoReplyForm from '@/components/account/AutoReplyForm.vue'
import APISetupForm from '@/components/account/APISetupForm.vue'