I was trying to comprehend how passport strategy is working.
Consider these api routes which I am using to authenticate.
router.get("/google", passport.authenticate('google', { scope: ['profile', 'email'] }));
router.get("/google/callback", passport.authenticate('google'), (req, res) => {
res.redirect("http://localhost:3000/")
})
And this is the passport strategy
const passport = require('passport')
const GoogleStratergy = require('passport-google-oauth20')
const keys = require("./key.js")
const User = require("../models/user-model.js")
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser((id, done) => {
User.findById(id).then((user) => {
done(null, user) //pass it in req of our routes
})
})
passport.use(
new GoogleStratergy({
//Options for the stratergy
callbackURL: "/auth/google/callback",
clientID: keys.google.clientID,
clientSecret: keys.google.clientSecret
}, (accessToken, refreshToken, profile, done) => {
User.findOne({userId: profile.id }).then((currentUser) => {
if (currentUser) {
done(null, currentUser)
} else {
//Changing Image String
let oldURL= profile.photos[0]["value"]
let newURL = oldURL.substr(0, oldURL.length-2);
newURL = newURL + "250"
//Creating Mongoose Database
new User({
username: profile.displayName,
userId: profile.id,
image: newURL,
email: profile.emails[0]["value"]
}).save().then((newUser) => {
console.log("new user created", newUser)
done(null, newUser)
})
}
})
})
)
Now, I think I understand what is happening here, but one thing I am unable to comprehend here is..
How is
passport.use(
new GoogleStratergy({
//Options for the stratergy
being called here? I mean I don't see any export statements, so how is it linked with out Node App? or how does passport knows behind the scene about the location of our google strategy **
Also, Just to confirm, after we pass done from our passport.use? it goes to serialize?
When you require passport, you get a singleton instance i.e. it's constructed the first time you require passport, and is reused every time and everywhere it's required subsequently.
So you don't need to share the instance between modules i.e. no need for export. Any configuration you do on the instance is visible everywhere you require it.
There are other objects in NodeJS that work the same way, one prominent example is the express app instance.
Here is the source code for passport where you can verify this.
Related
I have a react application that uses passport authentication strategy using express server. In my local machine my set up will start a server (app.js) in one port and react app in other port. And I'm able to authenticate successfully using passport. In the hosting environment the server will take care of starting the server and building the react client to public folder.
My issue happens after I push my code to the hosting environment, the hosting environment starts the express server (node app.js) and build the react app to public folder. Even though I'm able to authenticate using passport and able to receive the user information, if someone else was logged in from a different browser of a different machine then that user's profile is available in my browser.
app.js
const express = require('express');
const passport = require('passport');
...
let user = {}; //this is an object to store the profile data
...
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (obj, done) {
done(null, obj);
});
var OpenIDConnectStrategy = require('passport-ci-oidc').IDaaSOIDCStrategy;
var Strategy = new OpenIDConnectStrategy({
clientID: settings.client_id,
clientSecret: settings.client_secret,
callbackURL: settings.callback_url,
function (iss, sub, profile, accessToken, refreshToken, params, done) {
process.nextTick(function () {
user = {...profile}
profile.accessToken = accessToken;
profile.refreshToken = refreshToken;
done(null, profile);
})
}
);
passport.use(Strategy);
app.use(express.static(__dirname + '/public'));
app.get('/login', passport.authenticate('openidconnect', {}));
app.get('/oidc_callback', function (req, res, next) {
passport.authenticate('openidconnect', {
successRedirect: redirect_url,
failureRedirect: '/failure',
})(req, res, next);
});
app.get('/user', function (req, res) { //This is the api i use to access user information in ract page
res.send(user);
});
app.get('/logout', function (req, res) {
user = {}; //empty the user data object
req.session.destroy();
req.logout();
res.end();
});
Here we can see I'm using a user object to store the user data and created a API /user (app.get('/user' ...) to access the user data from my client react application.
React component to access the user
constructor() {
super()
this.state = {
loggedUser: []
}
}
componentDidMount() {
axios.get('/user') //If I'm testing with local the API will be https://localhost:5000 - node server
.then(res => {
const loggedUser = res.data;
this.setState({ loggedUser });
}).catch(err => {
console.log("Fetch error", err)
})
}
Edit: completely reviewed answer after question edited with new data.
First of all we need a unique key in profile to uniquely identify a user, in my example I use "oid" but you can use the one better fits your requirements.
You could try to change following pieces of code:
let user = {}; //this is an object to store the profile data
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (obj, done) {
done(null, obj);
});
// Later ...
function(iss, sub, profile, accessToken, refreshToken, params, done) {
process.nextTick(function () {
user = {...profile}
profile.accessToken = accessToken;
profile.refreshToken = refreshToken;
done(null, profile);
})
}
with:
let uniqueKey = "oid"; // The field name of profile which uniquely identify a user
let users = {};
passport.serializeUser((user, done) => done(null, (users[user[uniqueKey]] = user)[uniqueKey]));
passport.deserializeUser((key, done) => done(null, users[key]));
// Later ...
(iss, sub, profile, accessToken, refreshToken, params, done) => done(null, { ...profile, accessToken, refreshToken })
// or
(iss, sub, profile, accessToken, refreshToken, params, done) => {
process.nextTick(() => done(null, { ...profile, accessToken, refreshToken }));
}
All these are async functions, so when I say to return I mean doing it through the done callback function.
The first login function only need to return the user object (actually passport-ci-oidc seems to require it in process.nextTick, you can try with or without it).
serializeUser needs indeed to serialize the user object, which in our case means just to sore it in the memory store users[user[uniqueKey]] = user and then to return the unique key which passport should later use to deserialize the same user done(null, user[uniqueKey]).
As last deserializeUser need to return the user object by a given identification key, in our case just to return it from the memory sore done(null, users[key]).
Hope this helps.
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));
}
I followed a course and it implemented user authentication using passport, passport-google-oauth20, cookie-session and it all works fine (login, logout, session handling) but when i send a request for a Log in/Sign Up it doesnt ask/prompt the google authentication window to enter the credentials, it always logs in with the same account.
Here is the passport-strategy configuration:
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',
proxy: true,
authorizationParams: {
access_type: 'offline',
approval_prompt: 'force'
}
},
async (accessToken, refreshToken, profile, done) => {
const existingUser = await User.findOne({ googleID: profile.id })
if (existingUser) {
// we already have a record with the given profile ID
return done(null, existingUser);
}
// we don't have a user record with this ID, make a new record!
const user = await new User({ googleID: profile.id, name: profile.displayName }).save()
done(null, user);
})
);
Add prompt: 'select_account' to the passport.authenticate() middleware in your /auth/google route.
app.get('/auth/google', passport.authenticate('google', {
scope: ['profile', 'email'],
prompt: 'select_account'
});
Visit this page: https://developers.google.com/identity/protocols/OpenIDConnect#scope-param