Require user password to setup tfa

Fixes #183
This commit is contained in:
rijkvanzanten
2020-10-02 17:12:25 -04:00
parent 9098776964
commit f57b542c4d
5 changed files with 89 additions and 13 deletions

View File

@@ -1,4 +1,5 @@
import express from 'express';
import argon2 from 'argon2';
import asyncHandler from 'express-async-handler';
import Joi from 'joi';
import {
@@ -193,7 +194,15 @@ router.post(
throw new InvalidCredentialsException();
}
if (!req.body.password) {
throw new InvalidPayloadException(`"password" is required`);
}
const service = new UsersService({ accountability: req.accountability });
const authService = new AuthenticationService({ accountability: req.accountability });
await authService.verifyPassword(req.accountability.user, req.body.password);
const { url, secret } = await service.enableTFA(req.accountability.user);
res.locals.payload = { data: { secret, otpauth_url: url } };

View File

@@ -181,4 +181,22 @@ export class AuthenticationService {
const secret = user.tfa_secret;
return authenticator.check(otp, secret);
}
async verifyPassword(pk: string, password: string) {
const userRecord = await this.knex
.select('password')
.from('directus_users')
.where({ id: pk })
.first();
if (!userRecord || !userRecord.password) {
throw new InvalidCredentialsException();
}
if ((await argon2.verify(userRecord.password, password)) === false) {
throw new InvalidCredentialsException();
}
return true;
}
}

View File

@@ -57,7 +57,8 @@ export const onError = async (error: RequestError) => {
status === 401 &&
code === 'INVALID_CREDENTIALS' &&
error.request.responseURL.includes('refresh') === false &&
error.request.responseURL.includes('login') === false
error.request.responseURL.includes('login') === false &&
error.request.responseURL.includes('tfa') === false
) {
let newToken: string;

View File

@@ -10,8 +10,24 @@
<v-dialog persistent v-model="enableActive">
<v-card>
<v-progress-circular class="loader" indeterminate v-if="loading === true" />
<template v-show="loading === false">
<template v-if="tfaEnabled === false" v-show="loading === false">
<v-card-title>
{{ $t('enter_password_to_enable_tfa') }}
</v-card-title>
<v-card-text>
<v-input v-model="password" type="password" :placeholder="$t('password')" />
<v-error v-if="error" :error="error" />
</v-card-text>
<v-card-actions>
<v-button @click="enableActive = false" secondary>{{ $t('cancel') }}</v-button>
<v-button @click="enableTFA" :loading="loading">{{ $t('next') }}</v-button>
</v-card-actions>
</template>
<v-progress-circular class="loader" indeterminate v-else-if="loading === true" />
<div v-show="tfaEnabled === true && loading === false">
<v-card-title>
{{ $t('tfa_scan_code') }}
</v-card-title>
@@ -22,7 +38,7 @@
<v-card-actions>
<v-button @click="enableActive = false">{{ $t('done') }}</v-button>
</v-card-actions>
</template>
</div>
</v-card>
</v-dialog>
@@ -45,7 +61,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, watch } from '@vue/composition-api';
import { defineComponent, ref, watch, onMounted } from '@vue/composition-api';
import api from '@/api';
import qrcode from 'qrcode';
import { nanoid } from 'nanoid';
@@ -66,6 +82,11 @@ export default defineComponent({
const secret = ref<string>();
const otp = ref('');
const error = ref<any>();
const password = ref('');
onMounted(() => {
password.value = '';
});
watch(
() => props.value,
@@ -75,25 +96,46 @@ export default defineComponent({
{ immediate: true }
);
return { tfaEnabled, toggle, enableActive, disableActive, loading, canvasID, secret, disableTFA, otp, error };
return {
tfaEnabled,
enableTFA,
toggle,
password,
enableActive,
disableActive,
loading,
canvasID,
secret,
disableTFA,
otp,
error,
};
function toggle() {
if (tfaEnabled.value === false) {
enableActive.value = true;
enableTFA();
} else {
disableActive.value = true;
}
}
async function enableTFA() {
if (loading.value === true) return;
loading.value = true;
const response = await api.post('/users/me/tfa/enable');
const url = response.data.data.otpauth_url;
secret.value = response.data.data.secret;
await qrcode.toCanvas(document.getElementById(canvasID), url);
loading.value = false;
tfaEnabled.value = true;
try {
const response = await api.post('/users/me/tfa/enable', { password: password.value });
const url = response.data.data.otpauth_url;
secret.value = response.data.data.secret;
await qrcode.toCanvas(document.getElementById(canvasID), url);
tfaEnabled.value = true;
error.value = null;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
async function disableTFA() {
@@ -145,4 +187,8 @@ export default defineComponent({
--v-button-background-color: var(--warning);
--v-button-background-color-hover: var(--warning-125);
}
.v-error {
margin-top: 24px;
}
</style>

View File

@@ -10,6 +10,8 @@
"only_show_the_file_extension": "Only show the file extension",
"textarea": "Textarea",
"enter_password_to_enable_tfa": "Enter your password to enable Two-Factor Authentication",
"add_field": "Add Field",
"role_name": "Role Name",