Skip to content

Commit

Permalink
feat: login change password step
Browse files Browse the repository at this point in the history
  • Loading branch information
NGPixel committed Oct 1, 2023
1 parent fe8066c commit c5a441c
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 79 deletions.
3 changes: 1 addition & 2 deletions server/db/migrations/3.0.0.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
72 changes: 36 additions & 36 deletions server/graph/resolvers/user.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

await WIKI.db.users.updateUser({
id: usr.id,
newPassword: args.new
})

const newToken = await WIKI.db.users.refreshToken(usr)

return {
responseResult: generateSuccess('Password changed successfully'),
jwt: newToken.token
}
} catch (err) {
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()
// }

// await WIKI.db.users.updateUser({
// id: usr.id,
// newPassword: args.new
// })

// const newToken = await WIKI.db.users.refreshToken(usr)

// return {
// responseResult: generateSuccess('Password changed successfully'),
// jwt: newToken.token
// }
// } catch (err) {
// return generateError(err)
// }
// },
/**
* UPLOAD USER AVATAR
*/
Expand Down
2 changes: 2 additions & 0 deletions server/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions server/models/userKeys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
}

Expand Down
57 changes: 35 additions & 22 deletions server/models/users.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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 {
Expand Down Expand Up @@ -435,52 +444,56 @@ 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) {
await user.enableTFA(strategyId)
}
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')
}
}

Expand Down
12 changes: 7 additions & 5 deletions server/modules/authentication/local/authentication.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 17 additions & 12 deletions ux/src/components/AuthLoginPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -715,29 +716,29 @@ async function changePwd () {
message
}
jwt
mustChangePwd
mustProvideTFA
mustSetupTFA
nextAction
continuationToken
redirect
tfaQRImage
}
}
`,
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({
Expand Down Expand Up @@ -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'))
Expand Down

0 comments on commit c5a441c

Please sign in to comment.