Unverified Commit fe8066c8 authored by NGPixel's avatar NGPixel

feat: setup TFA

parent 7c2b5dd4
......@@ -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'
......
......@@ -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
}
......
......@@ -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: ''
}
......
......@@ -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)
......
......@@ -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
}
......@@ -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",
......
......@@ -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,
......
......@@ -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) {
......
......@@ -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
<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
q-item(tag='label')
blueprint-icon(icon='register')
q-item-section
q-item-label {{t(`admin.auth.selfRegistration`)}}
q-item-label(caption) {{t(`admin.auth.selfRegistrationHint`)}}
q-item-label {{t(`admin.auth.registration`)}}
q-item-label(caption) {{state.strategy.strategy.key === `local` ? t(`admin.auth.registrationLocalHint`) : t(`admin.auth.registrationHint`)}}
q-item-section(avatar)
q-toggle(
v-model='state.strategy.selfRegistration'
v-model='state.strategy.registration'
color='primary'
checked-icon='las la-check'
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-item
blueprint-icon(icon='team')
......@@ -431,7 +431,7 @@ async function load () {
displayName
isEnabled
config
selfRegistration
registration
allowedEmailRegex
autoEnrollGroups
}
......@@ -504,7 +504,7 @@ function addStrategy (str) {
}, {}),
isEnabled: true,
displayName: str.title,
selfRegistration: true,
registration: true,
allowedEmailRegex: '',
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