Unverified Commit c5a441c9 authored by NGPixel's avatar NGPixel

feat: login change password step

parent fe8066c8
...@@ -735,8 +735,7 @@ export async function up (knex) { ...@@ -735,8 +735,7 @@ export async function up (knex) {
auth: { auth: {
[authModuleId]: { [authModuleId]: {
password: await bcrypt.hash(process.env.ADMIN_PASS || '12345678', 12), 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, restrictLogin: false,
tfaIsActive: false, tfaIsActive: false,
tfaRequired: false, tfaRequired: false,
......
...@@ -283,42 +283,42 @@ export default { ...@@ -283,42 +283,42 @@ export default {
return generateError(err) return generateError(err)
} }
}, },
async changePassword (obj, args, context) { // async changePassword (obj, args, context) {
try { // try {
if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) { // if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
throw new WIKI.Error.AuthRequired() // throw new WIKI.Error.AuthRequired()
} // }
const usr = await WIKI.db.users.query().findById(context.req.user.id) // const usr = await WIKI.db.users.query().findById(context.req.user.id)
if (!usr.isActive) { // if (!usr.isActive) {
throw new WIKI.Error.AuthAccountBanned() // throw new WIKI.Error.AuthAccountBanned()
} // }
if (!usr.isVerified) { // if (!usr.isVerified) {
throw new WIKI.Error.AuthAccountNotVerified() // throw new WIKI.Error.AuthAccountNotVerified()
} // }
if (usr.providerKey !== 'local') { // if (usr.providerKey !== 'local') {
throw new WIKI.Error.AuthProviderInvalid() // throw new WIKI.Error.AuthProviderInvalid()
} // }
try { // try {
await usr.verifyPassword(args.current) // await usr.verifyPassword(args.current)
} catch (err) { // } catch (err) {
throw new WIKI.Error.AuthPasswordInvalid() // throw new WIKI.Error.AuthPasswordInvalid()
} // }
await WIKI.db.users.updateUser({ // await WIKI.db.users.updateUser({
id: usr.id, // id: usr.id,
newPassword: args.new // newPassword: args.new
}) // })
const newToken = await WIKI.db.users.refreshToken(usr) // const newToken = await WIKI.db.users.refreshToken(usr)
return { // return {
responseResult: generateSuccess('Password changed successfully'), // responseResult: generateSuccess('Password changed successfully'),
jwt: newToken.token // jwt: newToken.token
} // }
} catch (err) { // } catch (err) {
return generateError(err) // return generateError(err)
} // }
}, // },
/** /**
* UPLOAD USER AVATAR * UPLOAD USER AVATAR
*/ */
......
...@@ -1129,6 +1129,7 @@ ...@@ -1129,6 +1129,7 @@
"auth.changePwd.newPasswordVerify": "Verify New Password", "auth.changePwd.newPasswordVerify": "Verify New Password",
"auth.changePwd.proceed": "Change Password", "auth.changePwd.proceed": "Change Password",
"auth.changePwd.subtitle": "Choose a new password", "auth.changePwd.subtitle": "Choose a new password",
"auth.changePwd.success": "Password updated successfully.",
"auth.enterCredentials": "Enter your credentials", "auth.enterCredentials": "Enter your credentials",
"auth.errors.forgotPassword": "Missing or invalid email address.", "auth.errors.forgotPassword": "Missing or invalid email address.",
"auth.errors.invalidEmail": "Email is invalid.", "auth.errors.invalidEmail": "Email is invalid.",
...@@ -1198,6 +1199,7 @@ ...@@ -1198,6 +1199,7 @@
"auth.tfaSetupInstrSecond": "Enter the security code generated from your trusted device:", "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.tfaSetupTitle": "Your administrator has required Two-Factor Authentication (2FA) to be enabled on your account.",
"auth.tfaSetupVerifying": "Verifying...", "auth.tfaSetupVerifying": "Verifying...",
"auth.tfaSetupSuccess": "2FA enabled successfully on your account.",
"common.actions.activate": "Activate", "common.actions.activate": "Activate",
"common.actions.add": "Add", "common.actions.add": "Add",
"common.actions.apply": "Apply", "common.actions.apply": "Apply",
......
...@@ -66,14 +66,14 @@ export class UserKey extends Model { ...@@ -66,14 +66,14 @@ export class UserKey extends Model {
await WIKI.db.userKeys.query().deleteById(res.id) await WIKI.db.userKeys.query().deleteById(res.id)
} }
if (DateTime.utc() > DateTime.fromISO(res.validUntil)) { if (DateTime.utc() > DateTime.fromISO(res.validUntil)) {
throw new WIKI.Error.AuthValidationTokenInvalid() throw new Error('ERR_EXPIRED_VALIDATION_TOKEN')
} }
return { return {
...res.meta, ...res.meta,
user: res.user user: res.user
} }
} else { } else {
throw new WIKI.Error.AuthValidationTokenInvalid() throw new Error('ERR_INVALID_VALIDATION_TOKEN')
} }
} }
......
...@@ -325,7 +325,10 @@ export class User extends Model { ...@@ -325,7 +325,10 @@ export class User extends Model {
try { try {
const tfaToken = await WIKI.db.userKeys.generateToken({ const tfaToken = await WIKI.db.userKeys.generateToken({
kind: 'tfa', kind: 'tfa',
userId: user.id userId: user.id,
meta: {
strategyId
}
}) })
return { return {
nextAction: 'provideTfa', nextAction: 'provideTfa',
...@@ -341,7 +344,10 @@ export class User extends Model { ...@@ -341,7 +344,10 @@ export class User extends Model {
const tfaQRImage = await user.generateTFA(strategyId, siteId) const tfaQRImage = await user.generateTFA(strategyId, siteId)
const tfaToken = await WIKI.db.userKeys.generateToken({ const tfaToken = await WIKI.db.userKeys.generateToken({
kind: 'tfaSetup', kind: 'tfaSetup',
userId: user.id userId: user.id,
meta: {
strategyId
}
}) })
return { return {
nextAction: 'setupTfa', nextAction: 'setupTfa',
...@@ -361,7 +367,10 @@ export class User extends Model { ...@@ -361,7 +367,10 @@ export class User extends Model {
try { try {
const pwdChangeToken = await WIKI.db.userKeys.generateToken({ const pwdChangeToken = await WIKI.db.userKeys.generateToken({
kind: 'changePwd', kind: 'changePwd',
userId: user.id userId: user.id,
meta: {
strategyId
}
}) })
return { return {
...@@ -435,11 +444,16 @@ export class User extends Model { ...@@ -435,11 +444,16 @@ export class User extends Model {
*/ */
static async loginTFA ({ strategyId, siteId, securityCode, continuationToken, setup }, context) { static async loginTFA ({ strategyId, siteId, securityCode, continuationToken, setup }, context) {
if (securityCode.length === 6 && continuationToken.length > 1) { 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', kind: setup ? 'tfaSetup' : 'tfa',
token: continuationToken, token: continuationToken,
skipDelete: setup skipDelete: setup
}) })
if (strategyId !== expectedStrategyId) {
throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
}
if (user) { if (user) {
if (user.verifyTFA(strategyId, securityCode)) { if (user.verifyTFA(strategyId, securityCode)) {
if (setup) { if (setup) {
...@@ -447,40 +461,39 @@ export class User extends Model { ...@@ -447,40 +461,39 @@ export class User extends Model {
} }
return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true }) return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
} else { } 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 * Change Password from a Mandatory Password Change after Login
*/ */
static async loginChangePassword ({ continuationToken, newPassword }, context) { static async loginChangePassword ({ strategyId, siteId, continuationToken, newPassword }, context) {
if (!newPassword || newPassword.length < 6) { if (!newPassword || newPassword.length < 8) {
throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!') 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', kind: 'changePwd',
token: continuationToken token: continuationToken
}) })
if (usr) { if (strategyId !== expectedStrategyId) {
await WIKI.db.users.query().patch({ throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
password: newPassword, }
mustChangePwd: false
}).findById(usr.id)
return new Promise((resolve, reject) => { if (user) {
context.req.logIn(usr, { session: false }, async err => { user.auth[strategyId].password = await bcrypt.hash(newPassword, 12),
if (err) { return reject(err) } user.auth[strategyId].mustChangePwd = false
const jwtToken = await WIKI.db.users.refreshToken(usr) await user.$query().patch({
resolve({ jwt: jwtToken.token }) auth: user.auth
})
}) })
return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipChangePwd: true, skipTFA: true })
} else { } else {
throw new WIKI.Error.UserNotFound() throw new Error('ERR_INVALID_USER')
} }
} }
......
...@@ -21,18 +21,20 @@ export default { ...@@ -21,18 +21,20 @@ export default {
if (user) { if (user) {
const authStrategyData = user.auth[strategyId] const authStrategyData = user.auth[strategyId]
if (!authStrategyData) { if (!authStrategyData) {
throw new WIKI.Error.AuthLoginFailed() throw new Error('ERR_INVALID_STRATEGY_ID')
} else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) { } else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
throw new WIKI.Error.AuthLoginFailed() throw new Error('ERR_AUTH_FAILED')
} else if (!user.isActive) { } 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) { } else if (!user.isVerified) {
throw new WIKI.Error.AuthAccountNotVerified() throw new Error('ERR_USER_NOT_VERIFIED')
} else { } else {
done(null, user) done(null, user)
} }
} else { } else {
throw new WIKI.Error.AuthLoginFailed() throw new Error('ERR_AUTH_FAILED')
} }
} catch (err) { } catch (err) {
done(err, null) done(err, null)
......
...@@ -620,6 +620,11 @@ async function forgotPassword () { ...@@ -620,6 +620,11 @@ async function forgotPassword () {
if (!isFormValid) { if (!isFormValid) {
throw new Error(t('auth.errors.forgotPassword')) throw new Error(t('auth.errors.forgotPassword'))
} }
// TODO: Implement forgot password
$q.notify({
type: 'negative',
message: 'Not implemented yet.'
})
} catch (err) { } catch (err) {
$q.notify({ $q.notify({
type: 'negative', type: 'negative',
...@@ -695,17 +700,13 @@ async function changePwd () { ...@@ -695,17 +700,13 @@ async function changePwd () {
const resp = await APOLLO_CLIENT.mutate({ const resp = await APOLLO_CLIENT.mutate({
mutation: gql` mutation: gql`
mutation ( mutation (
$userId: UUID!
$continuationToken: String $continuationToken: String
$currentPassword: String
$newPassword: String! $newPassword: String!
$strategyId: UUID! $strategyId: UUID!
$siteId: UUID $siteId: UUID
) { ) {
changePassword ( changePassword (
userId: $userId
continuationToken: $continuationToken continuationToken: $continuationToken
currentPassword: $currentPassword
newPassword: $newPassword newPassword: $newPassword
strategyId: $strategyId strategyId: $strategyId
siteId: $siteId siteId: $siteId
...@@ -715,9 +716,7 @@ async function changePwd () { ...@@ -715,9 +716,7 @@ async function changePwd () {
message message
} }
jwt jwt
mustChangePwd nextAction
mustProvideTFA
mustSetupTFA
continuationToken continuationToken
redirect redirect
tfaQRImage tfaQRImage
...@@ -725,19 +724,21 @@ async function changePwd () { ...@@ -725,19 +724,21 @@ async function changePwd () {
} }
`, `,
variables: { variables: {
userId: userStore.id,
continuationToken: state.continuationToken, continuationToken: state.continuationToken,
currentPassword: state.password,
newPassword: state.newPassword, newPassword: state.newPassword,
strategyId: state.selectedStrategyId, strategyId: state.selectedStrategyId,
siteId: siteStore.id siteId: siteStore.id
} }
}) })
if (resp.data?.login?.operation?.succeeded) { if (resp.data?.changePassword?.operation?.succeeded) {
state.password = '' state.password = ''
await handleLoginResponse(resp.data.login) $q.notify({
type: 'positive',
message: t('auth.changePwd.success')
})
await handleLoginResponse(resp.data.changePassword)
} else { } 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) { } catch (err) {
$q.notify({ $q.notify({
...@@ -855,6 +856,10 @@ async function finishSetupTFA () { ...@@ -855,6 +856,10 @@ async function finishSetupTFA () {
if (resp.data?.loginTFA?.operation?.succeeded) { if (resp.data?.loginTFA?.operation?.succeeded) {
state.continuationToken = '' state.continuationToken = ''
state.securityCode = '' state.securityCode = ''
$q.notify({
type: 'positive',
message: t('auth.tfaSetupSuccess')
})
await handleLoginResponse(resp.data.loginTFA) await handleLoginResponse(resp.data.loginTFA)
} else { } else {
throw new Error(resp.data?.loginTFA?.operation?.message || t('auth.errors.loginError')) throw new Error(resp.data?.loginTFA?.operation?.message || t('auth.errors.loginError'))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment