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
This commit is contained in:
Marco Beretta 2023-08-18 16:11:00 +02:00 committed by GitHub
parent 46ed5aaccd
commit c40b95f424
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 224 deletions

View file

@ -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

View file

@ -49,6 +49,8 @@ config.validate(); // Validate the config
app.use(passport.initialize());
passport.use(await jwtLogin());
passport.use(await passportLogin());
if (process.env.ALLOW_SOCIAL_LOGIN === 'true') {
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
passport.use(await googleLogin());
}
@ -78,6 +80,7 @@ config.validate(); // Validate the config
app.use(passport.session());
await setupOpenId();
}
}
app.use('/oauth', routes.oauth);
// api endpoint
app.use('/api/auth', routes.auth);

View file

@ -3,20 +3,15 @@ const User = require('../models/User');
const config = require('../../config/loader');
const domains = config.domains;
const discordLogin = async () =>
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) => {
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';
@ -26,28 +21,40 @@ const discordLogin = async () =>
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({
} else if (ALLOW_SOCIAL_REGISTRATION) {
const newUser = await new User({
provider: 'discord',
discordId,
username: profile.username,
email,
name: profile.global_name,
avatar: avatarURL,
});
}).save();
cb(null, newUser);
return cb(null, newUser);
}
return cb(null, false, {
message: 'User not found.',
});
} catch (err) {
console.error(err);
cb(err);
return cb(err);
}
},
);
};
module.exports = discordLogin;
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'],
authorizationURL: 'https://discord.com/api/oauth2/authorize?prompt=none',
},
discordLogin,
);

View file

@ -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;

View file

@ -1,36 +1,24 @@
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');
// GitHub strategy
const githubLogin = async () =>
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) => {
const githubLogin = async (accessToken, refreshToken, profile, cb) => {
try {
let email;
if (profile.emails && profile.emails.length > 0) {
email = profile.emails[0].value;
}
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';
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: profile.id,
githubId,
username: profile.username,
email,
emailVerified: profile.emails[0].verified,
@ -38,12 +26,24 @@ const githubLogin = async () =>
avatar: profile.photos[0].value,
}).save();
cb(null, newUser);
return cb(null, newUser);
}
return cb(null, false, { message: 'User not found.' });
} catch (err) {
console.error(err);
cb(err);
return cb(err);
}
},
);
};
module.exports = githubLogin;
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'],
},
githubLogin,
);

View file

@ -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;

View file

@ -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;

View file

@ -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.