feat: self-contained auth modules + login UI + icons

This commit is contained in:
NGPixel
2017-09-10 23:00:03 -04:00
parent 52630127cd
commit cac3d21c6e
22 changed files with 341 additions and 249 deletions

View File

@@ -8,24 +8,29 @@
const AzureAdOAuth2Strategy = require('passport-azure-ad-oauth2').Strategy
module.exports = (passport, conf) => {
const jwt = require('jsonwebtoken')
passport.use('azure_ad_oauth2',
new AzureAdOAuth2Strategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
resource: conf.resource,
tenant: conf.tenant
}, (accessToken, refreshToken, params, profile, cb) => {
let waadProfile = jwt.decode(params.id_token)
waadProfile.id = waadProfile.oid
waadProfile.provider = 'azure'
wiki.db.User.processProfile(waadProfile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'azure',
title: 'Azure Active Directory',
props: ['clientId', 'clientSecret', 'callbackURL', 'resource', 'tenant'],
init (passport, conf) {
const jwt = require('jsonwebtoken')
passport.use('azure_ad_oauth2',
new AzureAdOAuth2Strategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
resource: conf.resource,
tenant: conf.tenant
}, (accessToken, refreshToken, params, profile, cb) => {
let waadProfile = jwt.decode(params.id_token)
waadProfile.id = waadProfile.oid
waadProfile.provider = 'azure'
wiki.db.User.processProfile(waadProfile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -8,19 +8,24 @@
const FacebookStrategy = require('passport-facebook').Strategy
module.exports = (passport, conf) => {
passport.use('facebook',
new FacebookStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
profileFields: ['id', 'displayName', 'email']
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'facebook',
title: 'Facebook',
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('facebook',
new FacebookStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
profileFields: ['id', 'displayName', 'email']
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -8,19 +8,24 @@
const GitHubStrategy = require('passport-github2').Strategy
module.exports = (passport, conf) => {
passport.use('github',
new GitHubStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
scope: ['user:email']
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'github',
title: 'GitHub',
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('github',
new GitHubStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL,
scope: ['user:email']
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -8,18 +8,23 @@
const GoogleStrategy = require('passport-google-oauth20').Strategy
module.exports = (passport, conf) => {
passport.use('google',
new GoogleStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'google',
title: 'Google ID',
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('google',
new GoogleStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -9,32 +9,37 @@
const LdapStrategy = require('passport-ldapauth').Strategy
const fs = require('fs')
module.exports = (passport, conf) => {
passport.use('ldapauth',
new LdapStrategy({
server: {
url: conf.url,
bindDn: conf.bindDn,
bindCredentials: conf.bindCredentials,
searchBase: conf.searchBase,
searchFilter: conf.searchFilter,
searchAttributes: ['displayName', 'name', 'cn', 'mail'],
tlsOptions: (conf.tlsEnabled) ? {
ca: [
fs.readFileSync(conf.tlsCertPath)
]
} : {}
},
usernameField: 'email',
passReqToCallback: false
}, (profile, cb) => {
profile.provider = 'ldap'
profile.id = profile.dn
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'ldap',
title: 'LDAP / Active Directory',
props: ['url', 'bindDn', 'bindCredentials', 'searchBase', 'searchFilter', 'tlsEnabled', 'tlsCertPath'],
init (passport, conf) {
passport.use('ldapauth',
new LdapStrategy({
server: {
url: conf.url,
bindDn: conf.bindDn,
bindCredentials: conf.bindCredentials,
searchBase: conf.searchBase,
searchFilter: conf.searchFilter,
searchAttributes: ['displayName', 'name', 'cn', 'mail'],
tlsOptions: (conf.tlsEnabled) ? {
ca: [
fs.readFileSync(conf.tlsCertPath)
]
} : {}
},
usernameField: 'email',
passReqToCallback: false
}, (profile, cb) => {
profile.provider = 'ldap'
profile.id = profile.dn
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -8,25 +8,30 @@
const LocalStrategy = require('passport-local').Strategy
module.exports = (passport, conf) => {
passport.use('local',
new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (uEmail, uPassword, done) => {
wiki.db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
if (user) {
return user.validatePassword(uPassword).then(() => {
return done(null, user) || true
}).catch((err) => {
return done(err, null)
})
} else {
return done(new Error('INVALID_LOGIN'), null)
}
}).catch((err) => {
done(err, null)
})
}
))
module.exports = {
key: 'local',
title: 'Local',
props: [],
init (passport, conf) {
passport.use('local',
new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, (uEmail, uPassword, done) => {
wiki.db.User.findOne({ email: uEmail, provider: 'local' }).then((user) => {
if (user) {
return user.validatePassword(uPassword).then(() => {
return done(null, user) || true
}).catch((err) => {
return done(err, null)
})
} else {
return done(new Error('INVALID_LOGIN'), null)
}
}).catch((err) => {
done(err, null)
})
}
))
}
}

View File

@@ -8,18 +8,23 @@
const WindowsLiveStrategy = require('passport-windowslive').Strategy
module.exports = (passport, conf) => {
passport.use('windowslive',
new WindowsLiveStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'microsoft',
title: 'Microsoft Account',
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('windowslive',
new WindowsLiveStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, function (accessToken, refreshToken, profile, cb) {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -8,18 +8,23 @@
const SlackStrategy = require('passport-slack').Strategy
module.exports = (passport, conf) => {
passport.use('slack',
new SlackStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
module.exports = {
key: 'slack',
title: 'Slack',
props: ['clientId', 'clientSecret', 'callbackURL'],
init (passport, conf) {
passport.use('slack',
new SlackStrategy({
clientID: conf.clientId,
clientSecret: conf.clientSecret,
callbackURL: conf.callbackURL
}, (accessToken, refreshToken, profile, cb) => {
wiki.db.User.processProfile(profile).then((user) => {
return cb(null, user) || true
}).catch((err) => {
return cb(err, null) || true
})
}
))
}
}

View File

@@ -37,7 +37,8 @@ const bruteforce = new ExpressBrute(EBstore, {
*/
router.get('/login', function (req, res, next) {
res.render('auth/login', {
usr: res.locals.usr
authStrategies: wiki.auth.strategies,
hasMultipleStrategies: Object.keys(wiki.config.auth.strategies).length > 0
})
})

View File

@@ -16,6 +16,7 @@ module.exports = Promise.join(
// Load global modules
// ----------------------------------------
wiki.auth = require('./modules/auth').init()
wiki.disk = require('./modules/disk').init()
wiki.docs = require('./modules/documents').init()
wiki.git = require('./modules/git').init(false)
@@ -38,7 +39,6 @@ module.exports = Promise.join(
const http = require('http')
const i18nBackend = require('i18next-node-fs-backend')
const path = require('path')
const passport = require('passport')
const passportSocketIo = require('passport.socketio')
const session = require('express-session')
const SessionRedisStore = require('connect-redis')(session)
@@ -78,10 +78,6 @@ module.exports = Promise.join(
// Passport Authentication
// ----------------------------------------
require('./modules/auth').init(passport)
wiki.rights = require('./modules/rights')
// wiki.rights.init()
let sessionStore = new SessionRedisStore({
client: wiki.redis
})
@@ -95,8 +91,8 @@ module.exports = Promise.join(
saveUninitialized: false
}))
app.use(flash())
app.use(passport.initialize())
app.use(passport.session())
app.use(wiki.auth.passport.initialize())
app.use(wiki.auth.passport.session())
// ----------------------------------------
// SEO
@@ -135,6 +131,7 @@ module.exports = Promise.join(
// View accessible data
// ----------------------------------------
app.locals.basedir = wiki.ROOTPATH
app.locals._ = require('lodash')
app.locals.t = wiki.lang.t.bind(wiki.lang)
app.locals.moment = require('moment')

View File

@@ -9,7 +9,7 @@
* @return {any} void
*/
module.exports = (req, res, next) => {
res.locals.appflash = req.flash('alert')
res.locals.flash = req.flash('alert')
next()
}

View File

@@ -3,10 +3,16 @@
/* global wiki */
const _ = require('lodash')
const passport = require('passport')
const fs = require('fs-extra')
const path = require('path')
module.exports = {
init(passport) {
// Serialization user methods
strategies: {},
init() {
this.passport = passport
// Serialization user methods
passport.serializeUser(function (user, done) {
done(null, user._id)
@@ -27,20 +33,26 @@ module.exports = {
// Load authentication strategies
wiki.config.authStrategies = {
list: _.pickBy(wiki.config.auth, strategy => strategy.enabled),
socialEnabled: (_.chain(wiki.config.auth).omit('local').filter(['enabled', true]).value().length > 0)
}
_.forOwn(wiki.config.authStrategies.list, (strategyConfig, strategyName) => {
strategyConfig.callbackURL = `${wiki.config.site.host}/login/${strategyName}/callback`
require(`../authentication/${strategyName}`)(passport, strategyConfig)
wiki.logger.info(`Authentication Provider ${_.upperFirst(strategyName)}: OK`)
_.forOwn(wiki.config.auth.strategies, (strategyConfig, strategyKey) => {
strategyConfig.callbackURL = `${wiki.config.site.host}${wiki.config.site.path}/login/${strategyKey}/callback`
let strategy = require(`../authentication/${strategyKey}`)
strategy.init(passport, strategyConfig)
fs.readFile(path.join(wiki.ROOTPATH, `assets/svg/auth-icon-${strategyKey}.svg`), 'utf8').then(iconData => {
strategy.icon = iconData
}).catch(err => {
if (err.code === 'ENOENT') {
strategy.icon = '[missing icon]'
} else {
wiki.logger.error(err)
}
})
this.strategies[strategy.key] = strategy
wiki.logger.info(`Authentication Provider ${strategyKey}: OK`)
})
// Create Guest account for first-time
return wiki.db.User.findOne({
wiki.db.User.findOne({
where: {
provider: 'local',
email: 'guest@example.com'
@@ -88,5 +100,7 @@ module.exports = {
// })
// } else { return true }
// })
return this
}
}

View File

@@ -3,52 +3,30 @@ extends ../master.pug
block body
body
.login#root
.login-container
if config.authStrategies.socialEnabled
.login-container(:class={ "is-expanded": hasMultipleStrategies })
if flash.length > 0
.login-error
strong
i.icon-warning-outline
= flash[0].title
span= flash[0].message
if hasMultipleStrategies
.login-providers
button.is-active(onclick='window.location.assign("/login/ms")')
button.is-active(title=t('auth:providers.local'))
i.nc-icon-outline.ui-1_database
span= t('auth:providers.local')
if config.auth.microsoft && config.auth.microsoft.enabled
button(onclick='window.location.assign("/login/ms")')
i.icon-windows2
span= t('auth:providers.windowslive')
if config.auth.azure && config.auth.azure.enabled
button(onclick='window.location.assign("/login/azure")')
i.icon-windows2
span= t('auth:providers.azure')
if config.auth.google && config.auth.google.enabled
button(onclick='window.location.assign("/login/google")')
i.icon-google
span= t('auth:providers.google')
if config.auth.facebook && config.auth.facebook.enabled
button(onclick='window.location.assign("/login/facebook")')
i.icon-facebook
span= t('auth:providers.facebook')
if config.auth.github && config.auth.github.enabled
button(onclick='window.location.assign("/login/github")')
i.icon-github
span= t('auth:providers.github')
if config.auth.slack && config.auth.slack.enabled
button(onclick='window.location.assign("/login/slack")')
i.icon-slack
span= t('auth:providers.slack')
each strategy in authStrategies
button(onclick='window.location.assign("/login/' + strategy.key + '")', title=strategy.title)
!= strategy.icon
span= strategy.title
.login-frame
h1= config.site.title
h2= t('auth:loginrequired')
if appflash.length > 0
h3
i.icon-warning-outline
= appflash[0].title
h4= appflash[0].message
if config.auth.local.enabled
form(method='post', action='/login')
input#login-user(type='text', name='email', placeholder=t('auth:fields.emailuser'))
input#login-pass(type='password', name='password', placeholder=t('auth:fields.password'))
button.button.is-light-green.is-fullwidth(type='submit')
span= t('auth:actions.login')
form(method='post', action='/login')
input#login-user(type='text', name='email', placeholder=t('auth:fields.emailuser'))
input#login-pass(type='password', name='password', placeholder=t('auth:fields.password'))
button.button.is-light-green.is-fullwidth(type='submit')
span= t('auth:actions.login')
.login-copyright
= t('footer.poweredby') + ' '
a.icon(href='https://github.com/Requarks/wiki')
i.icon-github
a(href='https://wiki.requarks.io/') Wiki.js
= t('footer.poweredby')
a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js