Feat: 2fa (#1783)
* preplan otp, better qrcode library * add 2fa as feature * add totp generation * working totp lifecycle * don't allow disabled user to log in not a security issue as permission handler would fail anyway * require 2fa on login if enabled * update packages * fix typo * remove console.logs
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { Secret, TOTP } from 'otpauth';
|
||||
import { UserUpdateTotpSchema } from '#db/repositories/user/types';
|
||||
|
||||
type Response =
|
||||
| {
|
||||
success: boolean;
|
||||
type: 'setup';
|
||||
key: string;
|
||||
uri: string;
|
||||
}
|
||||
| { success: boolean; type: 'created' }
|
||||
| { success: boolean; type: 'deleted' };
|
||||
|
||||
export default definePermissionEventHandler(
|
||||
'me',
|
||||
'update',
|
||||
async ({ event, user, checkPermissions }) => {
|
||||
const body = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserUpdateTotpSchema, event)
|
||||
);
|
||||
|
||||
checkPermissions(user);
|
||||
|
||||
if (body.type === 'setup') {
|
||||
const key = new Secret({ size: 20 });
|
||||
|
||||
const totp = new TOTP({
|
||||
issuer: 'wg-easy',
|
||||
label: user.username,
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret: key,
|
||||
});
|
||||
|
||||
await Database.users.updateTotpKey(user.id, key.base32);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'setup',
|
||||
key: key.base32,
|
||||
uri: totp.toString(),
|
||||
} as Response;
|
||||
} else if (body.type === 'create') {
|
||||
await Database.users.verifyTotp(user.id, body.code);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'created',
|
||||
} as Response;
|
||||
} else if (body.type === 'delete') {
|
||||
await Database.users.deleteTotpKey(user.id, body.currentPassword);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
type: 'deleted',
|
||||
} as Response;
|
||||
}
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Invalid request',
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -20,5 +20,6 @@ export default defineEventHandler(async (event) => {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
totpVerified: user.totpVerified,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
import { UserLoginSchema } from '#db/repositories/user/types';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { username, password, remember } = await readValidatedBody(
|
||||
const { username, password, remember, totpCode } = await readValidatedBody(
|
||||
event,
|
||||
validateZod(UserLoginSchema, event)
|
||||
);
|
||||
|
||||
// TODO: timing can be used to enumerate usernames
|
||||
const result = await Database.users.login(username, password, totpCode);
|
||||
|
||||
const user = await Database.users.getByUsername(username);
|
||||
if (!user)
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Incorrect credentials',
|
||||
});
|
||||
// TODO: add localization support
|
||||
|
||||
const userHashPassword = user.password;
|
||||
const passwordValid = await isPasswordValid(password, userHashPassword);
|
||||
if (!passwordValid) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Incorrect credentials',
|
||||
});
|
||||
if (!result.success) {
|
||||
switch (result.error) {
|
||||
case 'INCORRECT_CREDENTIALS':
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Invalid username or password',
|
||||
});
|
||||
case 'TOTP_REQUIRED':
|
||||
return { status: 'TOTP_REQUIRED' };
|
||||
case 'INVALID_TOTP_CODE':
|
||||
return { status: 'INVALID_TOTP_CODE' };
|
||||
case 'USER_DISABLED':
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'User disabled',
|
||||
});
|
||||
case 'UNEXPECTED_ERROR':
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Unexpected error',
|
||||
});
|
||||
}
|
||||
assertUnreachable(result.error);
|
||||
}
|
||||
|
||||
const user = result.user;
|
||||
|
||||
const session = await useWGSession(event, remember);
|
||||
|
||||
const data = await session.update({
|
||||
@@ -34,5 +47,5 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
SERVER_DEBUG(`New Session: ${data.id} for ${user.id} (${user.username})`);
|
||||
|
||||
return { success: true };
|
||||
return { status: 'success' };
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user