Unverified Commit fe8066c8 authored by NGPixel's avatar NGPixel

feat: setup TFA

parent 7c2b5dd4
...@@ -133,6 +133,10 @@ editors: ...@@ -133,6 +133,10 @@ editors:
wysiwyg: wysiwyg:
contentType: html contentType: html
config: {} config: {}
systemIds:
localAuthId: '5a528c4c-0a82-4ad2-96a5-2b23811e6588'
guestsGroupId: '10000000-0000-4000-8000-000000000001'
usersGroupId: '20000000-0000-4000-8000-000000000002'
groups: groups:
defaultPermissions: defaultPermissions:
- 'read:pages' - 'read:pages'
......
...@@ -79,13 +79,9 @@ export default { ...@@ -79,13 +79,9 @@ export default {
for (const stg of enabledStrategies) { for (const stg of enabledStrategies) {
try { try {
const strategy = (await import(`../modules/authentication/${stg.module}/authentication.mjs`)).default 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` WIKI.auth.strategies[stg.id] = {
stg.config.key = stg.id
strategy.init(passport, stg.config)
strategy.config = stg.config
WIKI.auth.strategies[stg.key] = {
...strategy, ...strategy,
...stg ...stg
} }
......
...@@ -66,8 +66,8 @@ export async function up (knex) { ...@@ -66,8 +66,8 @@ export async function up (knex) {
table.boolean('isEnabled').notNullable().defaultTo(false) table.boolean('isEnabled').notNullable().defaultTo(false)
table.string('displayName').notNullable().defaultTo('') table.string('displayName').notNullable().defaultTo('')
table.jsonb('config').notNullable().defaultTo('{}') table.jsonb('config').notNullable().defaultTo('{}')
table.boolean('selfRegistration').notNullable().defaultTo(false) table.boolean('registration').notNullable().defaultTo(false)
table.string('allowedEmailRegex') table.string('allowedEmailRegex').notNullable().defaultTo('')
table.specificType('autoEnrollGroups', 'uuid[]') table.specificType('autoEnrollGroups', 'uuid[]')
}) })
.createTable('blocks', table => { .createTable('blocks', table => {
...@@ -430,10 +430,10 @@ export async function up (knex) { ...@@ -430,10 +430,10 @@ export async function up (knex) {
// -> GENERATE IDS // -> GENERATE IDS
const groupAdminId = uuid() const groupAdminId = uuid()
const groupUserId = uuid() const groupUserId = WIKI.data.systemIds.usersGroupId
const groupGuestId = '10000000-0000-4000-8000-000000000001' const groupGuestId = WIKI.data.systemIds.guestsGroupId
const siteId = uuid() const siteId = uuid()
const authModuleId = uuid() const authModuleId = WIKI.data.systemIds.localAuthId
const userAdminId = uuid() const userAdminId = uuid()
const userGuestId = uuid() const userGuestId = uuid()
...@@ -719,7 +719,11 @@ export async function up (knex) { ...@@ -719,7 +719,11 @@ export async function up (knex) {
id: authModuleId, id: authModuleId,
module: 'local', module: 'local',
isEnabled: true, isEnabled: true,
displayName: 'Local Authentication' displayName: 'Local Authentication',
config: JSON.stringify({
emailValidation: true,
enforceTfa: false
})
}) })
// -> USERS // -> USERS
...@@ -734,6 +738,7 @@ export async function up (knex) { ...@@ -734,6 +738,7 @@ export async function up (knex) {
mustChangePwd: false, // TODO: Revert to true (below) once change password flow is implemented 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,
tfaRequired: false, tfaRequired: false,
tfaSecret: '' tfaSecret: ''
} }
......
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
return { return {
...a, ...a,
config: _.transform(str.props, (r, v, k) => { 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 { ...@@ -102,7 +102,7 @@ export default {
if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) { if (args.strategy === 'ldap' && WIKI.config.flags.ldapdebug) {
WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err) WIKI.logger.warn('LDAP LOGIN ERROR (c1): ', err)
} }
console.error(err) WIKI.logger.debug(err)
return generateError(err) return generateError(err)
} }
...@@ -115,9 +115,10 @@ export default { ...@@ -115,9 +115,10 @@ export default {
const authResult = await WIKI.db.users.loginTFA(args, context) const authResult = await WIKI.db.users.loginTFA(args, context)
return { return {
...authResult, ...authResult,
responseResult: generateSuccess('TFA success') operation: generateSuccess('TFA success')
} }
} catch (err) { } catch (err) {
WIKI.logger.debug(err)
return generateError(err) return generateError(err)
} }
}, },
...@@ -129,9 +130,10 @@ export default { ...@@ -129,9 +130,10 @@ export default {
const authResult = await WIKI.db.users.loginChangePassword(args, context) const authResult = await WIKI.db.users.loginChangePassword(args, context)
return { return {
...authResult, ...authResult,
responseResult: generateSuccess('Password changed successfully') operation: generateSuccess('Password changed successfully')
} }
} catch (err) { } catch (err) {
WIKI.logger.debug(err)
return generateError(err) return generateError(err)
} }
}, },
...@@ -142,7 +144,7 @@ export default { ...@@ -142,7 +144,7 @@ export default {
try { try {
await WIKI.db.users.loginForgotPassword(args, context) await WIKI.db.users.loginForgotPassword(args, context)
return { return {
responseResult: generateSuccess('Password reset request processed.') operation: generateSuccess('Password reset request processed.')
} }
} catch (err) { } catch (err) {
return generateError(err) return generateError(err)
...@@ -153,9 +155,11 @@ export default { ...@@ -153,9 +155,11 @@ export default {
*/ */
async register (obj, args, context) { async register (obj, args, context) {
try { 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 { return {
responseResult: generateSuccess('Registration success') ...authResult,
operation: generateSuccess('Registration success')
} }
} catch (err) { } catch (err) {
return generateError(err) return generateError(err)
......
...@@ -30,14 +30,16 @@ extend type Mutation { ...@@ -30,14 +30,16 @@ extend type Mutation {
username: String! username: String!
password: String! password: String!
strategyId: UUID! strategyId: UUID!
siteId: UUID siteId: UUID!
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
loginTFA( loginTFA(
continuationToken: String! continuationToken: String!
securityCode: String! securityCode: String!
strategyId: UUID!
siteId: UUID!
setup: Boolean setup: Boolean
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
changePassword( changePassword(
userId: UUID userId: UUID
...@@ -46,7 +48,7 @@ extend type Mutation { ...@@ -46,7 +48,7 @@ extend type Mutation {
newPassword: String! newPassword: String!
strategyId: UUID! strategyId: UUID!
siteId: UUID siteId: UUID
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
forgotPassword( forgotPassword(
email: String! email: String!
...@@ -56,7 +58,7 @@ extend type Mutation { ...@@ -56,7 +58,7 @@ extend type Mutation {
email: String! email: String!
password: String! password: String!
name: String! name: String!
): AuthenticationRegisterResponse ): AuthenticationAuthResponse @rateLimit(limit: 5, duration: 60)
refreshToken( refreshToken(
token: String! token: String!
...@@ -105,7 +107,7 @@ type AuthenticationActiveStrategy { ...@@ -105,7 +107,7 @@ type AuthenticationActiveStrategy {
displayName: String displayName: String
isEnabled: Boolean isEnabled: Boolean
config: JSON config: JSON
selfRegistration: Boolean registration: Boolean
allowedEmailRegex: String allowedEmailRegex: String
autoEnrollGroups: [UUID] autoEnrollGroups: [UUID]
} }
...@@ -116,22 +118,15 @@ type AuthenticationSiteStrategy { ...@@ -116,22 +118,15 @@ type AuthenticationSiteStrategy {
isVisible: Boolean isVisible: Boolean
} }
type AuthenticationLoginResponse { type AuthenticationAuthResponse {
operation: Operation operation: Operation
jwt: String jwt: String
mustChangePwd: Boolean nextAction: AuthenticationNextAction
mustProvideTFA: Boolean
mustSetupTFA: Boolean
continuationToken: String continuationToken: String
redirect: String redirect: String
tfaQRImage: String tfaQRImage: String
} }
type AuthenticationRegisterResponse {
operation: Operation
jwt: String
}
type AuthenticationTokenResponse { type AuthenticationTokenResponse {
operation: Operation operation: Operation
jwt: String jwt: String
...@@ -140,11 +135,11 @@ type AuthenticationTokenResponse { ...@@ -140,11 +135,11 @@ type AuthenticationTokenResponse {
input AuthenticationStrategyInput { input AuthenticationStrategyInput {
key: String! key: String!
strategyKey: String! strategyKey: String!
config: [KeyValuePairInput] config: JSON!
displayName: String! displayName: String!
order: Int! order: Int!
isEnabled: Boolean! isEnabled: Boolean!
selfRegistration: Boolean! registration: Boolean!
allowedEmailRegex: String! allowedEmailRegex: String!
autoEnrollGroups: [UUID]! autoEnrollGroups: [UUID]!
} }
...@@ -163,3 +158,10 @@ type AuthenticationCreateApiKeyResponse { ...@@ -163,3 +158,10 @@ type AuthenticationCreateApiKeyResponse {
operation: Operation operation: Operation
key: String key: String
} }
enum AuthenticationNextAction {
changePassword
setupTfa
provideTfa
redirect
}
...@@ -76,11 +76,13 @@ ...@@ -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.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.displayName": "Display Name",
"admin.auth.displayNameHint": "The title shown to the end user for this authentication strategy.", "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.enabled": "Enabled",
"admin.auth.enabledForced": "This strategy cannot be disabled.", "admin.auth.enabledForced": "This strategy cannot be disabled.",
"admin.auth.enabledHint": "Should this strategy be available to sites for login.", "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.enforceTfa": "Enforce Two-Factor Authentication",
"admin.auth.force2faHint": "Users will be required to setup 2FA the first time they login and cannot be disabled by the user.", "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.globalAdvSettings": "Global Advanced Settings",
"admin.auth.info": "Info", "admin.auth.info": "Info",
"admin.auth.infoName": "Name", "admin.auth.infoName": "Name",
...@@ -90,10 +92,10 @@ ...@@ -90,10 +92,10 @@
"admin.auth.noConfigOption": "This strategy has no configuration options you can modify.", "admin.auth.noConfigOption": "This strategy has no configuration options you can modify.",
"admin.auth.refreshSuccess": "List of strategies has been refreshed.", "admin.auth.refreshSuccess": "List of strategies has been refreshed.",
"admin.auth.registration": "Registration", "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.saveSuccess": "Authentication configuration saved successfully.",
"admin.auth.security": "Security", "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.siteUrlNotSetup": "You must set a valid {siteUrl} first! Click on {general} in the left sidebar.",
"admin.auth.status": "Status", "admin.auth.status": "Status",
"admin.auth.strategies": "Strategies", "admin.auth.strategies": "Strategies",
...@@ -1192,9 +1194,10 @@ ...@@ -1192,9 +1194,10 @@
"auth.tfa.subtitle": "Security code required:", "auth.tfa.subtitle": "Security code required:",
"auth.tfa.verifyToken": "Verify", "auth.tfa.verifyToken": "Verify",
"auth.tfaFormTitle": "Enter the security code generated from your trusted device:", "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.tfaSetupInstrFirst": "Scan the QR code below from your mobile 2FA application:",
"auth.tfaSetupInstrSecond": "2) 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...",
"common.actions.activate": "Activate", "common.actions.activate": "Activate",
"common.actions.add": "Add", "common.actions.add": "Add",
"common.actions.apply": "Apply", "common.actions.apply": "Apply",
......
...@@ -47,6 +47,7 @@ export class UserKey extends Model { ...@@ -47,6 +47,7 @@ export class UserKey extends Model {
} }
static async generateToken ({ userId, kind, meta }, context) { static async generateToken ({ userId, kind, meta }, context) {
WIKI.logger.debug(`Generating ${kind} token for user ${userId}...`)
const token = await nanoid() const token = await nanoid()
await WIKI.db.userKeys.query().insert({ await WIKI.db.userKeys.query().insert({
kind, kind,
......
...@@ -8,8 +8,8 @@ import bcrypt from 'bcryptjs' ...@@ -8,8 +8,8 @@ import bcrypt from 'bcryptjs'
import { Strategy } from 'passport-local' import { Strategy } from 'passport-local'
export default { export default {
init (passport, conf) { init (passport, strategyId, conf) {
passport.use(conf.key, passport.use(strategyId,
new Strategy({ new Strategy({
usernameField: 'email', usernameField: 'email',
passwordField: 'password' passwordField: 'password'
...@@ -19,7 +19,7 @@ export default { ...@@ -19,7 +19,7 @@ export default {
email: uEmail.toLowerCase() email: uEmail.toLowerCase()
}) })
if (user) { if (user) {
const authStrategyData = user.auth[conf.key] const authStrategyData = user.auth[strategyId]
if (!authStrategyData) { if (!authStrategyData) {
throw new WIKI.Error.AuthLoginFailed() throw new WIKI.Error.AuthLoginFailed()
} else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) { } else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
......
...@@ -10,4 +10,16 @@ website: 'https://js.wiki' ...@@ -10,4 +10,16 @@ website: 'https://js.wiki'
isAvailable: true isAvailable: true
useForm: true useForm: true
usernameType: email 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
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#98ccfd" d="M4,23.5c-0.827,0-1.5-0.673-1.5-1.5V4c0-0.827,0.673-1.5,1.5-1.5h17c0.827,0,1.5,0.673,1.5,1.5v18 c0,0.827-0.673,1.5-1.5,1.5H4z"/><path fill="#4788c7" d="M21,3c0.551,0,1,0.449,1,1v18c0,0.551-0.449,1-1,1H4c-0.551,0-1-0.449-1-1V4c0-0.551,0.449-1,1-1 H21 M21,2H4C2.895,2,2,2.895,2,4v18c0,1.105,0.895,2,2,2h17c1.105,0,2-0.895,2-2V4C23,2.895,22.105,2,21,2L21,2z"/><path fill="#fff" d="M11 5H15V9H11zM5 5H9V9H5zM11 11H15V15H11zM5 11H9V15H5zM11 17H15V21H11zM5 17H9V21H5z"/><path fill="#dff0fe" d="M25.361,38.5c-2.531,0-4.841-1.179-6.338-3.236l-8.712-10.095 c-1.071-1.073-1.071-2.775-0.025-3.822c0.568-0.568,1.242-0.848,1.959-0.848s1.391,0.279,1.897,0.786l3.357,3.891V7 c0-1.378,1.121-2.5,2.5-2.5s2.5,1.122,2.5,2.5v8.902l0.835-0.752C23.8,14.731,24.392,14.5,25,14.5 c1.034,0,1.973,0.657,2.335,1.634l0.282,0.76l0.553-0.593C28.65,15.784,29.301,15.5,30,15.5c1.042,0,1.981,0.657,2.34,1.635 l0.28,0.766l0.556-0.597C33.657,16.785,34.306,16.5,35,16.5c1.379,0,2.5,1.122,2.5,2.5v11.921c0,4.179-3.399,7.579-7.578,7.579 H25.361z"/><path fill="#4788c7" d="M20,5c1.103,0,2,0.897,2,2v7.779v2.247l1.669-1.504C23.933,15.284,24.379,15,25,15 c0.826,0,1.576,0.526,1.866,1.308l0.564,1.519l1.105-1.185C28.921,16.228,29.441,16,30,16c0.833,0,1.585,0.525,1.87,1.306 l0.56,1.532l1.111-1.194C33.928,17.229,34.446,17,35,17c1.103,0,2,0.897,2,2l0,1v10.921C37,34.825,33.825,38,29.921,38h-4.56 c-2.369,0-4.532-1.104-5.934-3.03l-0.024-0.034l-0.027-0.031l-8.687-10.063l-0.024-0.028l-0.026-0.026 c-0.851-0.851-0.851-2.237,0-3.088l0.061-0.061c0.412-0.412,0.961-0.64,1.544-0.64c0.572,0,1.111,0.219,1.52,0.616l2.478,2.87 L18,26.522l0-2.689L18,22V7C18,5.897,18.897,5,20,5 M20,4c-1.657,0-3,1.343-3,3v15h0v1.833l-2.504-2.9 C13.875,20.311,13.06,20,12.245,20c-0.815,0-1.629,0.311-2.251,0.932l-0.062,0.062c-1.243,1.243-1.243,3.259,0,4.502 l8.687,10.063C20.136,37.642,22.577,39,25.361,39h4.56C34.383,39,38,35.383,38,30.921V20h0v-1c0-1.657-1.343-3-3-3 c-0.868,0-1.643,0.374-2.191,0.963C32.39,15.816,31.296,15,30,15c-0.871,0-1.648,0.372-2.196,0.96C27.38,14.818,26.289,14,25,14 c-0.772,0-1.468,0.3-2,0.779V7C23,5.343,21.657,4,20,4L20,4z"/><path fill="#4788c7" d="M32 17L32 20 33 20 33.009 18.17zM27 16L27 20 28 20 28 17zM22 15L22 20 23 20 23 16z"/><path fill="#4788c7" d="M22.5 19L22.5 19c.275 0 .5.225.5.5v1c0 .275-.225.5-.5.5l0 0c-.275 0-.5-.225-.5-.5v-1C22 19.225 22.225 19 22.5 19zM27.5 19L27.5 19c.275 0 .5.225.5.5v1c0 .275-.225.5-.5.5l0 0c-.275 0-.5-.225-.5-.5v-1C27 19.225 27.225 19 27.5 19zM32.5 19L32.5 19c.275 0 .5.225.5.5v1c0 .275-.225.5-.5.5l0 0c-.275 0-.5-.225-.5-.5v-1C32 19.225 32.225 19 32.5 19z"/></svg>
\ No newline at end of file
...@@ -111,17 +111,17 @@ q-page.admin-mail ...@@ -111,17 +111,17 @@ q-page.admin-mail
q-item(tag='label') q-item(tag='label')
blueprint-icon(icon='register') blueprint-icon(icon='register')
q-item-section q-item-section
q-item-label {{t(`admin.auth.selfRegistration`)}} q-item-label {{t(`admin.auth.registration`)}}
q-item-label(caption) {{t(`admin.auth.selfRegistrationHint`)}} q-item-label(caption) {{state.strategy.strategy.key === `local` ? t(`admin.auth.registrationLocalHint`) : t(`admin.auth.registrationHint`)}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='state.strategy.selfRegistration' v-model='state.strategy.registration'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='t(`admin.auth.selfRegistration`)' :aria-label='t(`admin.auth.registration`)'
) )
template(v-if='state.strategy.selfRegistration') template(v-if='state.strategy.registration')
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='team') blueprint-icon(icon='team')
...@@ -431,7 +431,7 @@ async function load () { ...@@ -431,7 +431,7 @@ async function load () {
displayName displayName
isEnabled isEnabled
config config
selfRegistration registration
allowedEmailRegex allowedEmailRegex
autoEnrollGroups autoEnrollGroups
} }
...@@ -504,7 +504,7 @@ function addStrategy (str) { ...@@ -504,7 +504,7 @@ function addStrategy (str) {
}, {}), }, {}),
isEnabled: true, isEnabled: true,
displayName: str.title, displayName: str.title,
selfRegistration: true, registration: true,
allowedEmailRegex: '', allowedEmailRegex: '',
autoEnrollGroups: [] autoEnrollGroups: []
} }
......
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