feat: mandatory password change on login + UI fixes
This commit is contained in:
@@ -42,6 +42,13 @@ defaults:
|
||||
theme: 'default'
|
||||
iconset: 'md'
|
||||
darkMode: false
|
||||
security:
|
||||
securityIframe: true
|
||||
securityReferrerPolicy: true
|
||||
securityHSTS: false
|
||||
securityHSTSDuration: 300
|
||||
securityCSP: false
|
||||
securityCSPDirectives: ''
|
||||
flags:
|
||||
ldapdebug: false
|
||||
sqllog: false
|
||||
|
@@ -7,16 +7,16 @@ const graphHelper = require('../../helpers/graph')
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
async authentication() { return {} }
|
||||
async authentication () { return {} }
|
||||
},
|
||||
Mutation: {
|
||||
async authentication() { return {} }
|
||||
async authentication () { return {} }
|
||||
},
|
||||
AuthenticationQuery: {
|
||||
/**
|
||||
* Fetch active authentication strategies
|
||||
*/
|
||||
async strategies(obj, args, context, info) {
|
||||
async strategies (obj, args, context, info) {
|
||||
let strategies = await WIKI.models.authentication.getStrategies(args.isEnabled)
|
||||
strategies = strategies.map(stg => {
|
||||
const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.key]) || {}
|
||||
@@ -44,7 +44,7 @@ module.exports = {
|
||||
/**
|
||||
* Perform Login
|
||||
*/
|
||||
async login(obj, args, context) {
|
||||
async login (obj, args, context) {
|
||||
try {
|
||||
const authResult = await WIKI.models.users.login(args, context)
|
||||
return {
|
||||
@@ -63,7 +63,7 @@ module.exports = {
|
||||
/**
|
||||
* Perform 2FA Login
|
||||
*/
|
||||
async loginTFA(obj, args, context) {
|
||||
async loginTFA (obj, args, context) {
|
||||
try {
|
||||
const authResult = await WIKI.models.users.loginTFA(args, context)
|
||||
return {
|
||||
@@ -74,10 +74,24 @@ module.exports = {
|
||||
return graphHelper.generateError(err)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Perform Mandatory Password Change after Login
|
||||
*/
|
||||
async loginChangePassword (obj, args, context) {
|
||||
try {
|
||||
const authResult = await WIKI.models.users.loginChangePassword(args, context)
|
||||
return {
|
||||
...authResult,
|
||||
responseResult: graphHelper.generateSuccess('Password changed successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
return graphHelper.generateError(err)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Register a new account
|
||||
*/
|
||||
async register(obj, args, context) {
|
||||
async register (obj, args, context) {
|
||||
try {
|
||||
await WIKI.models.users.register({ ...args, verify: true }, context)
|
||||
return {
|
||||
@@ -90,7 +104,7 @@ module.exports = {
|
||||
/**
|
||||
* Update Authentication Strategies
|
||||
*/
|
||||
async updateStrategies(obj, args, context) {
|
||||
async updateStrategies (obj, args, context) {
|
||||
try {
|
||||
WIKI.config.auth = {
|
||||
audience: _.get(args, 'config.audience', WIKI.config.auth.audience),
|
||||
@@ -122,7 +136,7 @@ module.exports = {
|
||||
/**
|
||||
* Generate New Authentication Public / Private Key Certificates
|
||||
*/
|
||||
async regenerateCertificates(obj, args, context) {
|
||||
async regenerateCertificates (obj, args, context) {
|
||||
try {
|
||||
await WIKI.auth.regenerateCertificates()
|
||||
return {
|
||||
@@ -135,7 +149,7 @@ module.exports = {
|
||||
/**
|
||||
* Reset Guest User
|
||||
*/
|
||||
async resetGuestUser(obj, args, context) {
|
||||
async resetGuestUser (obj, args, context) {
|
||||
try {
|
||||
await WIKI.auth.resetGuestUser()
|
||||
return {
|
||||
|
@@ -17,7 +17,8 @@ module.exports = {
|
||||
company: WIKI.config.company,
|
||||
...WIKI.config.seo,
|
||||
...WIKI.config.logo,
|
||||
...WIKI.config.features
|
||||
...WIKI.config.features,
|
||||
...WIKI.config.security
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -42,7 +43,15 @@ module.exports = {
|
||||
featurePageComments: args.featurePageComments,
|
||||
featurePersonalWikis: args.featurePersonalWikis
|
||||
}
|
||||
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features'])
|
||||
WIKI.config.security = {
|
||||
securityIframe: args.securityIframe,
|
||||
securityReferrerPolicy: args.securityReferrerPolicy,
|
||||
securityHSTS: args.securityHSTS,
|
||||
securityHSTSDuration: args.securityHSTSDuration,
|
||||
securityCSP: args.securityCSP,
|
||||
securityCSPDirectives: args.securityCSPDirectives
|
||||
}
|
||||
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'seo', 'logo', 'features', 'security'])
|
||||
|
||||
return {
|
||||
responseResult: graphHelper.generateSuccess('Site configuration updated successfully')
|
||||
|
@@ -32,9 +32,14 @@ type AuthenticationMutation {
|
||||
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
|
||||
|
||||
loginTFA(
|
||||
loginToken: String!
|
||||
continuationToken: String!
|
||||
securityCode: String!
|
||||
): DefaultResponse @rateLimit(limit: 5, duration: 60)
|
||||
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
|
||||
|
||||
loginChangePassword(
|
||||
continuationToken: String!
|
||||
newPassword: String!
|
||||
): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60)
|
||||
|
||||
register(
|
||||
email: String!
|
||||
@@ -76,8 +81,9 @@ type AuthenticationStrategy {
|
||||
type AuthenticationLoginResponse {
|
||||
responseResult: ResponseStatus
|
||||
jwt: String
|
||||
tfaRequired: Boolean
|
||||
tfaLoginToken: String
|
||||
mustChangePwd: Boolean
|
||||
mustProvideTFA: Boolean
|
||||
continuationToken: String
|
||||
}
|
||||
|
||||
type AuthenticationRegisterResponse {
|
||||
|
@@ -36,6 +36,12 @@ type SiteMutation {
|
||||
featurePageRatings: Boolean!
|
||||
featurePageComments: Boolean!
|
||||
featurePersonalWikis: Boolean!
|
||||
securityIframe: Boolean!
|
||||
securityReferrerPolicy: Boolean!
|
||||
securityHSTS: Boolean!
|
||||
securityHSTSDuration: Int!
|
||||
securityCSP: Boolean!
|
||||
securityCSPDirectives: String!
|
||||
): DefaultResponse @auth(requires: ["manage:system"])
|
||||
}
|
||||
|
||||
@@ -56,4 +62,10 @@ type SiteConfig {
|
||||
featurePageRatings: Boolean!
|
||||
featurePageComments: Boolean!
|
||||
featurePersonalWikis: Boolean!
|
||||
securityIframe: Boolean!
|
||||
securityReferrerPolicy: Boolean!
|
||||
securityHSTS: Boolean!
|
||||
securityHSTSDuration: Int!
|
||||
securityCSP: Boolean!
|
||||
securityCSPDirectives: String!
|
||||
}
|
||||
|
@@ -89,6 +89,8 @@ type User {
|
||||
providerKey: String!
|
||||
providerId: String
|
||||
isSystem: Boolean!
|
||||
isActive: Boolean!
|
||||
isVerified: Boolean!
|
||||
location: String!
|
||||
jobTitle: String!
|
||||
timezone: String!
|
||||
|
@@ -1,4 +1,4 @@
|
||||
'use strict'
|
||||
/* global WIKI */
|
||||
|
||||
/**
|
||||
* Security Middleware
|
||||
@@ -13,7 +13,9 @@ module.exports = function (req, res, next) {
|
||||
req.app.disable('x-powered-by')
|
||||
|
||||
// -> Disable Frame Embedding
|
||||
res.set('X-Frame-Options', 'deny')
|
||||
if (WIKI.config.securityIframe) {
|
||||
res.set('X-Frame-Options', 'deny')
|
||||
}
|
||||
|
||||
// -> Re-enable XSS Fitler if disabled
|
||||
res.set('X-XSS-Protection', '1; mode=block')
|
||||
@@ -25,7 +27,14 @@ module.exports = function (req, res, next) {
|
||||
res.set('X-UA-Compatible', 'IE=edge')
|
||||
|
||||
// -> Disables referrer header when navigating to a different origin
|
||||
res.set('Referrer-Policy', 'same-origin')
|
||||
if (WIKI.config.securityReferrerPolicy) {
|
||||
res.set('Referrer-Policy', 'same-origin')
|
||||
}
|
||||
|
||||
// -> Enforce HSTS
|
||||
if (WIKI.config.securityHSTS) {
|
||||
res.set('Strict-Transport-Security', `max-age=${WIKI.config.securityHSTSDuration}; includeSubDomains`)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
@@ -45,7 +45,7 @@ module.exports = class UserKey extends Model {
|
||||
}
|
||||
|
||||
static async generateToken ({ userId, kind }, context) {
|
||||
const token = await nanoid()
|
||||
const token = nanoid()
|
||||
await WIKI.models.userKeys.query().insert({
|
||||
kind,
|
||||
token,
|
||||
|
@@ -3,7 +3,6 @@
|
||||
const bcrypt = require('bcryptjs-then')
|
||||
const _ = require('lodash')
|
||||
const tfa = require('node-2fa')
|
||||
const securityHelper = require('../helpers/security')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const Model = require('objection').Model
|
||||
const validate = require('validate.js')
|
||||
@@ -280,30 +279,46 @@ module.exports = class User extends Model {
|
||||
if (err) { return reject(err) }
|
||||
if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
|
||||
|
||||
// Is 2FA required?
|
||||
if (user.tfaIsActive) {
|
||||
// Must Change Password?
|
||||
if (user.mustChangePwd) {
|
||||
try {
|
||||
let loginToken = await securityHelper.generateToken(32)
|
||||
await WIKI.redis.set(`tfa:${loginToken}`, user.id, 'EX', 600)
|
||||
const pwdChangeToken = await WIKI.models.userKeys.generateToken({
|
||||
kind: 'changePwd',
|
||||
userId: user.id
|
||||
})
|
||||
|
||||
return resolve({
|
||||
tfaRequired: true,
|
||||
tfaLoginToken: loginToken
|
||||
mustChangePwd: true,
|
||||
continuationToken: pwdChangeToken
|
||||
})
|
||||
} catch (err) {
|
||||
WIKI.logger.warn(err)
|
||||
return reject(new WIKI.Error.AuthGenericError())
|
||||
}
|
||||
} else {
|
||||
// No 2FA, log in user
|
||||
return context.req.logIn(user, { session: !strInfo.useForm }, async err => {
|
||||
if (err) { return reject(err) }
|
||||
const jwtToken = await WIKI.models.users.refreshToken(user)
|
||||
resolve({
|
||||
jwt: jwtToken.token,
|
||||
tfaRequired: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Is 2FA required?
|
||||
if (user.tfaIsActive) {
|
||||
try {
|
||||
const tfaToken = await WIKI.models.userKeys.generateToken({
|
||||
kind: 'tfa',
|
||||
userId: user.id
|
||||
})
|
||||
return resolve({
|
||||
tfaRequired: true,
|
||||
continuationToken: tfaToken
|
||||
})
|
||||
} catch (err) {
|
||||
WIKI.logger.warn(err)
|
||||
return reject(new WIKI.Error.AuthGenericError())
|
||||
}
|
||||
}
|
||||
|
||||
context.req.logIn(user, { session: !strInfo.useForm }, async err => {
|
||||
if (err) { return reject(err) }
|
||||
const jwtToken = await WIKI.models.users.refreshToken(user)
|
||||
resolve({ jwt: jwtToken.token })
|
||||
})
|
||||
})(context.req, context.res, () => {})
|
||||
})
|
||||
} else {
|
||||
@@ -348,7 +363,7 @@ module.exports = class User extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
static async loginTFA(opts, context) {
|
||||
static async loginTFA (opts, context) {
|
||||
if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
|
||||
let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
|
||||
if (result) {
|
||||
@@ -374,6 +389,36 @@ module.exports = class User extends Model {
|
||||
throw new WIKI.Error.AuthTFAInvalid()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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!')
|
||||
}
|
||||
const usr = await WIKI.models.userKeys.validateToken({
|
||||
kind: 'changePwd',
|
||||
token: continuationToken
|
||||
})
|
||||
|
||||
if (usr) {
|
||||
await WIKI.models.users.query().patch({
|
||||
password: newPassword,
|
||||
mustChangePwd: false
|
||||
}).findById(usr.id)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
context.req.logIn(usr, { session: false }, async err => {
|
||||
if (err) { return reject(err) }
|
||||
const jwtToken = await WIKI.models.users.refreshToken(usr)
|
||||
resolve({ jwt: jwtToken.token })
|
||||
})
|
||||
})
|
||||
} else {
|
||||
throw new WIKI.Error.UserNotFound()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user
|
||||
*
|
||||
@@ -520,7 +565,7 @@ module.exports = class User extends Model {
|
||||
}
|
||||
usrData.password = newPassword
|
||||
}
|
||||
if (!_.isEmpty(groups)) {
|
||||
if (_.isArray(groups)) {
|
||||
const usrGroupsRaw = await usr.$relatedQuery('groups')
|
||||
const usrGroups = _.map(usrGroupsRaw, 'id')
|
||||
// Relate added groups
|
||||
|
@@ -3,7 +3,7 @@ title: Local
|
||||
description: Built-in authentication for Wiki.js
|
||||
author: requarks.io
|
||||
logo: https://static.requarks.io/logo/wikijs.svg
|
||||
color: yellow darken-3
|
||||
color: primary
|
||||
website: https://wiki.js.org
|
||||
isAvailable: true
|
||||
useForm: true
|
||||
|
@@ -2,25 +2,11 @@ extends master.pug
|
||||
|
||||
block body
|
||||
#root.is-fullscreen
|
||||
v-app(dark)
|
||||
.app-error
|
||||
v-container
|
||||
.pt-5
|
||||
v-layout(row)
|
||||
v-flex(xs10)
|
||||
a(href='/'): img(src='/svg/logo-wikijs.svg')
|
||||
v-flex.text-right(xs2)
|
||||
v-btn(href='/', depressed, color='red darken-3')
|
||||
v-icon(left) home
|
||||
span Home
|
||||
v-alert(color='grey', outline, :value='true', icon='error')
|
||||
strong.red--text.text--lighten-3 Oops, something went wrong...
|
||||
.body-1.red--text.text--lighten-2= message
|
||||
.app-error
|
||||
a(href='/')
|
||||
img(src='/svg/logo-wikijs.svg')
|
||||
strong Oops, something went wrong...
|
||||
span= message
|
||||
|
||||
if error.stack
|
||||
v-expansion-panel.mt-5
|
||||
v-expansion-panel-content.red.darken-3(:value='true')
|
||||
div(slot='header') View Debug Trace
|
||||
v-card(color='grey darken-4')
|
||||
v-card-text
|
||||
pre: code #{error.stack}
|
||||
if error.stack
|
||||
pre: code #{error.stack}
|
||||
|
@@ -32,7 +32,7 @@ html
|
||||
link(
|
||||
type='text/css'
|
||||
rel='stylesheet'
|
||||
href='https://use.fontawesome.com/releases/v5.9.0/css/all.css'
|
||||
href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
|
||||
)
|
||||
else if config.theming.iconset === 'fa4'
|
||||
link(
|
||||
|
@@ -36,7 +36,7 @@ html(lang=siteConfig.lang)
|
||||
link(
|
||||
type='text/css'
|
||||
rel='stylesheet'
|
||||
href='https://use.fontawesome.com/releases/v5.9.0/css/all.css'
|
||||
href='https://use.fontawesome.com/releases/v5.10.0/css/all.css'
|
||||
)
|
||||
else if config.theming.iconset === 'fa4'
|
||||
link(
|
||||
|
Reference in New Issue
Block a user