I'm writing a small e-commerce app with third-party login via Google using a PostgreSQL database. This is my Google Strategy configuration:
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `${process.env.NODE_ENV === "production" ? [production URL] : "http://localhost:8000"}/api/auth/login/google/callback`,
passReqToCallback: true,
scope: ["email", "profile"]
}, db.third.login));
And this is db.third.login below, which is working perfectly with both newly registered and existing users who authenticated themselves via Google. Alongside authenticating the user, it also passes information that ultimately forces a newly registered user to set a phone number and password in the client before they can use the app.
const login = async(req, accessToken, refreshToken, profile, done) => {
// Get request IP address
const ip = requestIP.getClientIp(req);
// Generate login attempt ID
const attemptId = idGen(15);
try { // Get federated credentials
let result = await pool.query("SELECT * FROM federated_credentials WHERE id = $1 AND provider = $2", [profile.id, profile.provider]);
// Create user account if credentials don't exist
if (result.rows.length === 0) {
// Send error if email already exists in database
result = await pool.query("SELECT email FROM users WHERE email = $1", [profile.emails[0].value]);
if (result.rows.length > 0) return done({ status: 409, message: "Error: A user with the provided email already exists." });
// Generate user ID and cart ID
const userId = idGen(7);
const cartId = idGen(7);
// Generate password hash
const salt = await bcrypt.genSalt(17);
const passwordHash = await bcrypt.hash(process.env.GENERIC_PASSWORD, salt);
// Add user to database
let text = `INSERT INTO users (id, first_name, last_name, phone, email, password, created_at) VALUES ($1, $2, $3, $4, $5, $6, to_timestamp(${Date.now()} / 1000)) RETURNING id`;
let values = [userId, profile.name.givenName, profile.name.familyName, "254700000000", profile.emails[0].value, passwordHash];
result = await pool.query(text, values);
// Add user cart to database
result = await pool.query("INSERT INTO carts (id, user_id) VALUES ($1, $2)", [cartId, userId]);
// Add federated credentials to database
result = await pool.query("INSERT INTO federated_credentials (id, provider, user_id) VALUES ($1, $2, $3)", [profile.id, profile.provider, userId]);
// Add user details to be confirmed to session
const federatedCredentials = { id: profile.id, provider: profile.provider, confirm: true };
return done(null, { id: userId, email: profile.emails[0].value, role: "customer", cartId: cartId, federatedCredentials });
}
// Save federated credentials details
const federatedCredentials = { id: result.rows[0].id, provider: result.rows[0].provider, confirm: !result.rows[0].confirmed };
// Get user details
result = await pool.query("SELECT users.id AS id, users.email AS email, users.password AS password, users.role AS role, carts.id AS cart_id FROM users JOIN carts ON carts.user_id = users.id WHERE email = $1", [profile.emails[0].value]);
// Log login attempt
await loginAttempt(attemptId, ip, profile.emails[0].value, "google", true);
// Add user to session
return done(null, { id: result.rows[0].id, email: result.rows[0].email, role: result.rows[0].role, cartId: result.rows[0].cart_id, federatedCredentials });
} catch (err) {
return done({ status: 500, message: "An unknown error occurred. Kindly try again." });
}
}
I'd now like to add functionality that allows existing users (who registered otherwise) to link their Google accounts, but my problem is that req.user is apparently not accessible in passport.use() (console logs show that it is undefined even after I've logged in). How can I access information on the current user in the session in passport.use() to make this happen? (Or is there another solution altogether?)
Did some digging on the Passport repository on GitHub and found a user who reported setting the sameSite property in the express-session cookie object to "lax" as a working solution, and it worked for me as well. You can read the comment here.
const session = require("express-session");
app.use(session({
... // other session options
cookie: {
... // other cookie options
sameSite: "lax"
}
}));
Hopefully Mr. Jared Hanson can provide an update on why this is the case soon (as well as a fix that allows unconditional access to req.user from passport.use()).
Related
I am building a React App with Firebase back end.
I have two types of users, Admin and Standard users. The admins are created by a Super Admin using email and password and their phone numbers is set to their profile. The Standard user registers using phone number. I would like to send a different registration success SMS for the Admin and the standard user using Firebase cloud functions with the onCreate auth trigger.
Is there a way to tell which user was created through Admin SDK and which one registered through phone Auth.
My create Admin function is as below.
exports.addAdmin= functions.https.onCall((data, context) => {
return admin.auth().createUser({
email: data.email,
password: data.password,
displayName: data.name,
phoneNumber: data.phone,
disabled: false,
emailVerified: true
})
});
You can check if email and phoneNumber are defined in user object of onCreate() function like this:
export const onUserCreate = auth.user().onCreate(async (user) => {
if (user.phoneNumber) {
// User has phone number
}
if (user.email) {
// User has email
}
});
If both admin created and standard users can have email-password auth setup and you want check if they were created by a super admin or no, then you'll have to store the source in a database or custom claims. Try:
export const addAdmin = https.onCall(async (data, context) => {
const user = await getAuth().createUser({
email: "dharmaraj.r244#gmail.com",
password: "data.password",
});
// Add 'source' custom claim
await getAuth().setCustomUserClaims(user.uid, {
source: "admin",
});
return 'User created';
});
export const onUserCreate = auth.user().onCreate(async (user) => {
const { customClaims } = await getAuth().getUser(user.uid);
if (customClaims.source === "admin") {
console.log("User created by super admin")
}
return null;
});
I have seen some similar questions here but the answer is irrelevant to mine, as I have declared passReqToCallback.
I am trying to create an authentication system with passport. I have successfully integrated passport-local, which register/logs in user and creates a session but am having an issue in the logic of the google strategy:
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
passReqToCallback: true
},
(req, accessToken, refreshToken, profile, done) => {
/**
*
* This if statement is failing, as far as I can tell there is no req object being passed despite declaring it above
*
*/
// If user is logged in, proceed to simply link account
if (req.user) {
req.user.googleid = profile.id;
req.user.googleEmail = profile.emails[0].value;
req.user.googleDisplayName = profile.displayname;
pool.query('UPDATE users SET googleid = ?, google_email = ?, google_display_name = ? WHERE id = ?', [
req.user.googleid,
req.user.googleEmail,
req.user.googleDisplayName,
req.id
],
function(err, rows) {
// If google account is duplicate (linked to different account) will return error
if (err) {
return done(err, false, {message: "The Google account you tried to link is associated with another account."});
}
return done(null, req.user);
})
}
// Check if google account is registered
pool.query('SELECT * FROM users WHERE googleid = ?', [profile.id], function(err, rows) {
if (err) {
return done(err);
}
// If not logged in but user already registered, log in
if (rows.length) {
return done(null, rows[0]);
}
// If no existing record, register the user.
else {
let newUser = {
email: profile.emails[0].value,
// Google account specific fields
googleid: profile.id,
googleDisplayName: profile.displayName,
method: "gl", // This field ties this new user to the google account
// General fields (taken from the stuff google gives us)
firstName: profile.name.givenName,
lastName: profile.name.familyName,
googleEmail: profile.emails[0].value
}
let insertQuery = "INSERT INTO users (email, googleid, google_display_name, method, first_name, last_name, google_email) VALUES (?,?,?,?,?,?,?)";
pool.query(insertQuery, [
newUser.email,
newUser.googleid,
newUser.googleDisplayName,
newUser.method,
newUser.firstName,
newUser.lastName,
newUser.googleEmail
],
function(err, rows) {
if (err) return done(err, null);
newUser.id = rows.insertId;
return done(null, newUser);
})
}
})}));
So essentially the first if is supposed to see if the user is already authenticated and then just link the account as so. However, in practice, it will skip this even when authenticated and proceed to the rest of the logic, which works absolutely fine. It will go on to create a new account at the else statement (or return error if email is taken, I still need to implement that part).
But, interestingly, if I am logged in while using it, it doesn't log me in as the new user as it otherwise would, instead it keeps me logged in as the current session.
Where have I gone wrong? Why is the req.user not detected? I have also tried using req.isAuthenticated() with the same result.
Below is my callback route, if helpful:
// Google
router.get('/oauth/google', passport.authenticate('google', {
scope: ['email', 'profile']
}));
// Callback
router.get('/oauth/google/redirect', passport.authenticate('google', {
successRedirect: '/account',
failureFlash: 'Something went wrong. Please enable third party cookies to allow Google to sign in.',
failureRedirect: '/login'
}
));
UPDATE 1: If I try (!req.user), same result, skips to below, not sure what that means is happening
I read this interesting article about how to link accounts using passport:
https://codeburst.io/account-linking-with-passportjs-in-3-minutes-2cb1b09d4a76
passport.use(
new GitHubStrategy(
{
clientID: process.env.GithubClientID,
clientSecret: process.env.GithubClientSecret,
callbackURL: process.env.GithubCallbackURL,
passReqToCallback: true
},
(req, accessToken, refreshToken, profile, cb) => {
const { email, name, id } = profile._json
// Check if user is auth'd
if (req.user) {
// Link account
} else {
// Create new account
}
}
)
)
The interesting thing is that he check if user is logged or not. If user logged in then link user account
if (req.user) {
// Link account
} else {
// Create new account
}
I dont know why req object has user? Maybe the user comes from session but in my NodeJS application, I use token instead of session. My question is how to attach user object to req without using session (I use jwt)?
I do like this but the req.user is undefined
router
.route('/google')
.get(
checkToken(true),
passport.authenticate('google', {scope: 'profile email'}),
)
checkToken is a function that if we have valid token, then it will use userId (found in token) to get the user object and then attach it to req. Something like this
if (validToken) {
const id = fromToken(token)
const user = await User.findById(id)
req.user = user
next()
}
I am using spotify strategy with passport Js for authentication. I then want to use the spotify-web-api-node wrapper library for calls to spotify for the data. However using the access and refresh tokens gained from the authentication for the subsequent calls is not working out. I am getting a 401 - unauthorised when trying to make a call to spotify for user specific data.
I tried instantiating the SpotifyWebApiObject with the refresh token and access token. Although I can see in the library it only needs the access token to make the calls. I have tried logging in and out to get new sets of the tokens as well.
passport.use(new SpotifyStrategy({
clientID: keys.spotifyClientID,
clientSecret: keys.spotifyClientSecret,
callbackURL: '/auth/spotify/callback',
proxy: true
}, async (accessToken, refreshToken, profile, done) => {
const spotifyId = profile.id;
const name = profile.displayName;
const email = profile.emails[0].value;
console.log(profile.id)
const existingUser = await User.findOne({ spotifyId: profile.id });
if (existingUser) {
let userCredentials = await UserCredential.findOne({ userId: spotifyId });
if (!userCredentials) {
return await new UserCredential({ userId: spotifyId, name, accessToken, refreshToken }).save();
}
console.log('always get existing user')
userCredentials.accessToken = accessToken;
userCredentials.refreshToken = refreshToken;
return done(null, existingUser);
}
const user = await new User({ spotifyId }).save();
await new UserCredential({ userId: spotifyId, name, accessToken, refreshToken }).save();
done(null, user);
}));
Once they are stored in the db. I do a look up for the user and use the respective access and refresh tokens.
const getPlaylists = async (user) => {
let spotifyData = await initiateSpotifyWebApi(user);
spotifyData.getUserPlaylists(user.spotifyId).then((res) => {
console.log('response is ===', res)
}).catch((err) => console.error(err));
}
async function initiateSpotifyWebApi(user) {
const creds = await UserCredential.findOne({ userId: user.spotifyId });
const apiCaller = setUpApiObj(creds.refreshToken, creds.accessToken);
return apiCaller
}
function setUpApiObj(refreshTok, accessTok) {
const spotifyApi = new SpotifyWebApi({
accessToken: accessTok,
refreshToken: refreshTok
});
return spotifyApi;
}
getUserPlaylist()
returns an error
{ [WebapiError: Unauthorized] name: 'WebapiError', message: 'Unauthorized', statusCode: 401 }
Any idea why I cannot access the api using this library, the way I am trying?
thanks
Couple of things to check. This is just what springs to mind. Correct me if I'm on the wrong track and I'll try to help further.
Check your 'scopes' when you authenticate via Spotify. You need to have the correct scopes to perform different actions on the API. See here: https://developer.spotify.com/documentation/general/guides/authorization-guide/
If you get a 401, use your refresh token (I can see you're storing it) to automatically request, retrieve and overwrite your current AccessToken then perform the request again.
Using: passport-google-oauth2.
I want to use JWT with Google login - for that I need to disable session and somehow pass the user model back to client.
All the examples are using google callback that magically redirect to '/'.
How do I:
1. Disable session while using passport-google-oauth2.
2. res.send() user to client after google authentication.
Feel free to suggest alternatives if I'm not on the right direction.
Manage to overcome this with some insights:
1. disable session in express - just remove the middleware of the session
// app.use(session({secret: config.secret}))
2. when using Google authentication what actually happens is that there is a redirection to google login page and if login is successful it redirect you back with the url have you provided.
This actually mean that once google call your callback you cannot do res.send(token, user) - its simply does not work (anyone can elaborate why?). So you are force to do a redirect to the client by doing res.redirect("/").
But the whole purpose is to pass the token so you can also do res.redirect("/?token=" + token).
app.get( '/auth/google/callback',
passport.authenticate('google', {
//successRedirect: '/',
failureRedirect: '/'
, session: false
}),
function(req, res) {
var token = AuthService.encode(req.user);
res.redirect("/home?token=" + token);
});
But how the client will get the user entity?
So you can also pass the user in the same way but it didn't felt right for me (passing the whole user entity in the parameter list...).
So what I did is make the client use the token and retrieve the user.
function handleNewToken(token) {
if (!token)
return;
localStorageService.set('token', token);
// Fetch activeUser
$http.get("/api/authenticate/" + token)
.then(function (result) {
setActiveUser(result.data);
});
}
Which mean another http request - This make me think that maybe I didnt get right the token concept.
Feel free to enlighten me.
Initialize passport in index.js:
app.use(passport.initialize());
In your passport.js file:
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL:
'http://localhost:3000/auth/google/redirect',
},
async (accessToken, refreshToken, profile,
callback) => {
// Extract email from profile
const email = profile.emails![0].value;
if (!email) {
throw new BadRequestError('Login failed');
}
// Check if user already exist in database
const existingUser = await User.findOne({ email
});
if (existingUser) {
// Generate JWT
const jwt = jwt.sign(
{ id: existingUser.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update existing user
existingUser.token = jwt
await existingUser.save();
return callback(null, existingUser);
} else {
// Build a new User
const user = User.build({
email,
googleId: profile.id,
token?: undefined
});
// Generate JWT for new user
const jwt = jwt.sign(
{ id: user.id },
process.env.JWT_KEY,
{ expiresIn: '10m' }
);
// Update new user
user.token = jwt;
await auth.save();
return callback(null, auth);
}
}));
Receive this JWT in route via req.user
app.get('/google/redirect', passport.authenticate('google',
{failureRedirect: '/api/relogin', session: false}), (req, res) => {
// Fetch JWT from req.user
const jwt = req.user.token;
req.session = {jwt}
// Successful authentication, redirect home
res.status(200).redirect('/home');
}