From c40b95f424ad7110aef13b10725a3e2a900cf42e Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+Berry-13@users.noreply.github.com> Date: Fri, 18 Aug 2023 16:11:00 +0200 Subject: [PATCH] feat: Disable Registration with social login (#813) * Google, Github and Discord * update .env.example with ALLOW_SOCIAL_REGISTRATION * fix some conflict * refactor strategy * Update user_auth_system.md * Update user_auth_system.md --- .env.example | 3 + api/server/index.js | 59 ++++++++-------- api/strategies/discordStrategy.js | 89 ++++++++++++----------- api/strategies/facebookStrategy.js | 72 ++++++++++--------- api/strategies/githubStrategy.js | 72 +++++++++---------- api/strategies/googleStrategy.js | 67 +++++++++--------- api/strategies/localStrategy.js | 110 +++++++++++++++-------------- docs/install/user_auth_system.md | 4 ++ 8 files changed, 252 insertions(+), 224 deletions(-) diff --git a/.env.example b/.env.example index 251ddfb8a0..601f126b58 100644 --- a/.env.example +++ b/.env.example @@ -203,6 +203,9 @@ ALLOW_REGISTRATION=true # Allow Social Registration ALLOW_SOCIAL_LOGIN=false +# Allow Social Registration (WORKS ONLY for Google, Github, Discord) +ALLOW_SOCIAL_REGISTRATION=false + # JWT Secrets JWT_SECRET=secret JWT_REFRESH_SECRET=secret diff --git a/api/server/index.js b/api/server/index.js index 6ff02656a3..e54700c812 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -49,34 +49,37 @@ config.validate(); // Validate the config app.use(passport.initialize()); passport.use(await jwtLogin()); passport.use(await passportLogin()); - if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { - passport.use(await googleLogin()); - } - if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) { - passport.use(await facebookLogin()); - } - if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { - passport.use(await githubLogin()); - } - if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) { - passport.use(await discordLogin()); - } - if ( - process.env.OPENID_CLIENT_ID && - process.env.OPENID_CLIENT_SECRET && - process.env.OPENID_ISSUER && - process.env.OPENID_SCOPE && - process.env.OPENID_SESSION_SECRET - ) { - app.use( - session({ - secret: process.env.OPENID_SESSION_SECRET, - resave: false, - saveUninitialized: false, - }), - ); - app.use(passport.session()); - await setupOpenId(); + + if (process.env.ALLOW_SOCIAL_LOGIN === 'true') { + if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) { + passport.use(await googleLogin()); + } + if (process.env.FACEBOOK_CLIENT_ID && process.env.FACEBOOK_CLIENT_SECRET) { + passport.use(await facebookLogin()); + } + if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { + passport.use(await githubLogin()); + } + if (process.env.DISCORD_CLIENT_ID && process.env.DISCORD_CLIENT_SECRET) { + passport.use(await discordLogin()); + } + if ( + process.env.OPENID_CLIENT_ID && + process.env.OPENID_CLIENT_SECRET && + process.env.OPENID_ISSUER && + process.env.OPENID_SCOPE && + process.env.OPENID_SESSION_SECRET + ) { + app.use( + session({ + secret: process.env.OPENID_SESSION_SECRET, + resave: false, + saveUninitialized: false, + }), + ); + app.use(passport.session()); + await setupOpenId(); + } } app.use('/oauth', routes.oauth); // api endpoint diff --git a/api/strategies/discordStrategy.js b/api/strategies/discordStrategy.js index 3b4c1513d5..8bdb2bb2c8 100644 --- a/api/strategies/discordStrategy.js +++ b/api/strategies/discordStrategy.js @@ -3,51 +3,58 @@ const User = require('../models/User'); const config = require('../../config/loader'); const domains = config.domains; -const discordLogin = async () => +const discordLogin = async (accessToken, refreshToken, profile, cb) => { + try { + const email = profile.email; + const discordId = profile.id; + const oldUser = await User.findOne({ + email, + }); + const ALLOW_SOCIAL_REGISTRATION = + process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + let avatarURL; + if (profile.avatar) { + const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; + avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; + } else { + const defaultAvatarNum = Number(profile.discriminator) % 5; + avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; + } + + if (oldUser) { + oldUser.avatar = avatarURL; + await oldUser.save(); + return cb(null, oldUser); + } else if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await new User({ + provider: 'discord', + discordId, + username: profile.username, + email, + name: profile.global_name, + avatar: avatarURL, + }).save(); + + return cb(null, newUser); + } + + return cb(null, false, { + message: 'User not found.', + }); + } catch (err) { + console.error(err); + return cb(err); + } +}; + +module.exports = () => new DiscordStrategy( { clientID: process.env.DISCORD_CLIENT_ID, clientSecret: process.env.DISCORD_CLIENT_SECRET, callbackURL: `${domains.server}${process.env.DISCORD_CALLBACK_URL}`, - scope: ['identify', 'email'], // Request scopes - authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none', // Add the prompt query parameter - }, - async (accessToken, refreshToken, profile, cb) => { - try { - const email = profile.email; - const discordId = profile.id; - - let avatarURL; - if (profile.avatar) { - const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'; - avatarURL = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`; - } else { - const defaultAvatarNum = Number(profile.discriminator) % 5; - avatarURL = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNum}.png`; - } - - const oldUser = await User.findOne({ email }); - if (oldUser) { - oldUser.avatar = avatarURL; - await oldUser.save(); - return cb(null, oldUser); - } - - const newUser = await User.create({ - provider: 'discord', - discordId, - username: profile.username, - email, - name: profile.global_name, - avatar: avatarURL, - }); - - cb(null, newUser); - } catch (err) { - console.error(err); - cb(err); - } + scope: ['identify', 'email'], + authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none', }, + discordLogin, ); - -module.exports = discordLogin; diff --git a/api/strategies/facebookStrategy.js b/api/strategies/facebookStrategy.js index 91afda7e02..92ad853645 100644 --- a/api/strategies/facebookStrategy.js +++ b/api/strategies/facebookStrategy.js @@ -3,8 +3,44 @@ const User = require('../models/User'); const config = require('../../config/loader'); const domains = config.domains; -// facebook strategy -const facebookLogin = async () => +const facebookLogin = async (accessToken, refreshToken, profile, cb) => { + try { + console.log('facebookLogin => profile', profile); + const email = profile.emails[0].value; + const facebookId = profile.id; + const oldUser = await User.findOne({ + email, + }); + const ALLOW_SOCIAL_REGISTRATION = + process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; + + if (oldUser) { + oldUser.avatar = profile.photos[0].value; + await oldUser.save(); + return cb(null, oldUser); + } else if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await new User({ + provider: 'facebook', + facebookId, + username: profile.name.givenName + profile.name.familyName, + email, + name: profile.displayName, + avatar: profile.photos[0].value, + }).save(); + + return cb(null, newUser); + } + + return cb(null, false, { + message: 'User not found.', + }); + } catch (err) { + console.error(err); + return cb(err); + } +}; + +module.exports = () => new FacebookStrategy( { clientID: process.env.FACEBOOK_APP_ID, @@ -25,35 +61,5 @@ const facebookLogin = async () => // 'picture.type(large)' // ] }, - async (accessToken, refreshToken, profile, done) => { - console.log('facebookLogin => profile', profile); - try { - const oldUser = await User.findOne({ email: profile.emails[0].value }); - - if (oldUser) { - console.log('FACEBOOK LOGIN => found user', oldUser); - return done(null, oldUser); - } - } catch (err) { - console.log(err); - } - - // register user - try { - const newUser = await new User({ - provider: 'facebook', - facebookId: profile.id, - username: profile.name.givenName + profile.name.familyName, - email: profile.emails[0].value, - name: profile.displayName, - avatar: profile.photos[0].value, - }).save(); - - done(null, newUser); - } catch (err) { - console.log(err); - } - }, + facebookLogin, ); - -module.exports = facebookLogin; diff --git a/api/strategies/githubStrategy.js b/api/strategies/githubStrategy.js index e663e92cac..936a252597 100644 --- a/api/strategies/githubStrategy.js +++ b/api/strategies/githubStrategy.js @@ -1,49 +1,49 @@ const { Strategy: GitHubStrategy } = require('passport-github2'); +const User = require('../models/User'); const config = require('../../config/loader'); const domains = config.domains; -const User = require('../models/User'); +const githubLogin = async (accessToken, refreshToken, profile, cb) => { + try { + const email = profile.emails[0].value; + const githubId = profile.id; + const oldUser = await User.findOne({ email }); + const ALLOW_SOCIAL_REGISTRATION = + process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; -// GitHub strategy -const githubLogin = async () => + if (oldUser) { + oldUser.avatar = profile.photos[0].value; + await oldUser.save(); + return cb(null, oldUser); + } else if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await new User({ + provider: 'github', + githubId, + username: profile.username, + email, + emailVerified: profile.emails[0].verified, + name: profile.displayName, + avatar: profile.photos[0].value, + }).save(); + + return cb(null, newUser); + } + + return cb(null, false, { message: 'User not found.' }); + } catch (err) { + console.error(err); + return cb(err); + } +}; + +module.exports = () => new GitHubStrategy( { clientID: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, callbackURL: `${domains.server}${process.env.GITHUB_CALLBACK_URL}`, proxy: false, - scope: ['user:email'], // Request email scope - }, - async (accessToken, refreshToken, profile, cb) => { - try { - let email; - if (profile.emails && profile.emails.length > 0) { - email = profile.emails[0].value; - } - - const oldUser = await User.findOne({ email }); - if (oldUser) { - oldUser.avatar = profile.photos[0].value; - await oldUser.save(); - return cb(null, oldUser); - } - - const newUser = await new User({ - provider: 'github', - githubId: profile.id, - username: profile.username, - email, - emailVerified: profile.emails[0].verified, - name: profile.displayName, - avatar: profile.photos[0].value, - }).save(); - - cb(null, newUser); - } catch (err) { - console.error(err); - cb(err); - } + scope: ['user:email'], }, + githubLogin, ); - -module.exports = githubLogin; diff --git a/api/strategies/googleStrategy.js b/api/strategies/googleStrategy.js index c44d2ef733..0870e23fee 100644 --- a/api/strategies/googleStrategy.js +++ b/api/strategies/googleStrategy.js @@ -1,11 +1,42 @@ const { Strategy: GoogleStrategy } = require('passport-google-oauth20'); +const User = require('../models/User'); const config = require('../../config/loader'); const domains = config.domains; -const User = require('../models/User'); +const googleLogin = async (accessToken, refreshToken, profile, cb) => { + try { + const email = profile.emails[0].value; + const googleId = profile.id; + const oldUser = await User.findOne({ email }); + const ALLOW_SOCIAL_REGISTRATION = + process.env.ALLOW_SOCIAL_REGISTRATION?.toLowerCase() === 'true'; -// google strategy -const googleLogin = async () => + if (oldUser) { + oldUser.avatar = profile.photos[0].value; + await oldUser.save(); + return cb(null, oldUser); + } else if (ALLOW_SOCIAL_REGISTRATION) { + const newUser = await new User({ + provider: 'google', + googleId, + username: profile.name.givenName, + email, + emailVerified: profile.emails[0].verified, + name: `${profile.name.givenName} ${profile.name.familyName}`, + avatar: profile.photos[0].value, + }).save(); + + return cb(null, newUser); + } + + return cb(null, false, { message: 'User not found.' }); + } catch (err) { + console.error(err); + return cb(err); + } +}; + +module.exports = () => new GoogleStrategy( { clientID: process.env.GOOGLE_CLIENT_ID, @@ -13,33 +44,5 @@ const googleLogin = async () => callbackURL: `${domains.server}${process.env.GOOGLE_CALLBACK_URL}`, proxy: true, }, - async (accessToken, refreshToken, profile, cb) => { - try { - const oldUser = await User.findOne({ email: profile.emails[0].value }); - if (oldUser) { - oldUser.avatar = profile.photos[0].value; - await oldUser.save(); - return cb(null, oldUser); - } - } catch (err) { - console.log(err); - } - - try { - const newUser = await new User({ - provider: 'google', - googleId: profile.id, - username: profile.name.givenName, - email: profile.emails[0].value, - emailVerified: profile.emails[0].verified, - name: `${profile.name.givenName} ${profile.name.familyName}`, - avatar: profile.photos[0].value, - }).save(); - cb(null, newUser); - } catch (err) { - console.log(err); - } - }, + googleLogin, ); - -module.exports = googleLogin; diff --git a/api/strategies/localStrategy.js b/api/strategies/localStrategy.js index 014f1cb751..e145796cb2 100644 --- a/api/strategies/localStrategy.js +++ b/api/strategies/localStrategy.js @@ -1,10 +1,62 @@ -const PassportLocalStrategy = require('passport-local').Strategy; - +const { Strategy: PassportLocalStrategy } = require('passport-local'); const User = require('../models/User'); const { loginSchema } = require('./validators'); const DebugControl = require('../utils/debug.js'); -const passportLogin = async () => +async function validateLoginRequest(req) { + const { error } = loginSchema.validate(req.body); + return error ? error.details[0].message : null; +} + +async function findUserByEmail(email) { + return User.findOne({ email: email.trim() }); +} + +async function comparePassword(user, password) { + return new Promise((resolve, reject) => { + user.comparePassword(password, function (err, isMatch) { + if (err) { + return reject(err); + } + resolve(isMatch); + }); + }); +} + +async function passportLogin(req, email, password, done) { + try { + const validationError = await validateLoginRequest(req); + if (validationError) { + logError('Passport Local Strategy - Validation Error', { reqBody: req.body }); + return done(null, false, { message: validationError }); + } + + const user = await findUserByEmail(email); + if (!user) { + logError('Passport Local Strategy - User Not Found', { email }); + return done(null, false, { message: 'Email does not exist.' }); + } + + const isMatch = await comparePassword(user, password); + if (!isMatch) { + logError('Passport Local Strategy - Password does not match', { isMatch }); + return done(null, false, { message: 'Incorrect password.' }); + } + + return done(null, user); + } catch (err) { + return done(err); + } +} + +function logError(title, parameters) { + DebugControl.log.functionName(title); + if (parameters) { + DebugControl.log.parameters(parameters); + } +} + +module.exports = () => new PassportLocalStrategy( { usernameField: 'email', @@ -12,55 +64,5 @@ const passportLogin = async () => session: false, passReqToCallback: true, }, - async (req, email, password, done) => { - const { error } = loginSchema.validate(req.body); - if (error) { - log({ - title: 'Passport Local Strategy - Validation Error', - parameters: [{ name: 'req.body', value: req.body }], - }); - return done(null, false, { message: error.details[0].message }); - } - - try { - const user = await User.findOne({ email: email.trim() }); - if (!user) { - log({ - title: 'Passport Local Strategy - User Not Found', - parameters: [{ name: 'email', value: email }], - }); - return done(null, false, { message: 'Email does not exists.' }); - } - - user.comparePassword(password, function (err, isMatch) { - if (err) { - log({ - title: 'Passport Local Strategy - Compare password error', - parameters: [{ name: 'error', value: err }], - }); - return done(err); - } - if (!isMatch) { - log({ - title: 'Passport Local Strategy - Password does not match', - parameters: [{ name: 'isMatch', value: isMatch }], - }); - return done(null, false, { message: 'Incorrect password.' }); - } - - return done(null, user); - }); - } catch (err) { - return done(err); - } - }, + passportLogin, ); - -function log({ title, parameters }) { - DebugControl.log.functionName(title); - if (parameters) { - DebugControl.log.parameters(parameters); - } -} - -module.exports = passportLogin; diff --git a/docs/install/user_auth_system.md b/docs/install/user_auth_system.md index d46f4d8ab5..b5f7075390 100644 --- a/docs/install/user_auth_system.md +++ b/docs/install/user_auth_system.md @@ -173,6 +173,10 @@ NOTE: The variable EMAIL_FROM currently does not work. To stay updated, check th To disable or re-enable registration, open up the root `.env` file and set `ALLOW_REGISTRATION=true` or `ALLOW_REGISTRATION=false` depending on if you want registration open or closed. +To disable or re-enable social registration, open up the root `.env` file and set `ALLOW_SOCIAL_REGISTRATION=true` or `ALLOW_SOCIAL_REGISTRATION=false` depending on if you want social registration open or closed. + +**NOTE: OpenID does not support the ability to disable only registration.** + ### ⚠️***Warning*** If you previously implemented your own user system using the original scaffolding that was provided, you will no longer see conversations and presets by switching to the new user system. This is because of a design flaw in the scaffolding implementation that was problematic for the inclusion of social login.