I am having trouble understanding why my koa v2 app is 404ing when it receives the callback from my oauth2 provider. I see in the network tab that it is receiving a GET to /oauth/callback with a code query parameter. My routes definitely exist because if I open the page in the browser myself it 500s with an error:
TokenError: The provided authorization grant is invalid, expired,
revoked, does not match the redirection URI used in the authorization
request, or was issued to another client.
Here is my app so far, following the koa-passport-example:
const Koa = require('koa')
const app = new Koa()
// trust proxy
app.proxy = true
// sessions
const convert = require('koa-convert')
const session = require('koa-generic-session')
app.keys = ['your-session-secret']
app.use(convert(session()))
// body parser
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
// authentication
require('./authn')
const passport = require('koa-passport')
app.use(passport.initialize())
app.use(passport.session())
// routes
const fs = require('fs')
const route = require('koa-route')
app.use(route.get('/logout', function(ctx) {
ctx.logout()
ctx.redirect('/login')
}))
app.use(route.get('/login',
passport.authenticate('oauth2')
))
app.use(route.get('/oauth/callback',
passport.authenticate('oauth2', {
failureRedirect: '/login',
successRedirect: '/'
})
))
// Require authentication for now
app.use(function(ctx, next) {
console.log('auth check', ctx.isAuthenticated())
if (ctx.isAuthenticated()) {
return next()
} else {
ctx.redirect('/login')
}
})
app.use(route.get('/', function(ctx) {
ctx.type = 'html'
ctx.body = fs.createReadStream('views/app.html')
const { token } = ctx.state
const authed = ctx.isAuthenticated()
if (authed) {
console.log('token', token)
}
}))
// start server
const port = process.env.PORT || 3000
app.listen(port, () => console.log('Server listening on', port))
And the authn.js file:
import passport from 'koa-passport'
const user = { id: 1, username: 'dmarr#foo.com' }
passport.serializeUser(function(user, done) {
done(null, user.id)
})
passport.deserializeUser(function(id, done) {
done(null, user)
})
var OAuth2Strategy = require('passport-oauth2').Strategy
passport.use(new OAuth2Strategy({
authorizationURL: 'redacted',
tokenURL: 'https://redacted/token',
clientID: 'redacted',
clientSecret: 'redacted',
callbackURL: "http://localhost:8080/oauth/callback"
},
function(accessToken, refreshToken, profile, done) {
console.log('authed with oauth')
console.log('token', accessToken)
console.log('refresh token', refreshToken)
done(null, user)
// User.findOrCreate({ exampleId: profile.id }, function (err, user) {
// return done(err, user);
// });
// console.log(accessToken)
}
));
Thank you for any help
Well this seems to be working after all. I did have a typo which appears to have been the issue. Keeping the above in case anyone else needs help with oauth2 and koa and passport.
Edit: It turns out I can't seem to access the modified user from the authenticate callback. For example:
function(accessToken, refreshToken, profile, done) {
user.accessToken = accessToken
done(null, user)
})
and in my route handler
const { passport } = ctx.session
const user = passport.user
// user.accessToken is undefined because session.passport.user is the serialized one
Related
I'm currently working on my first MERN project, with Passport Local, Google OAuth 2.0 and JWT. All my routes are protected with passport.authenticate("jwt", { session: false }), (req, res) => {}). I know Google Strategy relies on Express session and here's where my issue starts.
I don't know if this is the correct approach, but I want to generate a JWT token from Google OAuth login to handle the user session with it and keep all my routes requests working properly. From the past issues opened here I've found this one to be the most recent and practical solution, but it seemed to me that it would introduce a new problem - since the local login already generates the JWT token, wouldn't it conflict with the request on the step 4 from the linked solution? If I'm thinking right, the token would be generated two times when using the standard login - one right at the authentication and another when reaching the app's home page, not to mention that it looks like the request to /auth/login/success will be made every time the home page is reached, which doesn't seem right to me.
So, what would be the best and cleaner way to approach this? I'd also like to validate everything on the server side and avoid appending the token to the URL, if possible. Anyway, here is my code:
passport.js
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const JwtStrategy = require("passport-jwt").Strategy;
const User = require("../models/user.model");
require("dotenv").config();
module.exports = function (passport) {
// CONFIGURE STRATEGIES
// Local
passport.use(User.createStrategy());
// Google
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "http://localhost:5000/auth/google/movie-log",
},
function (accessToken, refreshToken, profile, cb) {
User.findOrCreate(
{ googleId: profile.id },
{ first_name: profile.displayName, email: profile._json.email },
function (err, user) {
return cb(err, user);
}
);
}
)
);
// JWT
const cookieExtractor = (req) => {
let token = null;
if (req && req.cookies) {
token = req.cookies["access_token"];
}
return token;
};
passport.use(
new JwtStrategy(
{
jwtFromRequest: cookieExtractor,
secretOrKey: process.env.SECRET,
},
(payload, done) => {
User.findById(payload.sub, (err, user) => {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
} else {
return done(null, false);
}
});
}
)
);
// CONFIGURE AUTHENTICATED SESSION PERSISTENCE
passport.serializeUser(function (user, done) {
done(null, user.id);
});
passport.deserializeUser(function (id, done) {
User.findById(id, function (err, user) {
done(err, user);
});
});
};
auth.js (for Google authentication)
const express = require("express");
const passport = require("passport");
const router = express.Router();
const login = "http://localhost:3000/login";
const diary = "http://localhost:3000/diary";
router.get(
"/google",
passport.authenticate("google", {
scope: ["email", "profile"],
})
);
router.get(
"/google/movie-log",
passport.authenticate("google", {
successRedirect: diary,
failureRedirect: login,
})
);
module.exports = router;
login.js (for local authentication)
const express = require("express");
const passport = require("passport");
const JWT = require("jsonwebtoken");
const router = express.Router();
const signToken = (userID) => {
return JWT.sign(
{
iss: "Movie.log",
sub: userID,
},
process.env.SECRET,
{ expiresIn: "1h" }
);
};
router
.route("/")
.post(passport.authenticate("local", { session: false }), (req, res) => {
if (req.isAuthenticated()) {
const { _id, first_name } = req.user;
const token = signToken(_id);
res.cookie("access_token", token, { httpOnly: true, sameSite: true });
res.status(200).json({
isAuthenticated: true,
user: first_name
});
}
});
module.exports = router;
so, im using express, and passport-oauth2 / passport-discord to create a website for my discord bot, but the checkauth function always returns false, and during the auth process, i just get redirected to the main auth route and nothing happens. (i never get redirected to the dashboard)
heres the code: (i tried to include every file relative to the problem):
// strategy file
const DiscordStrategy = require('passport-discord').Strategy;
const passport = require('passport');
const DiscordUser = require('.././models/DiscordUser');
var session = require('express-session')
passport.serializeUser((user, done) =>{
done(null, user.id)
})
passport.deserializeUser(async (id, done) =>{
const user = await DiscordUser.findById(id);
if(user){
done(null, user);
}
})
passport.use(new DiscordStrategy({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: process.env.CLIENT_REDIRECT,
scope: ['identify', 'guilds', 'email', 'bot']
}, async (accessToken, refreshToken, profile, done) => {
try{
const user = await DiscordUser.findOne({ discordId: profile.id});
if(user) {
console.log('a known user entered!')
done(null, user);
}else{
console.log('a new user entered!')
const newUser = await DiscordUser.create({
discordId: profile.id,
username: profile.username,
discriminator: profile.discriminator,
email: profile.email,
guilds: profile.guilds
});
const savedUser = await newUser.save();
done(null, savedUser);
}
}catch(err) {
console.log(err);
done(err, null);
}
}));
//checkauth file
module.exports = async (req, res, next) => {
if(req.isAuthenticated()){
return next();
} else {
return res.redirect("/auth");
}
};
//auth router
const router = require('express').Router();
const passport = require('passport');
router.get('/', passport.authenticate('discord'));
router.get('/redirect', passport.authenticate('discord', {
failureRedirect: '/forbidden',
successRedirect: '/dashboard'
}), (req, res) => {
res.sendStatus(200);
})
module.exports = router;
//dashboard router
const router = require('express').Router();
var session = require('express-session');
const CheckAuth = require('../CheckAuth');
router.get('/', CheckAuth, (req, res) => {
res.sendFile('../views/dashboard/index')
})
module.exports = router;
in the main file i just create a random cookie, define the routes, and use app.use(passport.initialize()) and app.use(passport.session()).
if you need anything else let me know, ty :)
In the documentation they use the callback method instead of successRedirect.
router.get('/redirect', passport.authenticate('discord', {
failureRedirect: '/'
}), function(req, res) {
res.redirect('/secretstuff') // Successful auth
});
I would like to use Oauth1.o to get credentials from a provider but I am halfway stuck. I am using passportjs for the implementation.
Here is the brief code:
oauth.js
const passport = require('passport');
const OAuthStrategy = require('passport-oauth').OAuthStrategy;
passport.use(
'provider',
new OAuthStrategy({
requestTokenURL: "****",
accessTokenURL: "****",
userAuthorizationURL: "****",
consumerKey: "****",
consumerSecret: "****",
callbackURL: "****",
},
(token, tokenSecret, profile, done) => {
store[token] = tokenSecret;
console.log(profile);
done(null, profile);
})
);
module.exports = passport;
Express mini-app(modularised routing)
const express = require('express');
const router = express.Router();
const passport = require('./oauth');
/**
* Initialize Passport
* Restore authentication state, if any, from the session
*/
router.use(passport.initialize());
router.use(passport.session());
router.get('/oauth/sign-in', passport.authenticate('provider'));
router.get('/oauth/callback', (req, res) => {
const { oauth_token, oauth_verifier, denied } = req.query;
if (denied !== null && denied !== undefined) {
req.session.destroy();
res.json(denied);
}
res.json({ oauth_token, oauth_verifier });
});
module.exports = router;
From the above code, I successfully get to the callback URL after I authorize the app on the Twitter page. But I am unable to console.log the token, tokenSecret & profile. The documentation was also not clear to me as to how to use the done callback function.
I am on a learning journey using https://github.com/twitterdev/twauth-web/blob/master/twauth-web.py and would rather avoid using passport-twitter to learn master the example they have given.
You need to add the authenticate middleware in the callback route as well. You can check out the docs here:
router.get('/oauth/callback', passport.authenticate('provider', {
failureRedirect: '/login' //optional
}),
(req, res) => {
//...your code...
});
I'm having a problem I'm not able to resolve. I'm developing an app with nodejs, using mongodb, expressjs and passportjs as my authentication middleware.
I currently have 3 strategies: facebook, twitter and instagram. What I want to achieve is that when a user login for the first time, if the user is logged with one strategy and logs in with another one save the profiles into the same mongodb user document.
This is my auth/index.js:
require('./local/passport').setup(User, config);
require('./facebook/passport').setup(User, config);
require('./twitter/passport').setup(User, config);
require('./instagram/passport').setup(User, config);
var router = express.Router();
router.use('/local', require('./local'));
router.use('/facebook', require('./facebook'));
router.use('/twitter', require('./twitter'));
router.use('/instagram', require('./instagram'));
And this is, for example, my auth/twitter/index.js
var router = express.Router();
router
.get('/', passport.authenticate('twitter', {
failureRedirect: '/',
session: false
}))
.get('/callback', passport.authenticate('twitter', {
failureRedirect: '/',
session: false
}), auth.setTokenCookie);
module.exports = router;
But how could I pass for example a mongodb _id to this auth/twitter/passport.js in order to pass it to the mongoose query and update an user? Something like making a POST to auth/twitter and accessing to req.user._id ? I can't figure out how to do it.
exports.setup = function (User, config) {
var passport = require('passport');
var TwitterStrategy = require('passport-twitter').Strategy;
var TwitterApi = require('twitter');
passport.use(new TwitterStrategy({
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
callbackURL: config.twitter.callbackURL
},
function(token, tokenSecret, profile, done) {
User.findOne({
'twitter.id_str': profile.id
}, function(err, user) {
if (err) {
return done(err);
}
if (!user) {
user = new User({
role: 'user',
[...]
Thank you very much.
EDIT:
This is how I set my cookie:
function setTokenCookie(req, res) {
if (!req.user) return res.json(404, { message: 'Something went wrong, please try again.'});
var token = signToken(req.user._id, req.user.role);
res.cookie('token', JSON.stringify(token));
res.redirect('/');
}
and the signToken function:
function signToken(id) {
return jwt.sign({ _id: id }, config.secrets.session, { expiresInMinutes: 60*24*30 });
}
Why req.user and req.session are always empty in my Strategy?
EDIT2:
I think I could use the auth.isAuthenticated() function to attach user to the request before invoking the Strategy. What I have done is this:
router
.get('/', auth.isAuthenticated(), passport.authenticate('twitter', auth.isAuthenticated, {
failureRedirect: '/',
session: false
}))
.get('/callback', auth.isAuthenticated(), passport.authenticate('twitter', {
failureRedirect: '/',
session: false
}), auth.setTokenCookie);
But now I'm having this problem:
UnauthorizedError: No Authorization header was found
My request to auth/twitter comes from a $window.location. It seems that this does not attach the user object to the request, because when I make a GET or POST using isAuthenticated() the user object is passed correctly. This is my isAuthenticated() function:
function isAuthenticated() {
return compose()
// Validate jwt
.use(function(req, res, next) {
// allow access_token to be passed through query parameter as well
if(req.query && req.query.hasOwnProperty('access_token')) {
req.headers.authorization = 'Bearer ' + req.query.access_token;
}
validateJwt(req, res, next);
})
// Attach user to request
.use(function(req, res, next) {
User.findById(req.user._id, function (err, user) {
if (err) return next(err);
if (!user) return res.send(401);
req.user = user;
next();
});
});
}
FYI I just dealed with this decoding the JWT token in my Strategies. I don't know if this is a good practice but the problem was I was not having my user attached to the request if the request was made with a $window.location.href
So in my strategies I read the cookie and decode it on the fly for searching a user in database.
Thank you.
You can set passReqToCallback to true when you define your passport strategy. It will make the current request, thus the current logged in user available to your callback function.
exports.setup = function (User, config) {
var passport = require('passport');
var TwitterStrategy = require('passport-twitter').Strategy;
var TwitterApi = require('twitter');
passport.use(new TwitterStrategy({
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
callbackURL: config.twitter.callbackURL,
passReqToCallback: true
},
function(req, token, tokenSecret, profile, done) {
User.findOne({
'twitter.id_str': profile.id
}, function(err, user) {
if (err) return done(err);
if (!user) {
if (req.user) {
[...]
I'm stuck on an error with passport. I am building an api that is using restify. I am using client-session for session and passport with google oauth 2.
I am having trouble at the point it would serialise the user and create a session. I seem to be getting no errors however it is printing out large node objects to the console.
var restify = require('restify');
var massive = require('massive');
var passport = require('passport');
var GoogleStrategy = require('passport-google-oauth2').Strategy;
var _ = require('lodash');
var sessions = require("client-sessions");
var app = function(config, done) {
// declare DB and server
var server = restify.createServer(),
db;
// Set server settings
server.use(restify.bodyParser());
server.pre(restify.pre.sanitizePath());
server.use(restify.queryParser());
server.use(sessions({
// cookie name dictates the key name added to the request object
cookieName: 'Skyrail_session',
// should be a large unguessable string
secret: 'abc123yyighhcggfgucgdguhvgcydtfugjvhfguijkvhgcfgvcfg',
// how long the session will stay valid in ms
duration: 24 * 60 * 60 * 1000,
activeDuration: 1000 * 60 * 5
}));
server.use(passport.initialize());
server.use(passport.session());
server.use(function logger(req, res, next) {
console.log(new Date(), req.method, req.url);
next();
});
server.on('uncaughtException', function(request, response, route, error) {
console.error(error.stack);
response.send(error);
});
// passport auth settings
passport.use(new GoogleStrategy({
clientID: ID,
clientSecret: SECRET,
callbackURL: 'http://127.0.0.1:8080/oauth2callback',
passReqToCallback: true
},
function(request, accessToken, refreshToken, profile, done) {
var isEmail = false;
for (var i = profile.emails.length - 1; i >= 0; i--) {
if (_.endsWith(profile.emails[i].value, 'test.co.uk')) {
isEmail = true;
}
};
if (isEmail) {
// do user stuff
done(null, profile);
} else {
done(null, profile);
}
}
));
passport.serializeUser(function(user, done) {
console.log('serializing user.');
done(null, user.id);
});
passport.deserializeUser(function(user, done) {
console.log('deserialize user.');
done(null, user.id);
});
// connect to the database
massive.connect({
connectionString: config.postgres.conString
}, function(err, massiveInstance) {
db = massiveInstance;
done();
});
var google = passport.authenticate('google', {
scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile'
});
var googleCallback = passport.authenticate('google');
var authenticate = passport.authenticate('google', {
successRedirect: '/success',
failureRedirect: '/fail'
})
// setup shceduler
var scheduler = require('node-schedule');
// return the appropriate method and objects.
return {
server: server,
db: db,
authenticate: authenticate,
google: google,
googleCallback: googleCallback,
scheduler: scheduler
}
};
module.exports = app
It all works fine and I end up at google accept and then when it gets to this point.
passport.serializeUser(function(user, done) {
console.log('serializing user.');
done(null, user.id);
});
it logs to the console and then starts throwing out node objects. I am pretty sure it has something to do with setting the cooking.
I am at a bit of loss as how to debug. There is not much information about using passport with restify.
Old question, but see my answer here for an example of how to use client-session with Passport.
TL;DR: set {cookieName: 'session'} (not "Skyrail_session") and serialize the entire user object, not just the ID. You may need to use JSON.stringify() and JSON.parse() if Passport doesn't do this for you.