How to use passport with next.js API? - node.js

I'm trying to use passport-spotify with next.js's pages/api (the one where you use export default (req, res)...), but I can't get it to redirect to Spotify's authorization page. This is my code for pages/api/spotify.js:
var passport = require('passport');
const SpotifyStrategy = require('passport-spotify').Strategy;
passport.use(
new SpotifyStrategy(
{
clientID: clientid,
clientSecret: clientsecret,
callbackURL: 'http://localhost:3000/auth/spotify/callback'
},
function(accessToken, refreshToken, expires_in, profile, done) {
User.findOrCreate({ spotifyId: profile.id }, function(err, user) {
return done(err, user);
});
}
)
);
export default (req, res, next) => {
passport.authenticate('spotify', {
scope: ['user-read-email', 'user-read-private'],
showDialog: true
}),
res.end()
}
I've tried using express to test it, and it works there. Here's my code:
const express = require('express')
const app = express()
const port = 8000
var passport = require('passport');
const SpotifyStrategy = require('passport-spotify').Strategy;
passport.use(
new SpotifyStrategy(
{
clientID: clientid,
clientSecret: clientsecret,
callbackURL: 'http://localhost:3000/api/callback'
},
function(accessToken, refreshToken, expires_in, profile, done) {
User.findOrCreate({ spotifyId: profile.id }, function(err, user) {
return done(err, user);
});
}
)
);
app.get(
'/auth/spotify',
passport.authenticate('spotify', {
scope: ['user-top-read'],
showDialog: true
}),
function(req, res) {
// The request will be redirected to spotify for authentication, so this
// function will not be called.
}
);
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
Is there a way to have Next.js work with Passport?

I got it! I used next-connect. Here's an example Spotify authentication request with passport-spotify and next.js:
// pages/api/spotify.js
import nc from 'next-connect';
var passport = require('passport');
const SpotifyStrategy = require('passport-spotify').Strategy;
passport.use(
new SpotifyStrategy(
{
clientID: clientid,
clientSecret: clientsecret,
callbackURL: 'http://localhost:3000/api/callback'
},
function(accessToken, refreshToken, expires_in, profile, done) {
User.findOrCreate({ spotifyId: profile.id }, function(err, user) {
return done(err, user);
})
}
))
const handler = nc()
.get(passport.authenticate('spotify', {
scope: ['user-top-read']
}), (req, res) => {
})
export default handler;

Related

How to integrate Passport Google OAuth 2.0 with Passport JWT?

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;

how to use passport-google-oauth20 twice in the same app

I'm trying to use the passport-google-oauth20 twice in my node app but it's not working. Still learning passport authentication.
So here's the problem:
i made a button for the user to login using google(/auth.google) and a button for a user to register using google(/users/google). I want it so that if the user is already registered on my app using google and he goes on a presses "register using google" button, he should be redirected back to the register page(auth fails). In my case when i add the strategy once and add a login button only(without register), auth works and users is redirected to dashboard. However, when i add the strategy again in passport.js and the 'already registered user" tries to login using google, he gets redirected to register page(where the register using google button is found) instead of dashboard. I know there's no need to do such thing and no need to use the same strat again for registeration as it works the same for logging in. just wondering if it's possible to work.
Here's my code:
Passport.js
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const myClientId = require("./keys").clientID;
const myClientSecret = require("./keys").clientSecret;
// get the user schema
const User = require("../models/User");
// passport google authentication
module.exports = function (passport) {
passport.use(
new GoogleStrategy(
{
clientID: myClientId,
clientSecret: myClientSecret,
callbackURL: "/auth/google/callback",
},
(accessToken, refreshToken, profile, done) => {
const newUser = new User({
googleId: profile.id,
displayName: profile.displayName,
firstName: profile.name.givenName,
lastName: profile.name.familyName,
image: profile.photos[0].value,
});
User.findOne({ googleId: profile.id })
.then((user) => {
if (user) {
done(null, user);
} else {
newUser.save().then((userd) => {
done(null, userd);
});
}
})
.catch((err) => console.log(err));
}
)
);
// using same strategy again but different callback URL
passport.use(
new GoogleStrategy(
{
clientID: myClientId,
clientSecret: myClientSecret,
callbackURL: "/users/google/callback",
},
(accessToken, refreshToken, profile, done) => {
const newUser = new User({
googleId: profile.id,
displayName: profile.displayName,
firstName: profile.name.givenName,
lastName: profile.name.familyName,
image: profile.photos[0].value,
});
User.findOne({ googleId: profile.id })
.then((user) => {
if (user) {
done(null, false);
} else {
newUser.save().then((userd) => {
done(null, userd);
});
}
})
.catch((err) => console.log(err));
}
)
);
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
};
app.js
/* initalized passport, session and other stuff here but just showing you the routes and the passport
function in app.js */
require("./config/passport")(passport);
app.use("/auth", require("./routes/auth"));
app.use("/users", require("./routes/users"));
auth.js
const express = require("express");
const passport = require("passport");
const router = express.Router();
//? register and login handle
router.get(
"/google",
passport.authenticate("google", {
scope: ["profile"],
})
);
//? google auth callback
router.get(
"/google/callback",
passport.authenticate("google", {
successRedirect: "/dashboard",
failureRedirect: "/",
})
);
module.exports = router;
users.js
const express = require("express");
const router = express.Router();
const passport = require("passport");
const { ensureAuthenticated, forwardAuth } = require("../config/checkAuth");
//? register page
router.get("/register", forwardAuth, (req, res) => {
res.render("register");
});
//? login page
router.get("/login", forwardAuth, (req, res) => {
res.render("login");
});
//? logout handle
router.get("/logout", (req, res) => {
req.logOut();
res.redirect("/");
});
router.get(
"/google",
passport.authenticate("google", {
scope: ["profile"],
})
);
router.get(
"/google/callback",
passport.authenticate("google", {
successRedirect: "/dashboard",
failureRedirect: "/users/register",
})
);
module.exports = router;
So if the users logs in on /auth/google ...he gets redirected to the register page(/users/register)! Can't know why
I solved my problem.
the solution was actually simple. Just using the same passport.js file.
in passport.js
const GoogleStrategy = require("passport-google-oauth20").Strategy;
module.exports = function (passport) {
passport.use("firstUse", new GoogleStrategy(....))
passport.use("secondUse", new GoogleStrategy(...))
}
so the thing is that you can register the strategy with any name you want and then when you want to handle the authorization middleware in the routes you just do this:
in users and auth.js
app.get("/auth/google/callback", passport.authenticate("firstUse"));
app.get("/users/google/callback", passport.authenticate("secondUse"));
just add the name that you registered with. I thought that it is necessary to only use passport.authenticate("google") and not any name else than "google".

duplicate key error index in mongodb mongoose

I am building a website in which logging in with Google and typing email and password were working fine, but when I introduced logging in with Facebook, mongoDb is giving following error-
MongoError: E11000 duplicate key error collection: userDB.users index: username_1 dup key: { username: null }
//jshint esversion:6
require('dotenv').config();
const express = require("express");
const bodyParser = require("body-parser");
const ejs= require("ejs");
const mongoose = require("mongoose");
const session = require("express-session");
const passport = require("passport");
const passportLocalMongoose = require("passport-local-mongoose");
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const FacebookStrategy = require('passport-facebook').Strategy;
const findOrCreate = require("mongoose-findorcreate");
const app = express();
console.log(process.env.API_KEY);
app.use(express.static("public"));
app.set('view engine', 'ejs');
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(session({
secret: "Our little secret.",
resave: false,
saveIninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
mongoose.connect("mongodb://localhost:27017/userDB",{ useUnifiedTopology: true });
mongoose.set("useCreateIndex", true);
const userSchema = new mongoose.Schema ({
email: String,
password: String,
googleId: String,
facebookId: String,
secret: String
});
userSchema.plugin(passportLocalMongoose);
userSchema.plugin(findOrCreate);
const User = new mongoose.model("User", userSchema);
passport.use(User.createStrategy());
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
passport.use(new GoogleStrategy({
clientID: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
callbackURL: "http://localhost:3000/auth/google/secrets",
userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo"
},
function(accessToken, refreshToken, profile, cb) {
User.findOrCreate({ googleId: profile.id }, function (err, user) {
return cb(err, user);
});
}
));
passport.use(new FacebookStrategy({
clientID: process.env.FACEBOOK_APP_ID,
clientSecret: process.env.FACEBOOK_APP_SECRET,
callbackURL: "http://localhost:3000/auth/facebook/secrets"
},
function(accessToken, refreshToken, profile, cb) {
User.findOrCreate({ facebookId: profile.id }, function (err, user) {
return cb(err, user);
});
}
));
app.get("/",function(req,res){
res.render("home")
});
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile'] })
);
app.get('/auth/google/secrets',
passport.authenticate('google', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect to secrets.
res.redirect('/secrets');
});
app.get('/auth/facebook',
passport.authenticate('facebook'));
app.get('/auth/facebook/secrets',
passport.authenticate('facebook', { failureRedirect: '/login' }),
function(req, res) {
// Successful authentication, redirect home.
res.redirect('/secrets');
});
app.get("/login",function(req,res){
res.render("login")
});
app.get("/register",function(req,res){
res.render("register")
});
...
How to solve this issue so that I can login via both google and facebook one after the other or vice-versa?
see the solution here:
Setting Up Facebook Authentication with MongoDB Atlas and Passport.js
the problem is with the code above that both facebook and google users create a document in database which has no username. and MongoD set username as unique by default. so just drop the indexes from username and that solves the issue.

SSL is blocking login with Passport in production, fully working on localhost

I've built a small authentication app with Passport. It works just fine on localhost, which by the way is insecure. But on my domain, with Cloudflare SSL I'm not sure what's going on, I can't reach consent screen with Google and GitHub strategies. I don't have much knowledge about ssl certificate and want to know what part is wrong on my server.
I have added app.set('trust proxy'), app.use(cors()), set 'proxy: true' in both strategies.
Here is the relevant part of the code and the live code on my domain https://authapp.mambaoro.com
In index.js
app.set('trust proxy', 1);
app.use(cors());
app.use(
session({
secret: process.env.SESSION_KEY_1,
proxy: true,
saveUninitialized: false,
resave: false,
maxAge: 6.048e8,
}),
);
app.use(passport.initialize());
app.use(passport.session());
app.use('/auth', authRoutes);
app.get('/getUser', (req, res) => {
if (!req.user) {
return res.send({ isAuthenticated: false });
}
res.send(req.user);
});
In passport-setup.js
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
proxy: true,
},
async (accesToken, refreshToken, profile, done) => {
try {
await User.sync();
const newUser = await User.findOrCreate({
where: {
googleId: profile.id,
username: profile.displayName,
profileImageUrl: profile.photos[0].value,
},
});
done(null, newUser[0]);
} catch (e) {
done(e);
}
},
),
);
passport.use(
new GithubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: process.env.GITHUB_CALLBACK_URL,
proxy: true,
},
async (accessToken, refreshToken, profile, done) => {
try {
await User.sync();
const newUser = await User.findOrCreate({
where: {
githubId: profile.id,
username: profile.username,
profileImageUrl: profile.photos[0].value,
},
});
done(null, newUser[0]);
} catch (e) {
done(e);
}
},
),
);
In auth-routes.js
// auth with google
router.get(
'/google',
passport.authenticate('google', {
scope: ['profile'],
}),
);
// auth with github
router.get('/github', passport.authenticate('github', { scope:
['profile'] }));
In index.js On the client with React, the code below is used to get the user details with Passport if a cookie exists.
function SignUser() {
const [user, setUserData] = useState(null);
const [isAuthenticated, setIsAuth] = useState(false);
useEffect(() => {
const getUser = async () => {
const res = await axios.get('/getUser');
res.data.user && setUserData(res.data.user);
res.data.isAuthenticated && setIsAuth(true);
};
getUser();
}, []);
...

404 from callbackURL with koa-passport using passport-oauth2 strategy

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

Resources