diff --git a/server/app/data.yml b/server/app/data.yml
index 535ed24f..4d0ef92b 100644
--- a/server/app/data.yml
+++ b/server/app/data.yml
@@ -133,6 +133,10 @@ editors:
wysiwyg:
contentType: html
config: {}
+systemIds:
+ localAuthId: '5a528c4c-0a82-4ad2-96a5-2b23811e6588'
+ guestsGroupId: '10000000-0000-4000-8000-000000000001'
+ usersGroupId: '20000000-0000-4000-8000-000000000002'
groups:
defaultPermissions:
- 'read:pages'
diff --git a/server/core/auth.mjs b/server/core/auth.mjs
index a5dac737..d889d21f 100644
--- a/server/core/auth.mjs
+++ b/server/core/auth.mjs
@@ -79,13 +79,9 @@ export default {
for (const stg of enabledStrategies) {
try {
const strategy = (await import(`../modules/authentication/${stg.module}/authentication.mjs`)).default
+ strategy.init(passport, stg.id, stg.config)
- stg.config.callbackURL = `${WIKI.config.host}/login/${stg.id}/callback`
- stg.config.key = stg.id
- strategy.init(passport, stg.config)
- strategy.config = stg.config
-
- WIKI.auth.strategies[stg.key] = {
+ WIKI.auth.strategies[stg.id] = {
...strategy,
...stg
}
diff --git a/server/db/migrations/3.0.0.mjs b/server/db/migrations/3.0.0.mjs
index 406f8514..9886c145 100644
--- a/server/db/migrations/3.0.0.mjs
+++ b/server/db/migrations/3.0.0.mjs
@@ -66,8 +66,8 @@ export async function up (knex) {
table.boolean('isEnabled').notNullable().defaultTo(false)
table.string('displayName').notNullable().defaultTo('')
table.jsonb('config').notNullable().defaultTo('{}')
- table.boolean('selfRegistration').notNullable().defaultTo(false)
- table.string('allowedEmailRegex')
+ table.boolean('registration').notNullable().defaultTo(false)
+ table.string('allowedEmailRegex').notNullable().defaultTo('')
table.specificType('autoEnrollGroups', 'uuid[]')
})
.createTable('blocks', table => {
@@ -430,10 +430,10 @@ export async function up (knex) {
// -> GENERATE IDS
const groupAdminId = uuid()
- const groupUserId = uuid()
- const groupGuestId = '10000000-0000-4000-8000-000000000001'
+ const groupUserId = WIKI.data.systemIds.usersGroupId
+ const groupGuestId = WIKI.data.systemIds.guestsGroupId
const siteId = uuid()
- const authModuleId = uuid()
+ const authModuleId = WIKI.data.systemIds.localAuthId
const userAdminId = uuid()
const userGuestId = uuid()
@@ -719,7 +719,11 @@ export async function up (knex) {
id: authModuleId,
module: 'local',
isEnabled: true,
- displayName: 'Local Authentication'
+ displayName: 'Local Authentication',
+ config: JSON.stringify({
+ emailValidation: true,
+ enforceTfa: false
+ })
})
// -> USERS
@@ -734,6 +738,7 @@ export async function up (knex) {
mustChangePwd: false, // TODO: Revert to true (below) once change password flow is implemented
// mustChangePwd: !process.env.ADMIN_PASS,
restrictLogin: false,
+ tfaIsActive: false,
tfaRequired: false,
tfaSecret: ''
}
diff --git a/server/graph/resolvers/authentication.mjs b/server/graph/resolvers/authentication.mjs
index 82d1c4c4..90cf3c27 100644
--- a/server/graph/resolvers/authentication.mjs
+++ b/server/graph/resolvers/authentication.mjs
@@ -46,7 +46,7 @@ export default {
return {
...a,
config: _.transform(str.props, (r, v, k) => {
- r[k] = v.sensitive ? a.config[k] : '********'
+ r[k] = v.sensitive ? '********' : a.config[k]
}, {})
}
})
@@ -102,7 +102,7 @@ export default {
if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) {
WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err)
}
- console.error(err)
+ WIKI.logger.debug(err)
return generateError(err)
}
@@ -115,9 +115,10 @@ export default {
const authResult = await WIKI.db.users.loginTFA(args, context)
return {
...authResult,
- responseResult: generateSuccess('TFA success')
+ operation: generateSuccess('TFA success')
}
} catch (err) {
+ WIKI.logger.debug(err)
return generateError(err)
}
},
@@ -129,9 +130,10 @@ export default {
const authResult = await WIKI.db.users.loginChangePassword(args, context)
return {
...authResult,
- responseResult: generateSuccess('Password changed successfully')
+ operation: generateSuccess('Password changed successfully')
}
} catch (err) {
+ WIKI.logger.debug(err)
return generateError(err)
}
},
@@ -142,7 +144,7 @@ export default {
try {
await WIKI.db.users.loginForgotPassword(args, context)
return {
- responseResult: generateSuccess('Password reset request processed.')
+ operation: generateSuccess('Password reset request processed.')
}
} catch (err) {
return generateError(err)
@@ -153,9 +155,11 @@ export default {
*/
async register (obj, args, context) {
try {
- await WIKI.db.users.register({ ...args, verify: true }, context)
+ const usr = await WIKI.db.users.createNewUser({ ...args, userInitiated: true })
+ const authResult = await WIKI.db.users.afterLoginChecks(usr, WIKI.data.systemIds.localAuthId, context)
return {
- responseResult: generateSuccess('Registration success')
+ ...authResult,
+ operation: generateSuccess('Registration success')
}
} catch (err) {
return generateError(err)
diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql
index 43049e71..79ae2c9d 100644
--- a/server/graph/schemas/authentication.graphql
+++ b/server/graph/schemas/authentication.graphql
@@ -30,14 +30,16 @@ extend type Mutation {
username: String!
password: String!
strategyId: UUID!
- siteId: UUID
- ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
+ siteId: UUID!
+ ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
loginTFA(
continuationToken: String!
securityCode: String!
+ strategyId: UUID!
+ siteId: UUID!
setup: Boolean
- ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
+ ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
changePassword(
userId: UUID
@@ -46,7 +48,7 @@ extend type Mutation {
newPassword: String!
strategyId: UUID!
siteId: UUID
- ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
+ ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
forgotPassword(
email: String!
@@ -56,7 +58,7 @@ extend type Mutation {
email: String!
password: String!
name: String!
- ): AuthenticationRegisterResponse
+ ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
refreshToken(
token: String!
@@ -105,7 +107,7 @@ type AuthenticationActiveStrategy {
displayName: String
isEnabled: Boolean
config: JSON
- selfRegistration: Boolean
+ registration: Boolean
allowedEmailRegex: String
autoEnrollGroups: [UUID]
}
@@ -116,22 +118,15 @@ type AuthenticationSiteStrategy {
isVisible: Boolean
}
-type AuthenticationLoginResponse {
+type AuthenticationAuthResponse {
operation: Operation
jwt: String
- mustChangePwd: Boolean
- mustProvideTFA: Boolean
- mustSetupTFA: Boolean
+ nextAction: AuthenticationNextAction
continuationToken: String
redirect: String
tfaQRImage: String
}
-type AuthenticationRegisterResponse {
- operation: Operation
- jwt: String
-}
-
type AuthenticationTokenResponse {
operation: Operation
jwt: String
@@ -140,11 +135,11 @@ type AuthenticationTokenResponse {
input AuthenticationStrategyInput {
key: String!
strategyKey: String!
- config: [KeyValuePairInput]
+ config: JSON!
displayName: String!
order: Int!
isEnabled: Boolean!
- selfRegistration: Boolean!
+ registration: Boolean!
allowedEmailRegex: String!
autoEnrollGroups: [UUID]!
}
@@ -163,3 +158,10 @@ type AuthenticationCreateApiKeyResponse {
operation: Operation
key: String
}
+
+enum AuthenticationNextAction {
+ changePassword
+ setupTfa
+ provideTfa
+ redirect
+}
diff --git a/server/locales/en.json b/server/locales/en.json
index c5800a5c..f25da081 100644
--- a/server/locales/en.json
+++ b/server/locales/en.json
@@ -76,11 +76,13 @@
"admin.auth.configReferenceSubtitle": "Some strategies may require some configuration values to be set on your provider. These are provided for reference only and may not be needed by the current strategy.",
"admin.auth.displayName": "Display Name",
"admin.auth.displayNameHint": "The title shown to the end user for this authentication strategy.",
+ "admin.auth.emailValidation": "Email Validation",
+ "admin.auth.emailValidationHint": "Send a verification email to the user with a validation link when registering.",
"admin.auth.enabled": "Enabled",
"admin.auth.enabledForced": "This strategy cannot be disabled.",
"admin.auth.enabledHint": "Should this strategy be available to sites for login.",
- "admin.auth.force2fa": "Force all users to use Two-Factor Authentication (2FA)",
- "admin.auth.force2faHint": "Users will be required to setup 2FA the first time they login and cannot be disabled by the user.",
+ "admin.auth.enforceTfa": "Enforce Two-Factor Authentication",
+ "admin.auth.enforceTfaHint": "Users will be required to setup 2FA the first time they login and cannot be disabled by the user.",
"admin.auth.globalAdvSettings": "Global Advanced Settings",
"admin.auth.info": "Info",
"admin.auth.infoName": "Name",
@@ -90,10 +92,10 @@
"admin.auth.noConfigOption": "This strategy has no configuration options you can modify.",
"admin.auth.refreshSuccess": "List of strategies has been refreshed.",
"admin.auth.registration": "Registration",
+ "admin.auth.registrationHint": "Allow any user successfully authorized by the strategy to access the wiki.",
+ "admin.auth.registrationLocalHint": "Whether to allow guests to register new accounts.",
"admin.auth.saveSuccess": "Authentication configuration saved successfully.",
"admin.auth.security": "Security",
- "admin.auth.selfRegistration": "Allow Self-Registration",
- "admin.auth.selfRegistrationHint": "Allow any user successfully authorized by the strategy to access the wiki.",
"admin.auth.siteUrlNotSetup": "You must set a valid {siteUrl} first! Click on {general} in the left sidebar.",
"admin.auth.status": "Status",
"admin.auth.strategies": "Strategies",
@@ -1192,9 +1194,10 @@
"auth.tfa.subtitle": "Security code required:",
"auth.tfa.verifyToken": "Verify",
"auth.tfaFormTitle": "Enter the security code generated from your trusted device:",
- "auth.tfaSetupInstrFirst": "1) Scan the QR code below from your mobile 2FA application:",
- "auth.tfaSetupInstrSecond": "2) Enter the security code generated from your trusted device:",
+ "auth.tfaSetupInstrFirst": "Scan the QR code below from your mobile 2FA application:",
+ "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...",
"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 c47952d3..f2dee07b 100644
--- a/server/models/userKeys.mjs
+++ b/server/models/userKeys.mjs
@@ -47,6 +47,7 @@ export class UserKey extends Model {
}
static async generateToken ({ userId, kind, meta }, context) {
+ WIKI.logger.debug(`Generating ${kind} token for user ${userId}...`)
const token = await nanoid()
await WIKI.db.userKeys.query().insert({
kind,
diff --git a/server/models/users.mjs b/server/models/users.mjs
index d63f71c5..d889c3d8 100644
--- a/server/models/users.mjs
+++ b/server/models/users.mjs
@@ -9,7 +9,6 @@ import qr from 'qr-image'
import bcrypt from 'bcryptjs'
import { Group } from './groups.mjs'
-import { Locale } from './locales.mjs'
/**
* Users model
@@ -73,35 +72,39 @@ export class User extends Model {
// Instance Methods
// ------------------------------------------------
- async generateTFA() {
- let tfaInfo = tfa.generateSecret({
- name: WIKI.config.title,
+ async generateTFA(strategyId, siteId) {
+ WIKI.logger.debug(`Generating new TFA secret for user ${this.id}...`)
+ const site = WIKI.sites[siteId] ?? WIKI.sites[0] ?? { config: { title: 'Wiki' }}
+ const tfaInfo = tfa.generateSecret({
+ name: site.config.title,
account: this.email
})
- await WIKI.db.users.query().findById(this.id).patch({
- tfaIsActive: false,
- tfaSecret: tfaInfo.secret
+ this.auth[strategyId].tfaSecret = tfaInfo.secret
+ this.auth[strategyId].tfaIsActive = false
+ await this.$query().patch({
+ auth: this.auth
})
- const safeTitle = WIKI.config.title.replace(/[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g, '')
+ const safeTitle = site.config.title.replace(/[\s-.,=!@#$%?&*()+[\]{}/\\;<>]/g, '')
return qr.imageSync(`otpauth://totp/${safeTitle}:${this.email}?secret=${tfaInfo.secret}`, { type: 'svg' })
}
- async enableTFA() {
- return WIKI.db.users.query().findById(this.id).patch({
- tfaIsActive: true
+ async enableTFA(strategyId) {
+ this.auth[strategyId].tfaIsActive = true
+ return this.$query().patch({
+ auth: this.auth
})
}
- async disableTFA() {
- return this.$query.patch({
+ async disableTFA(strategyId) {
+ this.auth[strategyId].tfaIsActive = false
+ return this.$query().patch({
tfaIsActive: false,
tfaSecret: ''
})
}
- verifyTFA(code) {
- let result = tfa.verifyToken(this.tfaSecret, code)
- return (result && has(result, 'delta') && result.delta === 0)
+ verifyTFA(strategyId, code) {
+ return tfa.verifyToken(this.auth[strategyId].tfaSecret, code)?.delta === 0
}
getPermissions () {
@@ -250,9 +253,9 @@ export class User extends Model {
/**
* Login a user
*/
- static async login (opts, context) {
- if (has(WIKI.auth.strategies, opts.strategy)) {
- const selStrategy = get(WIKI.auth.strategies, opts.strategy)
+ static async login ({ strategyId, siteId, username, password }, context) {
+ if (has(WIKI.auth.strategies, strategyId)) {
+ const selStrategy = WIKI.auth.strategies[strategyId]
if (!selStrategy.isEnabled) {
throw new WIKI.Error.AuthProviderInvalid()
}
@@ -261,9 +264,9 @@ export class User extends Model {
// Inject form user/pass
if (strInfo.useForm) {
- set(context.req, 'body.email', opts.username)
- set(context.req, 'body.password', opts.password)
- set(context.req.params, 'strategy', opts.strategy)
+ set(context.req, 'body.email', username)
+ set(context.req, 'body.password', password)
+ set(context.req.params, 'strategy', strategyId)
}
// Authenticate
@@ -277,6 +280,7 @@ export class User extends Model {
try {
const resp = await WIKI.db.users.afterLoginChecks(user, selStrategy.id, context, {
+ siteId,
skipTFA: !strInfo.useForm,
skipChangePwd: !strInfo.useForm
})
@@ -294,7 +298,12 @@ export class User extends Model {
/**
* Perform post-login checks
*/
- static async afterLoginChecks (user, strategyId, context, { skipTFA, skipChangePwd } = { skipTFA: false, skipChangePwd: false }) {
+ static async afterLoginChecks (user, strategyId, context, { siteId, skipTFA, skipChangePwd } = { skipTFA: false, skipChangePwd: false, siteId: null }) {
+ const str = WIKI.auth.strategies[strategyId]
+ if (!str) {
+ throw new Error('ERR_INVALID_STRATEGY')
+ }
+
// Get redirect target
user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions', 'redirectOnLogin')
let redirect = '/'
@@ -312,14 +321,14 @@ export class User extends Model {
// Is 2FA required?
if (!skipTFA) {
- if (authStr.tfaRequired && authStr.tfaSecret) {
+ if (authStr.tfaIsActive && authStr.tfaSecret) {
try {
const tfaToken = await WIKI.db.userKeys.generateToken({
kind: 'tfa',
userId: user.id
})
return {
- mustProvideTFA: true,
+ nextAction: 'provideTfa',
continuationToken: tfaToken,
redirect
}
@@ -327,15 +336,15 @@ export class User extends Model {
WIKI.logger.warn(errc)
throw new WIKI.Error.AuthGenericError()
}
- } else if (WIKI.config.auth.enforce2FA || (authStr.tfaIsActive && !authStr.tfaSecret)) {
+ } else if (str.config?.enforceTfa || authStr.tfaRequired) {
try {
- const tfaQRImage = await user.generateTFA()
+ const tfaQRImage = await user.generateTFA(strategyId, siteId)
const tfaToken = await WIKI.db.userKeys.generateToken({
kind: 'tfaSetup',
userId: user.id
})
return {
- mustSetupTFA: true,
+ nextAction: 'setupTfa',
continuationToken: tfaToken,
tfaQRImage,
redirect
@@ -356,7 +365,7 @@ export class User extends Model {
})
return {
- mustChangePwd: true,
+ nextAction: 'changePassword',
continuationToken: pwdChangeToken,
redirect
}
@@ -370,7 +379,11 @@ export class User extends Model {
context.req.login(user, { session: false }, async errc => {
if (errc) { return reject(errc) }
const jwtToken = await WIKI.db.users.refreshToken(user, strategyId)
- resolve({ jwt: jwtToken.token, redirect })
+ resolve({
+ nextAction: 'redirect',
+ jwt: jwtToken.token,
+ redirect
+ })
})
})
}
@@ -420,19 +433,19 @@ export class User extends Model {
/**
* Verify a TFA login
*/
- static async loginTFA ({ securityCode, continuationToken, setup }, context) {
+ 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 } = await WIKI.db.userKeys.validateToken({
kind: setup ? 'tfaSetup' : 'tfa',
token: continuationToken,
skipDelete: setup
})
if (user) {
- if (user.verifyTFA(securityCode)) {
+ if (user.verifyTFA(strategyId, securityCode)) {
if (setup) {
- await user.enableTFA()
+ await user.enableTFA(strategyId)
}
- return WIKI.db.users.afterLoginChecks(user, context, { skipTFA: true })
+ return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
} else {
throw new WIKI.Error.AuthTFAFailed()
}
@@ -508,7 +521,14 @@ export class User extends Model {
*
* @param {Object} param0 User Fields
*/
- static async createNewUser ({ email, password, name, groups, mustChangePassword = false, sendWelcomeEmail = false }) {
+ static async createNewUser ({ email, password, name, groups, userInitiated = false, mustChangePassword = false, sendWelcomeEmail = false }) {
+ const localAuth = await WIKI.db.authentication.getStrategy('local')
+
+ // Check if self-registration is enabled
+ if (userInitiated && !localAuth.registration) {
+ throw new Error('ERR_REGISTRATION_DISABLED')
+ }
+
// Input sanitization
email = email.toLowerCase().trim()
@@ -547,14 +567,23 @@ export class User extends Model {
throw new Error(`ERR_INVALID_INPUT: ${validation[0]}`)
}
+ // Check if email address is allowed
+ if (userInitiated && localAuth.allowedEmailRegex) {
+ const emailCheckRgx = new RegExp(localAuth.allowedEmailRegex, 'i')
+ if (!emailCheckRgx.test(email)) {
+ throw new Error('ERR_EMAIL_ADDRESS_NOT_ALLOWED')
+ }
+ }
+
// Check if email already exists
const usr = await WIKI.db.users.query().findOne({ email })
if (usr) {
throw new Error('ERR_ACCOUNT_ALREADY_EXIST')
}
+ WIKI.logger.debug(`Creating new user account for ${email}...`)
+
// Create the account
- const localAuth = await WIKI.db.authentication.getStrategy('local')
const newUsr = await WIKI.db.users.query().insert({
email,
name,
@@ -583,14 +612,41 @@ export class User extends Model {
dateFormat: WIKI.config.userDefaults.dateFormat || 'YYYY-MM-DD',
timeFormat: WIKI.config.userDefaults.timeFormat || '12h'
}
- })
+ }).returning('*')
// Assign to group(s)
- if (groups.length > 0) {
- await newUsr.$relatedQuery('groups').relate(groups)
+ const groupsToEnroll = [WIKI.data.systemIds.usersGroupId]
+ if (groups?.length > 0) {
+ groupsToEnroll.push(...groups)
+ }
+ if (userInitiated && localAuth.autoEnrollGroups?.length > 0) {
+ groupsToEnroll.push(...localAuth.autoEnrollGroups)
}
+ await newUsr.$relatedQuery('groups').relate(uniq(groupsToEnroll))
+
+ // Verification Email
+ if (userInitiated && localAuth.config?.emailValidation) {
+ // Create verification token
+ const verificationToken = await WIKI.db.userKeys.generateToken({
+ kind: 'verify',
+ userId: newUsr.id
+ })
- if (sendWelcomeEmail) {
+ // Send verification email
+ await WIKI.mail.send({
+ template: 'accountVerify',
+ to: email,
+ subject: 'Verify your account',
+ data: {
+ preheadertext: 'Verify your account in order to gain access to the wiki.',
+ title: 'Verify your account',
+ content: 'Click the button below in order to verify your account and gain access to the wiki.',
+ buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
+ buttonText: 'Verify'
+ },
+ text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`
+ })
+ } else if (sendWelcomeEmail) {
// Send welcome email
await WIKI.mail.send({
template: 'accountWelcome',
@@ -606,6 +662,10 @@ export class User extends Model {
text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`
})
}
+
+ WIKI.logger.debug(`Created new user account for ${email} successfully.`)
+
+ return newUsr
}
/**
@@ -680,113 +740,6 @@ export class User extends Model {
}
}
- /**
- * Register a new user (client-side registration)
- *
- * @param {Object} param0 User fields
- * @param {Object} context GraphQL Context
- */
- static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
- const localStrg = await WIKI.db.authentication.getStrategy('local')
- // Check if self-registration is enabled
- if (localStrg.selfRegistration || bypassChecks) {
- // Input sanitization
- email = email.toLowerCase()
-
- // Input validation
- const validation = validate({
- email,
- password,
- name
- }, {
- email: {
- email: true,
- length: {
- maximum: 255
- }
- },
- password: {
- presence: {
- allowEmpty: false
- },
- length: {
- minimum: 6
- }
- },
- name: {
- presence: {
- allowEmpty: false
- },
- length: {
- minimum: 2,
- maximum: 255
- }
- }
- }, { format: 'flat' })
- if (validation && validation.length > 0) {
- throw new WIKI.Error.InputInvalid(validation[0])
- }
-
- // Check if email domain is whitelisted
- if (get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
- const emailDomain = last(email.split('@'))
- if (!localStrg.domainWhitelist.v.includes(emailDomain)) {
- throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
- }
- }
- // Check if email already exists
- const usr = await WIKI.db.users.query().findOne({ email, providerKey: 'local' })
- if (!usr) {
- // Create the account
- const newUsr = await WIKI.db.users.query().insert({
- provider: 'local',
- email,
- name,
- password,
- locale: 'en',
- defaultEditor: 'markdown',
- tfaIsActive: false,
- isSystem: false,
- isActive: true,
- isVerified: false
- })
-
- // Assign to group(s)
- if (get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
- await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
- }
-
- if (verify) {
- // Create verification token
- const verificationToken = await WIKI.db.userKeys.generateToken({
- kind: 'verify',
- userId: newUsr.id
- })
-
- // Send verification email
- await WIKI.mail.send({
- template: 'accountVerify',
- to: email,
- subject: 'Verify your account',
- data: {
- preheadertext: 'Verify your account in order to gain access to the wiki.',
- title: 'Verify your account',
- content: 'Click the button below in order to verify your account and gain access to the wiki.',
- buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
- buttonText: 'Verify'
- },
- text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`
- })
- }
- return true
- } else {
- throw new WIKI.Error.AuthAccountAlreadyExists()
- }
- } else {
- throw new WIKI.Error.AuthRegistrationDisabled()
- }
- }
-
/**
* Logout the current user
*/
diff --git a/server/modules/authentication/local/authentication.mjs b/server/modules/authentication/local/authentication.mjs
index 3d3355fe..9394b648 100644
--- a/server/modules/authentication/local/authentication.mjs
+++ b/server/modules/authentication/local/authentication.mjs
@@ -8,8 +8,8 @@ import bcrypt from 'bcryptjs'
import { Strategy } from 'passport-local'
export default {
- init (passport, conf) {
- passport.use(conf.key,
+ init (passport, strategyId, conf) {
+ passport.use(strategyId,
new Strategy({
usernameField: 'email',
passwordField: 'password'
@@ -19,7 +19,7 @@ export default {
email: uEmail.toLowerCase()
})
if (user) {
- const authStrategyData = user.auth[conf.key]
+ const authStrategyData = user.auth[strategyId]
if (!authStrategyData) {
throw new WIKI.Error.AuthLoginFailed()
} else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
diff --git a/server/modules/authentication/local/definition.yml b/server/modules/authentication/local/definition.yml
index 7efaffd3..77478da2 100644
--- a/server/modules/authentication/local/definition.yml
+++ b/server/modules/authentication/local/definition.yml
@@ -10,4 +10,16 @@ website: 'https://js.wiki'
isAvailable: true
useForm: true
usernameType: email
-props: {}
+props:
+ enforceTfa:
+ type: Boolean
+ title: Enforce Two-Factor Authentication
+ hint: Users will be required to setup 2FA the first time they login and cannot be disabled by the user.
+ icon: pin-pad
+ default: false
+ emailValidation:
+ type: Boolean
+ title: Email Validation
+ hint: Send a verification email to the user with a validation link when registering (if registration is enabled).
+ icon: received
+ default: true
diff --git a/ux/public/_assets/icons/ultraviolet-pin-pad.svg b/ux/public/_assets/icons/ultraviolet-pin-pad.svg
new file mode 100644
index 00000000..85a41089
--- /dev/null
+++ b/ux/public/_assets/icons/ultraviolet-pin-pad.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ux/src/components/AuthLoginPanel.vue b/ux/src/components/AuthLoginPanel.vue
index eaa26352..64a455f8 100644
--- a/ux/src/components/AuthLoginPanel.vue
+++ b/ux/src/components/AuthLoginPanel.vue
@@ -53,7 +53,8 @@
)
template(v-if='selectedStrategy.activeStrategy?.strategy?.key === `local`')
q-separator.q-my-md
- q-btn.acrylic-btn.full-width(
+ q-btn.acrylic-btn.full-width.q-mb-sm(
+ v-if='selectedStrategy.activeStrategy.registration'
flat
color='primary'
:label='t(`auth.switchToRegister.link`)'
@@ -61,7 +62,7 @@
icon='las la-user-plus'
@click='switchTo(`register`)'
)
- q-btn.acrylic-btn.full-width.q-mt-sm(
+ q-btn.acrylic-btn.full-width(
flat
color='primary'
:label='t(`auth.forgotPasswordLink`)'
@@ -248,17 +249,15 @@
//- -----------------------------------------------------
template(v-else-if='state.screen === `tfa`')
p {{t('auth.tfa.subtitle')}}
- .auth-login-tfa
- v-otp-input(
- ref='tfaIpt'
- :num-inputs='6'
- :should-auto-focus='true'
- input-classes='otp-input'
- input-type='number'
- separator=''
- @on-change='v => state.securityCode = v'
- @on-complete='verifyTFA'
- )
+ v-otp-input(
+ v-model:value='state.securityCode'
+ :num-inputs='6'
+ :should-auto-focus='true'
+ input-classes='otp-input'
+ input-type='number'
+ separator=''
+ @on-complete='verifyTFA'
+ )
q-btn.full-width.q-mt-md(
push
color='primary'
@@ -271,7 +270,27 @@
//- TFA SETUP SCREEN
//- -----------------------------------------------------
template(v-else-if='state.screen === `tfasetup`')
- p TODO - TFA Setup not available yet.
+ p {{t('auth.tfaSetupTitle')}}
+ p {{t('auth.tfaSetupInstrFirst')}}
+ div(style='justify-content: center; display: flex;')
+ div(v-html='state.tfaQRImage', style='width: 200px;')
+ p.q-mt-sm {{t('auth.tfaSetupInstrSecond')}}
+ v-otp-input(
+ v-model:value='state.securityCode'
+ :num-inputs='6'
+ :should-auto-focus='true'
+ input-classes='otp-input'
+ input-type='number'
+ separator=''
+ )
+ q-btn.full-width.q-mt-md(
+ push
+ color='primary'
+ :label='t(`auth.tfa.verifyToken`)'
+ no-caps
+ icon='las la-sign-in-alt'
+ @click='finishSetupTFA'
+ )