From c5a441c9463d9f43563cd649e057493478f2fde3 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 1 Oct 2023 19:42:43 +0000 Subject: [PATCH] feat: login change password step --- server/db/migrations/3.0.0.mjs | 3 +- server/graph/resolvers/user.mjs | 66 +++++++++---------- server/locales/en.json | 2 + server/models/userKeys.mjs | 4 +- server/models/users.mjs | 57 +++++++++------- .../authentication/local/authentication.mjs | 12 ++-- ux/src/components/AuthLoginPanel.vue | 29 ++++---- 7 files changed, 97 insertions(+), 76 deletions(-) diff --git a/server/db/migrations/3.0.0.mjs b/server/db/migrations/3.0.0.mjs index 9886c145..bd2ad601 100644 --- a/server/db/migrations/3.0.0.mjs +++ b/server/db/migrations/3.0.0.mjs @@ -735,8 +735,7 @@ export async function up (knex) { auth: { [authModuleId]: { password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12), - mustChangePwd: false, // TODO: Revert to true (below) once change password flow is implemented - // mustChangePwd: !process.env.ADMIN_PASS, + mustChangePwd: !process.env.ADMIN_PASS, restrictLogin: false, tfaIsActive: false, tfaRequired: false, diff --git a/server/graph/resolvers/user.mjs b/server/graph/resolvers/user.mjs index 6982132f..d0db20ca 100644 --- a/server/graph/resolvers/user.mjs +++ b/server/graph/resolvers/user.mjs @@ -283,42 +283,42 @@ export default { return generateError(err) } }, - async changePassword (obj, args, context) { - try { - if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { - throw new WIKI.Error.AuthRequired() - } - const usr = await WIKI.db.users.query().findById(context.req.user.id) - if (!usr.isActive) { - throw new WIKI.Error.AuthAccountBanned() - } - if (!usr.isVerified) { - throw new WIKI.Error.AuthAccountNotVerified() - } - if (usr.providerKey !== 'local') { - throw new WIKI.Error.AuthProviderInvalid() - } - try { - await usr.verifyPassword(args.current) - } catch (err) { - throw new WIKI.Error.AuthPasswordInvalid() - } + // async changePassword (obj, args, context) { + // try { + // if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { + // throw new WIKI.Error.AuthRequired() + // } + // const usr = await WIKI.db.users.query().findById(context.req.user.id) + // if (!usr.isActive) { + // throw new WIKI.Error.AuthAccountBanned() + // } + // if (!usr.isVerified) { + // throw new WIKI.Error.AuthAccountNotVerified() + // } + // if (usr.providerKey !== 'local') { + // throw new WIKI.Error.AuthProviderInvalid() + // } + // try { + // await usr.verifyPassword(args.current) + // } catch (err) { + // throw new WIKI.Error.AuthPasswordInvalid() + // } - await WIKI.db.users.updateUser({ - id: usr.id, - newPassword: args.new - }) + // await WIKI.db.users.updateUser({ + // id: usr.id, + // newPassword: args.new + // }) - const newToken = await WIKI.db.users.refreshToken(usr) + // const newToken = await WIKI.db.users.refreshToken(usr) - return { - responseResult: generateSuccess('Password changed successfully'), - jwt: newToken.token - } - } catch (err) { - return generateError(err) - } - }, + // return { + // responseResult: generateSuccess('Password changed successfully'), + // jwt: newToken.token + // } + // } catch (err) { + // return generateError(err) + // } + // }, /** * UPLOAD USER AVATAR */ diff --git a/server/locales/en.json b/server/locales/en.json index f25da081..f42b754a 100644 --- a/server/locales/en.json +++ b/server/locales/en.json @@ -1129,6 +1129,7 @@ "auth.changePwd.newPasswordVerify": "Verify New Password", "auth.changePwd.proceed": "Change Password", "auth.changePwd.subtitle": "Choose a new password", + "auth.changePwd.success": "Password updated successfully.", "auth.enterCredentials": "Enter your credentials", "auth.errors.forgotPassword": "Missing or invalid email address.", "auth.errors.invalidEmail": "Email is invalid.", @@ -1198,6 +1199,7 @@ "auth.tfaSetupInstrSecond": "Enter the security code generated from your trusted device:", "auth.tfaSetupTitle": "Your administrator has required Two-Factor Authentication (2FA) to be enabled on your account.", "auth.tfaSetupVerifying": "Verifying...", + "auth.tfaSetupSuccess": "2FA enabled successfully on your account.", "common.actions.activate": "Activate", "common.actions.add": "Add", "common.actions.apply": "Apply", diff --git a/server/models/userKeys.mjs b/server/models/userKeys.mjs index f2dee07b..262e482f 100644 --- a/server/models/userKeys.mjs +++ b/server/models/userKeys.mjs @@ -66,14 +66,14 @@ export class UserKey extends Model { await WIKI.db.userKeys.query().deleteById(res.id) } if (DateTime.utc() > DateTime.fromISO(res.validUntil)) { - throw new WIKI.Error.AuthValidationTokenInvalid() + throw new Error('ERR_EXPIRED_VALIDATION_TOKEN') } return { ...res.meta, user: res.user } } else { - throw new WIKI.Error.AuthValidationTokenInvalid() + throw new Error('ERR_INVALID_VALIDATION_TOKEN') } } diff --git a/server/models/users.mjs b/server/models/users.mjs index d889c3d8..38d70c88 100644 --- a/server/models/users.mjs +++ b/server/models/users.mjs @@ -325,7 +325,10 @@ export class User extends Model { try { const tfaToken = await WIKI.db.userKeys.generateToken({ kind: 'tfa', - userId: user.id + userId: user.id, + meta: { + strategyId + } }) return { nextAction: 'provideTfa', @@ -341,7 +344,10 @@ export class User extends Model { const tfaQRImage = await user.generateTFA(strategyId, siteId) const tfaToken = await WIKI.db.userKeys.generateToken({ kind: 'tfaSetup', - userId: user.id + userId: user.id, + meta: { + strategyId + } }) return { nextAction: 'setupTfa', @@ -361,7 +367,10 @@ export class User extends Model { try { const pwdChangeToken = await WIKI.db.userKeys.generateToken({ kind: 'changePwd', - userId: user.id + userId: user.id, + meta: { + strategyId + } }) return { @@ -435,11 +444,16 @@ export class User extends Model { */ static async loginTFA ({ strategyId, siteId, securityCode, continuationToken, setup }, context) { if (securityCode.length === 6 && continuationToken.length > 1) { - const { user } = await WIKI.db.userKeys.validateToken({ + const { user, strategyId: expectedStrategyId } = await WIKI.db.userKeys.validateToken({ kind: setup ? 'tfaSetup' : 'tfa', token: continuationToken, skipDelete: setup }) + + if (strategyId !== expectedStrategyId) { + throw new Error('ERR_UNEXPECTED_STRATEGY_ID') + } + if (user) { if (user.verifyTFA(strategyId, securityCode)) { if (setup) { @@ -447,40 +461,39 @@ export class User extends Model { } return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true }) } else { - throw new WIKI.Error.AuthTFAFailed() + throw new Error('ERR_INCORRECT_TFA_TOKEN') } } } - throw new WIKI.Error.AuthTFAInvalid() + throw new Error('ERR_INVALID_TFA_REQUEST') } /** * Change Password from a Mandatory Password Change after Login */ - static async loginChangePassword ({ continuationToken, newPassword }, context) { - if (!newPassword || newPassword.length < 6) { - throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!') + static async loginChangePassword ({ strategyId, siteId, continuationToken, newPassword }, context) { + if (!newPassword || newPassword.length < 8) { + throw new Error('ERR_PASSWORD_TOO_SHORT') } - const usr = await WIKI.db.userKeys.validateToken({ + const { user, strategyId: expectedStrategyId } = await WIKI.db.userKeys.validateToken({ kind: 'changePwd', token: continuationToken }) - if (usr) { - await WIKI.db.users.query().patch({ - password: newPassword, - mustChangePwd: false - }).findById(usr.id) + if (strategyId !== expectedStrategyId) { + throw new Error('ERR_UNEXPECTED_STRATEGY_ID') + } - return new Promise((resolve, reject) => { - context.req.logIn(usr, { session: false }, async err => { - if (err) { return reject(err) } - const jwtToken = await WIKI.db.users.refreshToken(usr) - resolve({ jwt: jwtToken.token }) - }) + if (user) { + user.auth[strategyId].password = await bcrypt.hash(newPassword, 12), + user.auth[strategyId].mustChangePwd = false + await user.$query().patch({ + auth: user.auth }) + + return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipChangePwd: true, skipTFA: true }) } else { - throw new WIKI.Error.UserNotFound() + throw new Error('ERR_INVALID_USER') } } diff --git a/server/modules/authentication/local/authentication.mjs b/server/modules/authentication/local/authentication.mjs index 9394b648..3e41d8f0 100644 --- a/server/modules/authentication/local/authentication.mjs +++ b/server/modules/authentication/local/authentication.mjs @@ -21,18 +21,20 @@ export default { if (user) { const authStrategyData = user.auth[strategyId] if (!authStrategyData) { - throw new WIKI.Error.AuthLoginFailed() + throw new Error('ERR_INVALID_STRATEGY_ID') } else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) { - throw new WIKI.Error.AuthLoginFailed() + throw new Error('ERR_AUTH_FAILED') } else if (!user.isActive) { - throw new WIKI.Error.AuthAccountBanned() + throw new Error('ERR_INACTIVE_USER') + } else if (authStrategyData.restrictLogin) { + throw new Error('ERR_LOGIN_RESTRICTED') } else if (!user.isVerified) { - throw new WIKI.Error.AuthAccountNotVerified() + throw new Error('ERR_USER_NOT_VERIFIED') } else { done(null, user) } } else { - throw new WIKI.Error.AuthLoginFailed() + throw new Error('ERR_AUTH_FAILED') } } catch (err) { done(err, null) diff --git a/ux/src/components/AuthLoginPanel.vue b/ux/src/components/AuthLoginPanel.vue index 64a455f8..691a1b04 100644 --- a/ux/src/components/AuthLoginPanel.vue +++ b/ux/src/components/AuthLoginPanel.vue @@ -620,6 +620,11 @@ async function forgotPassword () { if (!isFormValid) { throw new Error(t('auth.errors.forgotPassword')) } + // TODO: Implement forgot password + $q.notify({ + type: 'negative', + message: 'Not implemented yet.' + }) } catch (err) { $q.notify({ type: 'negative', @@ -695,17 +700,13 @@ async function changePwd () { const resp = await APOLLO_CLIENT.mutate({ mutation: gql` mutation ( - $userId: UUID! $continuationToken: String - $currentPassword: String $newPassword: String! $strategyId: UUID! $siteId: UUID ) { changePassword ( - userId: $userId continuationToken: $continuationToken - currentPassword: $currentPassword newPassword: $newPassword strategyId: $strategyId siteId: $siteId @@ -715,9 +716,7 @@ async function changePwd () { message } jwt - mustChangePwd - mustProvideTFA - mustSetupTFA + nextAction continuationToken redirect tfaQRImage @@ -725,19 +724,21 @@ async function changePwd () { } `, variables: { - userId: userStore.id, continuationToken: state.continuationToken, - currentPassword: state.password, newPassword: state.newPassword, strategyId: state.selectedStrategyId, siteId: siteStore.id } }) - if (resp.data?.login?.operation?.succeeded) { + if (resp.data?.changePassword?.operation?.succeeded) { state.password = '' - await handleLoginResponse(resp.data.login) + $q.notify({ + type: 'positive', + message: t('auth.changePwd.success') + }) + await handleLoginResponse(resp.data.changePassword) } else { - throw new Error(resp.data?.login?.operation?.message || t('auth.errors.loginError')) + throw new Error(resp.data?.changePassword?.operation?.message || t('auth.errors.loginError')) } } catch (err) { $q.notify({ @@ -855,6 +856,10 @@ async function finishSetupTFA () { if (resp.data?.loginTFA?.operation?.succeeded) { state.continuationToken = '' state.securityCode = '' + $q.notify({ + type: 'positive', + message: t('auth.tfaSetupSuccess') + }) await handleLoginResponse(resp.data.loginTFA) } else { throw new Error(resp.data?.loginTFA?.operation?.message || t('auth.errors.loginError'))