After updating my dependencies at service I'm creating I faced the issue that my Passport JS strategy, used along with OAuth2Strategy stoped working properly.
Here are the prerequisite:
Node: 14.15.1
pnpm: 6.29.0
passport: 0.5.2
passport-oauth2 1.6.1
express-session 1.17.2
Strategy is initialised like follows:
const passport = require('passport');
const config = require('../../../../config');
const OAuth2Strategy = require('passport-oauth2');
const axios = require('axios');
const MyStrategy = new OAuth2Strategy({
// state: true,
authorizationURL: config.myapi.authorizationURL,
tokenURL: config.myapi.tokenURL,
clientID: config.myapi.clientId,
clientSecret: config.myapi.clientSecret,
callbackURL: config.myapi.callbackURL,
passReqToCallback: true,
}, () => {console.log('Fire!')}); // <- This line should be called, but it is not!
passport.use('oauth2', MyStrategy);
*Obviously, the part where fire is written should be replaced by callback function, but I replaced it for more cleaner code
And routes go this way
...
routes.get('/oauth/myapi', async (req, res, next) => {
const authParams = {
session: true,
scope: 'read, create',
state: req.csrfToken(),
};
return passport.authenticate('oauth', authParams,
async (err, passportUser, info) => {
if (err) {
return next(err);
}
if (passportUser) {
const user = passportUser;
user.token = passportUser.generateJWT();
return res.json(user.toAuthJSON());
}
res.status(400).json({error: info});
})(req, res, next);
});
routes.get('/oauth/myapi/callback',
async (req, res, next) => {
return passport.authenticate('oauth', {
// failWithError: true,
successRedirect: '/dashboard',
failureRedirect: '/login/oauth/myapi/failed'
})(req, res, next);
});
...
So in a callback I do receive response from third service and it looks like this
{
"code":"2QoCKOzHQbCdJID4m...pwHv4M1RqUKjKF",
"state":"dS9Gagcc-....a_yJ71YU",
"user_id":"101"
}
But when callback route attempts to execute passport.authenticate I receive
$ Error: Failed to obtain access token
$ at /Users/number16/Documents/GitHub/Video-Mixer-Node/node_modules/.pnpm/passport-oauth2#1.6.1/node_modules/passport-oauth2/lib/strategy.js:178:49
Debugging didn't help much either.
It seems to me that I do something wrong or I should update my code as some breaking change might require.
The problem seems to be caused by switching from 0.4.1 to 0.5.2 passport
Please, provide me with suggestions on what might cause this issue and how to resolve it.
Thanks in advance
Related
I am attempting to use Passport.js to authorize Google OAuth2 on Node.js. I have tried all week to make it work and have no idea why it isn't, so am now resorting to stack for some potential help. I have tried all solutions to similar problems available on forums online.
Each time it sends the request it returns TokenError: Bad Request, however, it is able to console.log the required data, so this to me demonstrates that the token was in fact successful. I cannot explain why this is occurring.
I have tried being more specific in callback request e.g http://localhost:3000/auth/google/redirect.
I have tried every other type of Oauth type google has Node server, web application, html ect.
I have tried different ports.
AUTH ROUTES
const router = require('express').Router();
const passport = require('passport');
// auth login
router.get('/login', (req, res) => {
res.render('login', { user: req.user });
});
// auth logout
router.get('/logout', (req, res) => {
// handle with passport
res.send('logging out');
});
// auth with google+
router.get('/google', passport.authenticate('google', {
scope: ['profile']
}));
// callback route for google to redirect to
// hand control to passport to use code to grab profile info
router.get('/google/redirect', passport.authenticate('google'),
(req,
res) => {
res.send('you reached the redirect URI');
});
module.exports = router;
PASSPORT_SETUP
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const keys = require('./keys');
passport.use(
new GoogleStrategy({
// options for google strategy
clientID: keys.google.clientID,
clientSecret: keys.google.clientSecret,
callbackURL: '/auth/google/redirect'
}, (accessToken, refreshToken, profile, done) => {
// passport callback function
console.log('passport callback function fired:');
console.log(profile);
})
);
When submitted the process progresses through SignIn page, delivers desired result the console.log and then just sits for about 1 minute awaiting localhost.
As you can see the very thing it is trying to retrieve is already in the console.
It then progresses to throw and Error:
Sorry for the late reply, dug up some old code this is the point where it was marked as 'All auth methods functioning'. As stated by Aritra Chakraborty in the comments, "done" method was not being called. See the following implementation with Nedb.
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const Datastore = require('nedb');
const database = new Datastore('database.db');
database.loadDatabase();
passport.serializeUser((user, done) => {
done(null, user.googleId || user.id);
});
passport.deserializeUser((googleId, done) => {
database.findOne({ googleId : googleId }, (err, user) => {
done(null, user);
});
});
var strategy = new GoogleStrategy({
// options for google strategy
clientID: keys.google.clientID,
clientSecret: keys.google.clientSecret,
callbackURL: '/auth/google/redirect'
}, (accessToken, refreshToken, object0, profile, done) => {
// check if user already exists in our own db
database.findOne({ googleId: profile.id }, (err, currentUser) => {
if (currentUser !== null) {
done(null, currentUser);
} else {
var d = new Date();
var n = d.getTime();
var duoID = uuidv1();
var User = {
duoVocalID: duoID,
googleId: profile.id,
username: profile.displayName,
thumbnail: profile._json.image.url,
oscope: object0.scope,
oaccess_token: object0.access_token,
otoken_type: object0.token_type,
oid_token: object0.id_token,
oexpires_in: object0.expires_in,
oemails: profile.emails,
olanguage: profile._json.language,
oname: profile.name,
TimeOfLastLogon: n,
RefreshToken: refreshToken
};
database.insert(User, (err, newUser) => { });
var newUser = User;
done(null, newUser);
}
});
});
passport.use(strategy);
// auth with google+
app.get('/auth/google', passport.authenticate('google', {
scope: ['profile', 'email', 'https://www.googleapis.com/auth/spreadsheets'],
accessType: 'offline',
approvalPrompt: 'force'
}));
// callback route for google to redirect to
// hand control to passport to use code to grab profile info
app.get('/auth/google/redirect', passport.authenticate('google'), async (req, res) => {
var userString = JSON.stringify(req.user)
jwt.sign({userString}, 'secretKey', { expiresIn: '365d' }, (err, token) => {
res.send("<script>localStorage.setItem('token', '"+token+"'); window.close(); window.opener.document.getElementById('modal-toggle').checked = false;</script>");
});
});
I am using a Twitter strategy with passport.js but when I hit a certain route such as auth/twitter in the app, it never redirects to the Twitter sign-in/authorization.
My passport.js
const passport = require("passport");
const TwitterStrategy = require("passport-twitter").Strategy;
passport.serializeUser(function(user, cb) {
cb(null, user.id);
});
passport.deserializeUser(function(id, cb) {
User.findOne({ id }, function(err, user) {
cb(err, user);
});
});
passport.use(
new TwitterStrategy(
{
consumerKey: "some_key",
consumerSecret: "some_secret",
callbackURL: "http://www.example.com/auth/twitter/callback"
},
function(token, tokenSecret, profile, cb) {
User.findOrCreate({ twitterId: profile.id }, function(err, user) {
return cb(err, user);
});
}
)
);
AuthController.js
const passport = require("passport");
module.exports = {
twitter: function(req, res) {
console.log("TWITTER AUTH IS RUNNING");
passport.authenticate("twitter");
console.log("NOT HANGING");
},
twitterCallback: function(req, res) {
console.log("TWITTER CALLBACK");
passport.authenticate("twitter", {
successRedirect: "/",
failureRedirect: "/search"
});
}
};
My routes.js
// AUTH
"GET /auth/twitter": "AuthController.twitter",
"GET /auth/twitter/callback": "AuthController.twitterCallback",
The console.logs do run accordingly but the app never goes to the twitter authentication portion.
UPDATE: I fixed the issue, it was most likely an issue with how I setup up passport and not including the middlewares, but now I have another issue of none of my pages being rendered. They are just blank pages and not even the favicon is being rendered in.
UPDATE: I seemed to narrow the issue down to the http.js file.
// order: [
// "session",
// "passportInit", // <==== If you're using "passport", you'll want to have its two
// "passportSession" // <==== middleware functions run after "session".
// ],
// passportInit: (function() {
// return require("passport").initialize();
// })(),
// passportSession: (function() {
// return require("passport").session();
// })()
Whenever I uncomment that block of code the pages won't render. More specifically when I only leave the order:["session"] uncommented. I think the middlewares are keeping the pages from rendering.
UPDATE: It was how left some of the middlewares out. I added all default middlewares in and now it works.
NEW ISSUE: The callback function after the initial passport.use does not fire. I tried console logging from within the callback function and nothing appears in the terminal.
I have an async function as a route handler, and i'd like to have errors handled as some kind of middleware. Here is my working attempt:
router.get(
"/",
asyncMiddleware(
routeProviderMiddleware(
async ({ y }) => ({
body: await db.query({x: y})
})
)
)
)
// This is the middleware that catches any errors from the business logic and calls next to render the error page
const asyncMiddleware = fn =>
(req, res, next) => {
Promise.resolve(fn(req, res, next))
.catch(next)
}
// This is a middleware that provides the route handler with the query and maybe some other services that I don't want the route handler to explicitly access to
const routeProviderMiddleware = routeHandlerFn => async (req, res) => {
const {status = 200, body = {}} = await routeHandlerFn(req.query)
res.status(status).json(body)
}
What I strive to is a way to make the route declaration cleaner - I don't want the 2 middleware wrappers there, ideally i'd like for the business logic function there only, and somehow declare that every route is wrapped in these.
Even combining the two middlewares together would be nice, but I didn't manage.
I use following approach:
Create asyncWrap as helper middleware:
const asyncWrap = fn =>
function asyncUtilWrap (req, res, next, ...args) {
const fnReturn = fn(req, res, next, ...args)
return Promise.resolve(fnReturn).catch(next)
}
module.exports = asyncWrap
All your routes/middlewares/controllers should use this asyncWrap to handle errors:
router.get('/', asyncWrap(async (req, res, next) => {
let result = await db.query({x: y})
res.send(result)
}));
At app.js, the last middleware will receive the errors of all asyncWrap:
// 500 Internal Errors
app.use((err, req, res, next) => {
res.status(err.status || 500)
res.send({
message: err.message,
errors: err.errors,
})
})
Express 5 automatically handles async errors correctly
https://expressjs.com/en/guide/error-handling.html currently says it clearly:
Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error. For example:
app.get('/user/:id', async function (req, res, next) {
var user = await getUserById(req.params.id)
res.send(user)
})
If getUserById throws an error or rejects, next will be called with either the thrown error or the rejected value. If no rejected value is provided, next will be called with a default Error object provided by the Express router.
I have shown that in an experiment at: Passing in Async functions to Node.js Express.js router
This means that you will be able to just make the callback async and use await from it directly without any extra wrappers:
router.get("/", async (req, res) =>
const obj = await db.query({x: req.params.id})
// Use obj normally.
)
and errors will be correctly handled automatically.
Express permits a list of middlewares for a route and this approach sometimes works for me better than higher-order functions (they sometimes look like an overengineering).
Example:
app.get('/',
validate,
process,
serveJson)
function validate(req, res, next) {
const query = req.query;
if (isEmpty(query)) {
return res.status(400).end();
}
res.locals.y = query;
next();
}
function process(req, res, next) {
Promise.resolve()
.then(async () => {
res.locals.data = await db.query({x: res.locals.y});
next();
})
.catch((err) =>
res.status(503).end()
);
}
function serveJson(req, res, next) {
res.status(200).json(res.locals.data);
}
What you can do is add an error handlers after your routes. https://expressjs.com/en/guide/error-handling.html
app.use(function (err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})
What I ended up doing is unifying the wrappers like this:
const routeProvider = routeHandlerFn => async (req, res, next) => {
try {
const {status = 200, body = {}} = await routeHandlerFn(req.query)
res.status(status).json(body)
} catch(error) {
next(error)
}
}
This wrapper is all any route would need. It catches unexpected errors and provides the route handler with the needed params.
So I wanted to pass a parameter to a middleware but I'm struggling on some points.
I have that function in the routes' handler:
router.get('/users', auth.required, userController.findAll);
Then it would go to the auth function which calls getTokenFromHeaders:
const auth = {
required: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders,
}),
...
};
In getTokenFromHeaders function, the token is retrieved and checked, it looks like that:
const getTokenFromHeaders = (req) => {
...
return token; // Or null in case it's not there or incorrect
So my goal would be to pass a parameter like that auth.required('role') to check the user's role inside getTokenFromHeaders function (defining more auth functions would be fine as well (auth.admin, auth.whatever, ...)
I already tried modifying it as the following:
const auth = {
required: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders(req, res, role),
}),
But it says that req and res is not defined.
Is there any way to do that?
get method receives a path and 2 callbacks where first one is a middleware:
router.get('/users', auth, userController.findAll);
Middleware is like findAll function and is executed before:
var auth = function (req, res) {
// Get token from header (http://expressjs.com/en/api.html#req.get)
var token = req.get("myToken");
// TODO Validate token:
var isValid = someFunction(token);
if (!isValid) {
// ************
// TODO Check if user can access this resource
// ************
res.json("not authorized");
}
// Go to findAll (next callback)
};
I suggest you take at look in docs specially in app.use to understand better how middleware works in express.
I think that :
1) Call of request
app.use('/users', required);
2) Check authentification
const required = (req, res, next) => {
const auth = {
required: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders(req),
}),
...
};
if (auth.required) next();
else // error
}
3) Next step
router.get('/users', userController.findAll);
4) userController.findAll
userController.findAll will send the response with with the parameters recovered (req and res)
I'm trying to authenticate user through passport-facebook, and there is 1 really odd thing happening which im really unsure of.
When defining my routes i have this line of code
app.get('/login/facebook',
IsAuth.facebookEnter ),
//passport.authenticate('facebook')
app.get('/login/facebook/return',
IsAuth.facebookComeBack)
And in my IsAuth file i have the following
const passport = require('passport')
const FacebookStrategy = require('../passport/facebook_passport')
module.exports = {
facebookEnter(){
passport.authenticate('facebook')
},
facebookComeBack(){
passport.authenticate('facebook', { failureRedirect: '/login' }),
function(req, res) {
res.redirect('/');
}
}
}
and the strategy itself here
const passport = require('passport')
const User = require('../models/User')
const FacebookStrategy = require('passport-facebook')
passport.use(new FacebookStrategy({
clientID: "ALMOST POSTED IT",
clientSecret: "Ntest test",
//call baack to one of our routes to valiate the auth request
//is called when the user pressed on OK to login with facebook
callbackURL: "http://localhost:8888/login/facebook/return"
},
function(accessToken, refreshToken, profile, cb) {
console.log(profile, accessToken, refreshToken)
}
))
The question is
Why when i write
app.get('/login/facebook',
IsAuth.facebookEnter ),
it doesnt work but if i write this code
app.get('/login/facebook',
passport.authenticate('facebook') ),
then it works, so why? My JWT passport authnetication works good as expected even though using the same folder structure
How can i make it work and keep passport.authenticate in a seperate file?
Mine works with this! Dividing the exports.
module.exports.fbAuth = passport.authenticate("facebook");
module.exports.fbAuthCB = function(req, res, next) {
passport.authenticate("facebook", (err, user, info) =>
generateTokenAndRedirect(err, user, info, req, res, next)
)(req, res);
};
router.get("/auth/facebook", authCtrl.fbAuth);
router.get("/auth/facebook/callback", authCtrl.fbAuthCB);