This is very odd behavior. Everytime i get a new credentials, I click on the link, I get the consent screen but after that consent screen does not show anymore but request to google server and and response to callback url is happening behind the scene. I logout user "/auth/logout", I also delete all the cookies stored manually, When I clieck on the button, I am automatically signed in again.
I belive there is nothing wrong with coding and I checked the consent screen of console.developers but there is nothing related to this issue.
this is a typescript project. here is the routes
import passport from "passport";
import { Application, Request, Response } from "express";
export const authRoutes = (app: Application) => {
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"],
})
);
app.get(
"/auth/google/callback",
passport.authenticate("google"),
(req: Request, res: Response) => {
res.redirect("/");
}
);
// passport sees the code here and it knows that it has to use the code to get user
app.get("/auth/current_user", (req: Request, res: Response) => {
res.send(req.user);
});
app.get("/auth/logout", (req: Request, res: Response) => {
req.logout();
res.json({ user: req.user });
});
};
here is the passport setting:
import GoogleStrategy from "passport-google-oauth20";
import passport from "passport";
import { User, UserDoc } from "../database/models/User";
// passport sets up the cookie and stuffs the user's database id not the googleId.
passport.serializeUser(
(user: UserDoc, done: (err: any, user: UserDoc) => void) => {
done(null, user.id);
}
);
passport.deserializeUser(
async (id: string, done: (err: any, user: UserDoc) => void) => {
const user = await User.findById(id);
if (user) {
done(null, user);
}
}
);
passport.use(
new GoogleStrategy.Strategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: "http://localhost:4500/auth/google/callback",
proxy: true,
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
done(undefined, existingUser);
}
const user = await new User({ googleId: profile.id }).save();
done(undefined, user);
}
)
);
What you're seeing is the desired behavior - once the user has granted authorization to the scopes you're asking for (that are shown in the consent screen), Google doesn't need to get the user's consent again. It keeps track of that.
Users can review and revoke access to your app (mobile or web-based) at https://myaccount.google.com/permissions. This is particularly useful during development so you don't have to constantly create new accounts to test with.
Related
I'm implementing facebook-auth using https://www.npmjs.com/package/passport and https://www.npmjs.com/package/passport-facebook-token npm packages
this is my code in the passport config file:
passport.use(
new FacebookTokenStrategy(
{
clientID: config.FACEBOOK_CLIENT_ID,
clientSecret: config.FACEBOOK_CLIENT_SECRET,
fbGraphVersion: 'v3.0',
},
(accessToken, refreshToken, profile, done) => {
User.findOne(
{ 'facebookProvider.id': profile.id },
(error: any, user: any) => {
if (!user) {
const newUser = new User({
name: profile.displayName,
email: profile.emails[0].value,
facebookProvider: { id: profile.id, token: accessToken },
imageProfile: profile.photos[0].value,
});
newUser.save((error, result) => {
if (error) {
return done(error);
}
return done(null, result);
});
}
return done(error, user);
}
);
}
)
);
and below is my route handler:
const authRouter = express.Router();
authRouter
.route('/facebook')
.get(
passport.authenticate('facebook-token'),
async (req: Request, res: Response) => {
if (req.user) {
const accessToken: String = jwt.sign(
{ user: req.user },
config.JWT_SECRET_KEY,
{
expiresIn: config.TOKEN_LIFE_TIME,
}
);
return sendSuccesResponse(res, 200, accessToken);
}
return sendErrorResponse(res, 400, 'Something went wrong');
}
);
export default authRouter;
also, I initialized the passport in the index.ts file which is the entry point of API and imported the passport config file:
import './config/passport';
.
.
.
app.use(passport.initialize());
app.use('/api/v1/oauth2', authRouter);
Moreover, I got the token from Facebook and when I request my API with the provided token, Facebook can authenticate and return the user profile but after authentication and creating a new user or if the user already exists in DB, then it does not go to the route handler where I want to generate JWT token for the user and return to the user.
I got the following error in postman:
500 internal server error
After a lot of struggling, I found that I must provide a second parameter { session: false } for passport.authenticate function.
The below code is working just fine:
passport.authenticate('facebook-token', { session: false })
I am working on a MERN stack appliaction and I wanna integrate google authencation using PassportJs, I already configured the server-side and ran a couple of test which were successful but all of a sudden it no longers returns the usual profile object which contains the user's name, first_name, _json e.t.c, instead it sends back this object;
{
access_token: 'ya29.A0ARrdaM8QpjEP_3sDyDRBT8OJiOlXVgOHFcyEV1nne13jd_qRelaTYh5ry0H0E8WvmOs14h6PycgTHqteS85U9lPxj2sfhnabOI6XdMgWAM_Z_y4WR1F50NR7MVDcjpH6aS8xLzewScSt8R-6Cs6t4Adn3Vgu',
expires_in: 3599,
scope: 'https://www.googleapis.com/auth/userinfo.email openid https://www.googleapis.com/auth/userinfo.profile',
token_type: 'Bearer',
id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImNlYzEzZGViZjRiOTY0Nzk2ODM3MzYyMDUwODI0NjZjMTQ3OTdiZDAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI5NDc2NDU0NDY5NTctZWhiNXN1dWduZnZkcGJzNnE5cXZjNDQ2ZWRzZTY4ZWwuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI5NDc2NDU0NDY5NTctZWhiNXN1dWduZnZkcGJzNnE5cXZjNDQ2ZWRzZTY4ZWwuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDA3NDM5NTc3MzIwMDkwMzE5OTAiLCJlbWFpbCI6InNpbW9uY2NvdmVuYW50QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiODBfN20xMVJVNHV5SHZiblc2UEZIZyIsIm5hbWUiOiJTaW1vbiBDb3ZlbmFudCIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQVRYQUp4QWtNdEhtSWFjLV9CNEFzVkVYQU5jMUF1WEh5QU5KNTdFVVRVPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IlNpbW9uIiwiZmFtaWx5X25hbWUiOiJDb3ZlbmFudCIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNjQ5MTA5MDk5LCJleHAiOjE2NDkxMTI2OTl9.cRn6YA8OYSkp_MGLoDQTqeu6R5Ajm3KQMg6cQkZaBXuduJIJOnVL1r-lgCAIDHmkTsH-gohBIWcELPL0Pzm0rueW2X6b0LEzPdFNMsHFL_RkbRh2bwPwqAZqToaEJsN6M9DqqQwjuc8aENwHOxflVfTqM71aidt96cEIucRcCYxF1Q-rxBw4STNy0c2Lqae_85fFO5uArEJPyOPYDjVYjEqR0wNWFezRadA8zAKV7tv2WJFhEbA2tgnnbIKP5rWmkF6V6mlbFKv9p2qFvBLUpj6ffqVnQZmwILJng6GvNrWu03VfbAvHao4PA-qLwPnge65hqjet3S8TxzlNkkAtDA'
}
This my passport setup:
const strategy = new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback",
},
function (accessToken, refreshToken, profile, cb, done) {
console.log(profile);
const { email, given_name, family_name, email_verified, gender } = profile._json;
User.findOne({ email }).exec((err, user) => {
if (user) {
const token = jwt.sign({ _id: user._id }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
const { password, ...others } = user._doc;
return cb(err, others, token);
} else {
let password = email + process.env.JWT_SECRET;
user = new User({
firstname: given_name,
lastname: family_name,
email,
password: CryptoJS.AES.encrypt(
password,
process.env.PASS_SEC
).toString(),
gender: "male",
});
console.log(user);
user.save((err, data) => {
console.log(data);
if (err) {
console.log("Save error", errorHandler(err));
return done(null, false, { message: errorHandler(err) });
} else {
const token = jwt.sign({ _id: data._id }, process.env.JWT_SECRET, {
expiresIn: "7d",
});
const { password, ...others } = data._doc;
console.log(others);
return cb(err, token, others);
}
});
}
});
}
);
passport.use(strategy);
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
And this is my routes :
router.get(
"/google",
passport.authenticate("google", {
scope: ["profile", "email"],
})
);
router.get(
"/google/callback",
passport.authenticate("google", {
successRedirect: "/login/success",
failureRedirect: "/login/failed",
})
);
I thought maybe it had something to do with the accessToken so I tried using the "passport-oauth2-refresh" dependency to refresh and get a new accessToken;
const refresh = require("passport-oauth2-refresh");
refresh.use(strategy);
But that didnt work, I have tried searching stack-overflow for similar issues but found none. The strange thing was that it was working before, cant seem to figure out what went wrong.
I also wanna ask, after successful authentication I want to be able to send the user to my frontend and store it in localStorage but I found out that Google or Oauth don't support CORS. So I cannot make axios request nor catch server response, so what would be the optimal way to handle the authentication on the frontend?
My frontend google login code:
const google = () => {
window.open(`${process.env.REACT_APP_API_URL}/auth/google`, "_self");
}
I am building an app using node.js and react. I have the need to authenticate users using Oauth v2 with Passport.js and MongoDB. I want to allow users to login using either Google, Facebook or LinkedIn. I have the Google authorization/authentication working great. The user fires up the app, gets a Landing page with Login options. When the Google option is selected an intermediary page is rendered, asking the user to choose their Google Account (I have 4). I select the Google account I want. The app moves on to the next page in the app. I can logout and go back to my landing page. All of that is just fine. Now if I select a login with LinkedIn (I have only 1 account), the app does not give me the intermediary page, asking me to allow (or disallow) the login. It simple and automatically gives me an authenticated user and moves on to the next page in the app. I can then logout and go back to my Landing page. So, the app is working sort of as it should be, but not entirely.
I have confirmed that when I start the login process with LinkedIn there is no item in the User Collection. I have confirmed that after the login I DO have an item in the user Collection for the Linkedin login. I have physically deleted the item (repeatedly) and retried. I get no opportunity to allow/disallow the login with Linkedin.
Here is my code:
services/passport.js
'use strict';
// node modules
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy
const LinkedinStrategy = require('passport-linkedin-oauth2').Strategy;
const mongoose = require('mongoose');
// local modules
const keys = require('../config/keys');
const User = mongoose.model('users');
passport.serializeUser((user, done) => {
done(null, user.id); // NOTE: user.id = mongoDB _id
});
passport.deserializeUser((id, done) => {
User.findById(id)
.then(user => {
done(null, user);
});
});
passport.use(
new GoogleStrategy({
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
proxy: true
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({
provider: profile.provider,
providerID: profile.id
})
if (existingUser) {
return done(null, existingUser);
}
const user = await new User({
provider: profile.provider,
providerID: profile.id,
displayName: profile.displayName
}).save()
done(null, user);
})
)
passport.use(
new LinkedinStrategy({
clientID: keys.linkedinAppID,
clientSecret: keys.linkedinAppSecret,
callbackURL: '/auth/linkedin/callback',
proxy: true
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({
provider: profile.provider,
providerID: profile.id
})
if (existingUser) {
return done(null, existingUser);
}
const user = await new User({
provider: profile.provider,
providerID: profile.id,
displayName: profile.displayName
}).save()
done(null, user);
})
)
routes/auth.js
use strict';
// node modules
const passport = require('passport');
// local modules
const keys = require('../config/keys');
module.exports = (app) => {
// google routes
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
app.get('/auth/google/callback',
passport.authenticate('google'),
(req, res) => {
res.redirect('/selector');
}
);
// linkedin routes
app.get('/auth/linkedin',
passport.authenticate('linkedin', {
request_type: 'code',
state: keys.linkedinAppState,
scope: ['r_basicprofile', 'r_emailaddress']
})
);
app.get('/auth/linkedin/callback',
passport.authenticate('linkedin'),
(req, res) => {
res.redirect('/selector');
}
);
// common routes
app.get('/api/logout', (req, res) => {
req.logout();
res.redirect('/');
});
app.get('/api/current_user', (req, res) => {
res.send(req.user);
});
}
I don't know if there is anything more you need to see. I have confirmed the the hrefs in my Header component are pointing to the correct endpoints and that they match the routes in auth.js
Upon further investigation I found that there is a fair amount of interpolation that must be engaged in when using passport.js instead of manually writing the API interfaces. My problems were encountered because of incomplete setup of the various parameters for the APIs. By brute force trial and error I have solved the problems.
I'm writing authentication Nodejs API using passport, passport-google-oauth20
Everything is work but the problem is now I want to verify email of the user via domain. My system just allows email with domain #framgia.com can log in to. If not, send the user back a message.
My code here:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const mongoose = require('mongoose');
const keys = require('../config/keys');
const User = mongoose.model('users');
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id).then(user => {
done(null, user);
})
});
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback',
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
return done(null, existingUser);
}
if (!profile._json.domain || profile._json.domain !== 'framgia.com') {
return done(null, {error: 'Not allow access!'});
}
const user = await new User({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
}).save();
done(null, user);
},
),
);
And I'm writing logic code like that:
if (!profile._json.domain || profile._json.domain !== 'framgia.com') {
return done(null, {error: 'Not allow access!'});
}
But I think it won't work, but I don't know how to handle the error and send the message back to user.
My routes:
const passport = require('passport');
module.exports = (app) => {
app.get(
'/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email'],
}),
);
app.get(
'/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication, redirect home.
res.redirect('/');
},
);
};
How to handle the error and redirect to route /error with some message?
Any ideas would be greatly appreciated, thanks.
First of all, if you want to return the user only if an email has a certain domain, you need to put your domain check logic before findOne(). With current logic, if you found a user it will simply return it without checking the email domain
//check email domain before finding the user
if (!profile._json.domain || profile._json.domain !== 'framgia.com') {
return done(null, {error: 'Not allow access!'});
}
const existingUser = await User.findOne({ googleId: profile.id });
if (existingUser) {
return done(null, existingUser);
}
According to passport js documentation, http://www.passportjs.org/docs/configure/ (check verify callback section)
An additional info message can be supplied to indicate the reason for
the failure. This is useful for displaying a flash message prompting
the user to try again.
so if the domain does not match, you should return an error like this
return done(null, false, { message: 'Not allow access!' });
I finished wiring up my authentication flow by enabling cookies inside my application and then essentially telling passport to use cookies inside my authentication.
To test this out, I added a new route handler inside of my application whose sole purpose is to inspect this req.user property.
This is my services/passport.js file:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const mongoose = require('mongoose');
const keys = require('../config/keys');
const User = mongoose.model('users');
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id).then(user => {
done(null, user);
});
});
// passport.use() is a generic register to make Passport
// aware of new strategy
// creates a new instance to authenticate users
passport.use(
new GoogleStrategy(
{
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: '/auth/google/callback'
},
(accessToken, refreshToken, profile, done) => {
User.findOne({ googleId: profile.id }).then(existingUser => {
if (existingUser) {
// we already have a record with given profile id
} else {
// we dont have a user record with this id, make a new record
done(null, existingUser);
new User({ googleId: profile.id })
.save()
.then(user => done(null, user));
}
});
}
)
);
The data is passed to passport which pulls out the id out of the cookie data. The id is then passed on to my deserializeUser function where I am taking that id and turning it into a user model instance and then the whole goal is that the user model instance returned from deserializeUser is added to the request object as req.user and so that new route handler mentioned above has a job of inspecting req.user.
So in my routes/authRoutes.js:
const passport = require('passport');
module.exports = app => {
app.get(
'/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
app.get('/auth/google/callback', passport.authenticate('google'));
app.get('/api/current_user', (req, res) => {
res.send(req.user);
});
};
So this is supposed to test that someone who has already gone through the OAuth flow in theory, can now log back in and we can authenticate that is the same user.
So the expected behavior being that I would once again go through the OAuth flow by visiting localhost:5000/auth/google and then open a separate browser in localhost:5000/api/current_user and be able to see the MongoDB record of that user as json in the browser, but instead I got a blank page and no error in my command line terminal or anywhere else.
What could be the matter here?
You have a minor flaw i've noticed in your if statement:
if (existingUser) {
// we already have a record with given profile id
} else {
// we dont have a user record with this id, make a new record
done(null, existingUser);
new User({ googleId: profile.id })
.save()
.then(user => done(null, user));
}
should be:
if (existingUser) {
// we already have a record with given profile id
done(null, existingUser);
} else {
// we dont have a user record with this id, make a new record
new User({ googleId: profile.id })
.save()
.then(user => done(null, user));
}