diff --git a/client/components/admin/admin-auth.vue b/client/components/admin/admin-auth.vue index 96ae68fc..2d86e3b6 100644 --- a/client/components/admin/admin-auth.vue +++ b/client/components/admin/admin-auth.vue @@ -183,8 +183,8 @@ .body-2 Allowed Web Origins .body-1 {{host}} v-divider.my-3 - .body-2 Callback URL - .body-1 {{host}}/login/callback/{{strategy.key}} + .body-2 Callback URL / Redirect URI + .body-1 {{host}}/login/{{strategy.key}}/callback v-divider.my-3 .body-2 Login URL .body-1 {{host}}/login diff --git a/client/components/common/nav-header.vue b/client/components/common/nav-header.vue index aed52948..2a7ae6d8 100644 --- a/client/components/common/nav-header.vue +++ b/client/components/common/nav-header.vue @@ -143,8 +143,10 @@ span Admin v-menu(v-if='isAuthenticated', offset-y, min-width='300', left) v-tooltip(bottom, slot='activator') - v-btn.btn-animate-grow(icon, slot='activator', outline, color='blue') - v-icon(color='grey') account_circle + v-btn(icon, slot='activator', outline, color='blue') + v-icon(v-if='picture.kind === `initials`', color='grey') account_circle + v-avatar(v-else-if='picture.kind === `image`', :size='29') + v-img(:src='picture.url') span Account v-list.py-0 v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`') diff --git a/package.json b/package.json index 67d5cdeb..a67ba1a2 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node": ">=10.12" }, "dependencies": { + "@aoberoi/passport-slack": "1.0.5", "@bugsnag/js": "5.2.0", "algoliasearch": "3.32.1", "apollo-fetch": "0.7.0", @@ -135,7 +136,6 @@ "passport-okta-oauth": "0.0.1", "passport-openidconnect": "0.0.2", "passport-saml": "1.0.0", - "passport-slack": "0.0.7", "passport-twitch": "1.0.3", "passport-windowslive": "1.0.2", "pem-jwk": "2.0.0", diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 4f48ba59..edf0b7c0 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -27,7 +27,8 @@ router.get('/login/:strategy/callback', async (req, res, next) => { const authResult = await WIKI.models.users.login({ strategy: req.params.strategy }, { req, res }) - console.info(authResult) + res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() }) + res.redirect('/') } catch (err) { next(err) } diff --git a/server/core/auth.js b/server/core/auth.js index a07b0314..e71e917a 100644 --- a/server/core/auth.js +++ b/server/core/auth.js @@ -78,17 +78,12 @@ module.exports = { stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback` strategy.init(passport, stg.config) + strategy.config = stg.config - try { - strategy.icon = await fs.readFile(path.join(WIKI.ROOTPATH, `assets/svg/auth-icon-${strategy.key}.svg`), 'utf8') - } catch (err) { - if (err.code === 'ENOENT') { - strategy.icon = '[missing icon]' - } else { - WIKI.logger.warn(err) - } + WIKI.auth.strategies[stg.key] = { + ...strategy, + ...stg } - WIKI.auth.strategies[stg.key] = strategy WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`) } } catch (err) { diff --git a/server/models/users.js b/server/models/users.js index b29556a1..e22d83a8 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -19,13 +19,13 @@ module.exports = class User extends Model { static get jsonSchema () { return { type: 'object', - required: ['email', 'name', 'provider'], + required: ['email'], properties: { id: {type: 'integer'}, email: {type: 'string', format: 'email'}, name: {type: 'string', minLength: 1, maxLength: 255}, - providerId: {type: 'number'}, + providerId: {type: 'string'}, password: {type: 'string'}, role: {type: 'string', enum: ['admin', 'guest', 'user']}, tfaIsActive: {type: 'boolean', default: false}, @@ -154,8 +154,17 @@ module.exports = class User extends Model { // Model Methods // ------------------------------------------------ - static async processProfile({ profile, provider }) { - // -> Parse email + static async processProfile({ profile, providerKey }) { + const provider = _.get(WIKI.auth.strategies, providerKey, {}) + provider.info = _.find(WIKI.data.authentication, ['key', providerKey]) + + // Find existing user + let user = await WIKI.models.users.query().findOne({ + providerId: profile.id, + providerKey + }) + + // Parse email let primaryEmail = '' if (_.isArray(profile.emails)) { const e = _.find(profile.emails, ['primary', true]) @@ -167,50 +176,75 @@ module.exports = class User extends Model { } else if (profile.user && profile.user.email && profile.user.email.length > 5) { primaryEmail = profile.user.email } else { - return Promise.reject(new Error('Missing or invalid email address from profile.')) + throw new Error('Missing or invalid email address from profile.') } primaryEmail = _.toLower(primaryEmail) - // -> Find user - let user = await WIKI.models.users.query().findOne({ - email: primaryEmail, - providerKey: provider - }) - if (user) { - user.$query().patchAdnFetch({ - email: primaryEmail, - providerKey: provider, - providerId: profile.id, - name: _.get(profile, 'displayName', primaryEmail.split('@')[0]) - }) + // Parse display name + let displayName = '' + if (_.isString(profile.displayName) && profile.displayName.length > 0) { + displayName = profile.displayName + } else if (_.isString(profile.name) && profile.name.length > 0) { + displayName = profile.name } else { - // user = await WIKI.models.users.query().insertAndFetch({ - // email: primaryEmail, - // providerKey: provider, - // providerId: profile.id, - // name: profile.displayName || _.split(primaryEmail, '@')[0] - // }) + displayName = primaryEmail.split('@')[0] } - // Handle unregistered accounts - // if (!user && profile.provider !== 'local' && (WIKI.config.auth.defaultReadAccess || profile.provider === 'ldap' || profile.provider === 'azure')) { - // let nUsr = { - // email: primaryEmail, - // provider: profile.provider, - // providerId: profile.id, - // password: '', - // name: profile.displayName || profile.name || profile.cn, - // rights: [{ - // role: 'read', - // path: '/', - // exact: false, - // deny: false - // }] - // } - // return WIKI.models.users.query().insert(nUsr) - // } + // Parse picture URL + let pictureUrl = _.get(profile, 'picture', _.get(user, 'pictureUrl', null)) - return user + // Update existing user + if (user) { + if (!user.isActive) { + throw new WIKI.Error.AuthAccountBanned() + } + if (user.isSystem) { + throw new Error('This is a system reserved account and cannot be used.') + } + + user = await user.$query().patchAndFetch({ + email: primaryEmail, + name: displayName, + pictureUrl: pictureUrl + }) + + return user + } + + // Self-registration + if (provider.selfRegistration) { + // Check if email domain is whitelisted + if (_.get(provider, 'domainWhitelist', []).length > 0) { + const emailDomain = _.last(primaryEmail.split('@')) + if (!_.includes(provider.domainWhitelist, emailDomain)) { + throw new WIKI.Error.AuthRegistrationDomainUnauthorized() + } + } + + // Create account + user = await WIKI.models.users.query().insertAndFetch({ + providerKey: providerKey, + providerId: profile.id, + email: primaryEmail, + name: displayName, + pictureUrl: pictureUrl, + localeCode: WIKI.config.lang.code, + defaultEditor: 'markdown', + tfaIsActive: false, + isSystem: false, + isActive: true, + isVerified: true + }) + + // Assign to group(s) + if (provider.autoEnrollGroups.length > 0) { + await user.$relatedQuery('groups').relate(provider.autoEnrollGroups) + } + + return user + } + + throw new Error('You are not authorized to login.') } static async login (opts, context) { @@ -227,7 +261,7 @@ module.exports = class User extends Model { return new Promise((resolve, reject) => { WIKI.auth.passport.authenticate(opts.strategy, { session: !strInfo.useForm, - scope: strInfo.scopes ? strInfo.scopes.join(' ') : null + scope: strInfo.scopes ? strInfo.scopes : null }, async (err, user, info) => { if (err) { return reject(err) } if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) } diff --git a/server/modules/authentication/auth0/authentication.js b/server/modules/authentication/auth0/authentication.js index 13125c2d..6d820899 100644 --- a/server/modules/authentication/auth0/authentication.js +++ b/server/modules/authentication/auth0/authentication.js @@ -15,9 +15,8 @@ module.exports = { clientSecret: conf.clientSecret, callbackURL: conf.callbackURL }, async (accessToken, refreshToken, extraParams, profile, cb) => { - console.info(accessToken, refreshToken, extraParams, profile) try { - const user = WIKI.models.users.processProfile({ profile, provider: 'auth0' }) + const user = await WIKI.models.users.processProfile({ profile, providerKey: 'auth0' }) cb(null, user) } catch (err) { cb(err, null) diff --git a/server/modules/authentication/discord/authentication.js b/server/modules/authentication/discord/authentication.js index 910bebfd..7295679e 100644 --- a/server/modules/authentication/discord/authentication.js +++ b/server/modules/authentication/discord/authentication.js @@ -5,6 +5,7 @@ // ------------------------------------ const DiscordStrategy = require('passport-discord').Strategy +const _ = require('lodash') module.exports = { init (passport, conf) { @@ -14,12 +15,20 @@ module.exports = { clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, scope: 'identify email' - }, function (accessToken, refreshToken, profile, cb) { - WIKI.models.users.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) + }, async (accessToken, refreshToken, profile, cb) => { + try { + const user = await WIKI.models.users.processProfile({ + profile: { + ...profile, + displayName: profile.username, + picture: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png` + }, + providerKey: 'discord' + }) + cb(null, user) + } catch (err) { + cb(err, null) + } } )) } diff --git a/server/modules/authentication/discord/definition.yml b/server/modules/authentication/discord/definition.yml index 192595e6..0436c067 100644 --- a/server/modules/authentication/discord/definition.yml +++ b/server/modules/authentication/discord/definition.yml @@ -5,7 +5,16 @@ author: requarks.io logo: https://static.requarks.io/logo/discord.svg color: indigo lighten-2 website: https://discordapp.com/ +isAvailable: true useForm: false props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2 diff --git a/server/modules/authentication/facebook/definition.yml b/server/modules/authentication/facebook/definition.yml index 1d3d739b..20bb9fbe 100644 --- a/server/modules/authentication/facebook/definition.yml +++ b/server/modules/authentication/facebook/definition.yml @@ -5,7 +5,16 @@ author: requarks.io logo: https://static.requarks.io/logo/facebook.svg color: indigo website: https://facebook.com/ +isAvailable: false useForm: false props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2 diff --git a/server/modules/authentication/firebase/authentication.js b/server/modules/authentication/firebase/authentication.js new file mode 100644 index 00000000..721d7da0 --- /dev/null +++ b/server/modules/authentication/firebase/authentication.js @@ -0,0 +1,34 @@ +/* global WIKI */ + +// ------------------------------------ +// GitHub Account +// ------------------------------------ + +const GitHubStrategy = require('passport-github2').Strategy +const _ = require('lodash') + +module.exports = { + init (passport, conf) { + passport.use('github', + new GitHubStrategy({ + clientID: conf.clientId, + clientSecret: conf.clientSecret, + callbackURL: conf.callbackURL, + scope: ['user:email'] + }, async (accessToken, refreshToken, profile, cb) => { + try { + const user = await WIKI.models.users.processProfile({ + profile: { + ...profile, + picture: _.get(profile, 'photos[0].value', '') + }, + providerKey: 'github' + }) + cb(null, user) + } catch (err) { + cb(err, null) + } + } + )) + } +} diff --git a/server/modules/authentication/firebase/definition.yml b/server/modules/authentication/firebase/definition.yml new file mode 100644 index 00000000..b8efe789 --- /dev/null +++ b/server/modules/authentication/firebase/definition.yml @@ -0,0 +1,11 @@ +key: firebase +title: Firebase +description: Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business. +author: requarks.io +logo: https://static.requarks.io/logo/firebase.svg +color: yellow darken-3 +website: https://firebase.google.com/ +isAvailable: false +useForm: false +props: {} + diff --git a/server/modules/authentication/github/authentication.js b/server/modules/authentication/github/authentication.js index d421665a..721d7da0 100644 --- a/server/modules/authentication/github/authentication.js +++ b/server/modules/authentication/github/authentication.js @@ -5,6 +5,7 @@ // ------------------------------------ const GitHubStrategy = require('passport-github2').Strategy +const _ = require('lodash') module.exports = { init (passport, conf) { @@ -14,12 +15,19 @@ module.exports = { clientSecret: conf.clientSecret, callbackURL: conf.callbackURL, scope: ['user:email'] - }, (accessToken, refreshToken, profile, cb) => { - WIKI.models.users.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) + }, async (accessToken, refreshToken, profile, cb) => { + try { + const user = await WIKI.models.users.processProfile({ + profile: { + ...profile, + picture: _.get(profile, 'photos[0].value', '') + }, + providerKey: 'github' + }) + cb(null, user) + } catch (err) { + cb(err, null) + } } )) } diff --git a/server/modules/authentication/github/definition.yml b/server/modules/authentication/github/definition.yml index ed47883e..6af5e24f 100644 --- a/server/modules/authentication/github/definition.yml +++ b/server/modules/authentication/github/definition.yml @@ -5,7 +5,17 @@ author: requarks.io logo: https://static.requarks.io/logo/github.svg color: grey darken-3 website: https://github.com +isAvailable: true useForm: false props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2 + diff --git a/server/modules/authentication/google/definition.yml b/server/modules/authentication/google/definition.yml index 6b3125bc..80a7a92d 100644 --- a/server/modules/authentication/google/definition.yml +++ b/server/modules/authentication/google/definition.yml @@ -5,7 +5,16 @@ author: requarks.io logo: https://static.requarks.io/logo/google.svg color: red darken-1 website: https://console.developers.google.com/ +isAvailable: false useForm: false props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2 diff --git a/server/modules/authentication/microsoft/authentication.js b/server/modules/authentication/microsoft/authentication.js index 843a5cb0..dc7b37d8 100644 --- a/server/modules/authentication/microsoft/authentication.js +++ b/server/modules/authentication/microsoft/authentication.js @@ -13,12 +13,20 @@ module.exports = { clientID: conf.clientId, clientSecret: conf.clientSecret, callbackURL: conf.callbackURL - }, function (accessToken, refreshToken, profile, cb) { - WIKI.models.users.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) + }, async (accessToken, refreshToken, profile, cb) => { + console.info(profile) + try { + const user = await WIKI.models.users.processProfile({ + profile: { + ...profile, + picture: _.get(profile, 'photos[0].value', '') + }, + providerKey: 'microsoft' + }) + cb(null, user) + } catch (err) { + cb(err, null) + } } )) } diff --git a/server/modules/authentication/microsoft/definition.yml b/server/modules/authentication/microsoft/definition.yml index ed88888f..62cd3255 100644 --- a/server/modules/authentication/microsoft/definition.yml +++ b/server/modules/authentication/microsoft/definition.yml @@ -5,7 +5,20 @@ author: requarks.io logo: https://static.requarks.io/logo/microsoft.svg color: blue website: https://apps.dev.microsoft.com/ +isAvailable: false useForm: false +scopes: + - openid + - profile + - email props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2 diff --git a/server/modules/authentication/slack/authentication.js b/server/modules/authentication/slack/authentication.js index 60e1d3fe..4d12c019 100644 --- a/server/modules/authentication/slack/authentication.js +++ b/server/modules/authentication/slack/authentication.js @@ -4,7 +4,8 @@ // Slack Account // ------------------------------------ -const SlackStrategy = require('passport-slack').Strategy +const SlackStrategy = require('@aoberoi/passport-slack').default.Strategy +const _ = require('lodash') module.exports = { init (passport, conf) { @@ -12,13 +13,21 @@ module.exports = { new SlackStrategy({ clientID: conf.clientId, clientSecret: conf.clientSecret, - callbackURL: conf.callbackURL - }, (accessToken, refreshToken, profile, cb) => { - WIKI.models.users.processProfile(profile).then((user) => { - return cb(null, user) || true - }).catch((err) => { - return cb(err, null) || true - }) + callbackURL: conf.callbackURL, + team: conf.team + }, async (accessToken, scopes, team, extra, { user: userProfile }, cb) => { + try { + const user = await WIKI.models.users.processProfile({ + profile: { + ...userProfile, + picture: _.get(userProfile, 'image_48', '') + }, + providerKey: 'slack' + }) + cb(null, user) + } catch (err) { + cb(err, null) + } } )) } diff --git a/server/modules/authentication/slack/definition.yml b/server/modules/authentication/slack/definition.yml index ab22ab03..dcd7b6b3 100644 --- a/server/modules/authentication/slack/definition.yml +++ b/server/modules/authentication/slack/definition.yml @@ -5,7 +5,25 @@ author: requarks.io logo: https://static.requarks.io/logo/slack.svg color: green website: https://api.slack.com/docs/oauth +isAvailable: true useForm: false +scope: + - identity.basic + - identity.email + - identity.avatar props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2 + team: + type: String + title: Team / Workspace ID + hint: Optional - Your unique team (workspace) identifier + order: 3 diff --git a/server/modules/authentication/twitch/definition.yml b/server/modules/authentication/twitch/definition.yml index 2a7596f5..0328dee2 100644 --- a/server/modules/authentication/twitch/definition.yml +++ b/server/modules/authentication/twitch/definition.yml @@ -5,7 +5,16 @@ author: requarks.io logo: https://static.requarks.io/logo/twitch.svg color: indigo darken-2 website: https://dev.twitch.tv/docs/authentication/ +isAvailable: false useForm: false props: - clientId: String - clientSecret: String + clientId: + type: String + title: Client ID + hint: Application Client ID + order: 1 + clientSecret: + type: String + title: Client Secret + hint: Application Client Secret + order: 2