Account link using passport without session - node.js

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()
}

Related

Passport JS Google oauth20 callback missing req object

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

Getting 401 when trying to access spotify web api with access token

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.

Error "Failed to serialize user into session" when using passport.js for Azure AD

I am trying to follow the Office 365 Graph API tutorial here.
However I keep running into the below error after I sign-in.
Error: Failed to serialize user into session
at pass (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport\lib\authenticator.js:271:19)
at serialized (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport\lib\authenticator.js:276:7)
at passport.serializeUser (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\app.js:20:3)
at pass (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport\lib\authenticator.js:284:9)
at Authenticator.serializeUser (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport\lib\authenticator.js:289:5)
at IncomingMessage.req.login.req.logIn (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport\lib\http\request.js:50:29)
at Strategy.strategy.success (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport\lib\middleware\authenticate.js:235:13)
at verified (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\node_modules\passport-azure-ad\lib\oidcstrategy.js:98:21)
at Strategy.signInComplete [as _verify] (C:\Users\SODS\devzone\src\nodejs\o365-graph\graph-tutorial\app.js:63:10)
at process._tickCallback (internal/process/next_tick.js:68:7)
The serializeUser method looks like below:
//Configure passport
//In-memory storage of users
let users = {};
//Call serializeUser and deserializeUser to manage users
passport.serializeUser((user, done) => {
//Use the OID prop of the user as the key
users[user.profile.oid] = user;
done(null, user.profile.oid);
});
passport.deserializeUser((id, done) => {
done(null, users[id]);
});
The problem might be that user.profile.oid seems to be undefined inside the callback function of serializeUser (tried also replacing arrow functions with normal ones but didn't work either).
But the oid is being set during the sign-in and I can see it when I trace the values in the code below.
//Callback when the sign-in is complete and an access token has been obtained
async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) {
console.log('iss profile oid -->' + profile.oid);
if (!profile.oid) {
return done(new Error("No OID found in user profile."), null);
}
try {
const user = await graph.getUserDetails(accessToken);
if (user) {
//Add properties to profile
profile['email'] = user.mail ? user.mail : user.userPrincipalName;
}
} catch(err) {
done(err, null);
}
//Create simple oauth token from raw tokens
let oauthToken = oauth2.accessToken.create(params);
//Save the profile and token in users storage
users[profile.oid] = {profile: oauthToken};
return done(null, users[profile.oid]);
}
Which is the call for the OIDCStratergy.
//Configure OIDC stratergy
passport.use(new OIDCStaregy(
{
identityMetadata: `${process.env.OAUTH_AUTHORITY}${process.env.OAUTH_ID_METADATA}`,
clientID: process.env.OAUTH_APP_ID,
responseType: 'code id_token',
responseMode: 'form_post',
redirectUrl: process.env.OAUTH_REDIRECT_URI,
allowHttpForRedirectUrl: true,
clientSecret: process.env.OAUTH_APP_PASSWORD,
validateIssuer: false,
passReqToCallback: false,
scope: process.env.OAUTH_SCOPES.split(' ')
},
signInComplete
));
So the question is is the undefined users.profile.oid the issue here? If so where & why does it get lost?

Passport & JWT & Google Strategy - Disable session & res.send() after google callback

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');
}

Authenticate user with passport through LinkedIn login

I have built a login system in Passport and works quite well. Now, I want to integrate LinkedIn login in my system. I already have clientID, clientSecret etc. needed to login. This is the code that is called when the LinkedIn login button is pressed.
passport.use('linkedin', new OAuth2Strategy({
authorizationURL: 'https://www.linkedin.com/uas/oauth2/authorization',
tokenURL: 'https://www.linkedin.com/uas/oauth2/accessToken',
clientID: clientid,
clientSecret: clientsecret,
callbackURL: '/linkedinLogin/linkedinCallbackUrlLogin',
passReqToCallback: true
},
function(req,accessToken, refreshToken, profile, done) {
console.log('authenticated');
console.log(accessToken);
req.session.code = accessToken;
process.nextTick(function () {
done(null, {
code : req.code
});
});
}));
Both the console.log() calls in the callback function are successfully fired, this means I am successfully logged in through LinkedIn and I receive my access token. The part where I connect with LinkedIn is thus correct, what I am missing is the part where I actually log in the user. As you can see, the callbackURL points to /linkedinLogin/linkedinCallbackUrlLogin. This is what I do in that route:
app.get('/linkedinLogin/linkedinCallbackUrlLogin', passport.authenticate('linkedin', {
session: false,
successRedirect:'/linkedinLogin/success',
failureRedirect:'/linkedinLogin/fail'
}));
I just specify a successRedirect and a failureRedirect. Note that if I put session : true I receive as an error Failed to serialize user into session, so for now I keep it to false.
The successRedirect is successfully called. In that route I call a GET request to LinkedIn to access some data about the user. I want to store this data in my DB and remember the user that logged in. This is how I do it:
https.get(
{
host: 'api.linkedin.com' ,
path: '/v1/people/~?format=json' ,
port:443 ,
headers : {'Authorization': ' Bearer ' + req.session.code}
},
function(myres) {
myres.on("data", function(chunk) {
var linkedinJsonResult = JSON.parse(chunk);
User.findOne({linkedinLogin : linkedinJsonResult.id}, function(err, userSearchResult){
if(err) {
throw err;
}
//user found, login
if(userSearchResult){
console.log(userSearchResult);
}
else {
//create user
var newUser = new User(
{
url : linkedinJsonResult.siteStandardProfileRequest.url,
name : linkedinJsonResult.firstName + " " + linkedinJsonResult.lastName,
linkedinLogin : linkedinJsonResult.id,
regDate : new Date()
}
);
//save user
newUser.save(function(err, user){
if(err){
throw err;
}
//login
console.log(user);
});
}
});
});
}
);
Let me explain the code there. After getting the data of the user I check the field "id" that is received. If this id matches one of my users' linkedinLogin field stored into the DB, I consider it already registered (the user has been found in the DB), thus I have to log him/her in. Otherwise I just create a new user using the data received from the GET request.
My question is, in both the cases - the user is found in my DB, or the user has to be created - how can I set req.user to be my user whenever it interacts with my website? Is it sufficient to just do req.user = userSearchResult (if the user is found, inside the if statement) or req.user = user (if the user has been created, inside the newUser.save() callback), or should I call some passport functions that will set it for me?
All the other passport functions related to the registration and login of users without using LinkedIn login are working fine. I am just worried about making this LinkedIn login work with passport.
Thank you.
passport.js will automatically set the req.user object to the object you will pass as the second argument to the done function of the strategy callback.
This means that you should do something like this:
function(req,accessToken, refreshToken, profile, done) {
console.log('authenticated');
console.log(accessToken);
req.session.code = accessToken;
process.nextTick(function () {
// retrieve your user here
getOrCreateUser(profile, function(err, user){
if(err) return done(err);
done(null, user);
})
});
}));
I hope this helps.

Resources