How can I authenticate a GraphQL endpoint with Passport? - express

I have a GraphQL endpoint:
app.use('/graphql', graphqlHTTP(request => ({
graphiql: true,
schema
})));
I also have a Passport route for logging in (and handling the callback, since I'm using Google OAuth2):
this.app.get('/login', passport.authenticate('google'));
this.app.get('/auth/callback/google', ....
Passport add a user to the request, and all of the articles I can find online recommend authenticating in each of my GraphQL resolvers using that:
resolve: (root, args, { user }) => {
if (!user) throw new NotLoggedInError();
However it doesn't make sense to have to add that logic to every resolver when it applies to all of them, so I was hoping to somehow authenticate the entire endpoint.
The problem is that I'm not sure how to combine middleware. I tried the following but it just broke the endpoint:
app.use('/graphql', passport.authenticate('google'), graphqlHTTP(request => ({
graphiql: true,
schema
})));

I have the following working. Some issues I had were around making sure my google API was enabled and the proper scopes were enabled. I am also only using the passport middleware on the auth endpoints and using an isAuthenticated middleware to check if the session is authenticated and if not redirect to the auth endpoint. also putting the request object into the context so that it can be used by the resolver to potentially authorize the user. You would of course need to update the user lookup as I am just passing mock data.
import express from "express";
import graphqlHTTP from "express-graphql";
import passport from "passport";
import cookieParser from "cookie-parser";
import session from "express-session";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { buildSchema } from "graphql";
const PORT = 5000;
const data = [
{ id: "1", name: "foo1" },
{ id: "2", name: "foo2" },
{ id: "3", name: "foo3" },
];
const def = `
type Foo {
id: String!
name: String
}
type Query {
readFoo(id: String!): Foo
}
schema {
query: Query
}
`;
const schema = buildSchema(def);
const fieldMap = schema.getType("Query").getFields();
fieldMap.readFoo.resolve = (source, args) => {
return data.filter(({ id }) => id === args.id)[0] || null;
};
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((obj, done) => {
done(null, obj);
});
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: `http://localhost:${PORT}/auth/google/callback`,
},
(accessToken, refreshToken, profile, cb) => {
return cb(null, {
id: "1",
username: "foo#bar.baz",
googleId: profile.id,
});
}
)
);
function isAuthenticated(req, res, next) {
return req.isAuthenticated() ? next() : res.redirect("/auth/google");
}
const app = express();
app.use(cookieParser());
app.use(
session({
secret: "sauce",
resave: false,
saveUninitialized: false,
})
);
app.use(passport.initialize());
app.use(passport.session());
app.get("/auth/fail", (req, res) => {
res.json({ loginFailed: true });
});
app.get(
"/auth/google",
passport.authenticate("google", { scope: ["profile"] })
);
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/auth/fail" }),
(req, res) => {
res.redirect("/graphql");
}
);
app.use(
"/graphql",
isAuthenticated,
graphqlHTTP((req) => ({
schema,
graphiql: true,
context: req,
}))
);
app.listen(PORT, () => {
console.log("Started local graphql server on port ", PORT);
});

vbranden's answer was excellent, and it is the basis of this answer. However, his answer has a lot of other code which obfuscates the solution a bit. I didn't want to mess with it, since it offers a more complete view of things, but hopefully this answer will be helpful in its own way by being more direct. But again, all credit for this solution belongs to vbranden (please upvote his answer accordingly).
If you make an isAuthenticated function with the appropriate signature (request, response, next) you can then "chain" that function in when you setup your GraphQL endpoint:
function isAuthenticated(req, res, next) {
return req.isAuthenticated() ?
next() :
res.redirect('/auth/google');
}
app.use(
'/graphql',
isAuthenticated,
graphqlHTTP(req => ({
schema,
graphiql: true,
context: req
}))
);

Related

How to send access_token as cookie to client in NextJs(fullframework) + Passport

First of all, please understand that I am not good at English.
I am currently implementing authentication using NextJS (full framework) + passport.
I succeeded in getting the user's Twitter information using strategy, and after that, I'm going to jwt the user's information in db and deliver it to you as a cookie, but I don't know when and where this should be done.
code is like
lib/passport.ts
passport.use(
new TwitterStrategy(
{
consumerKey: process.env.TWITTER_CONSUMER_KEY as string,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET as string,
callbackURL: '/api/auth/callback/twitter',
includeEmail: true,
},
async (_accessToken, _refreshToken, profile: TwitterProfile, cb: any) => {
try {
return cb(null, profile);
} catch (e: any) {
throw new Error(e);
}
}
)
);
req.session.passport
passport.serializeUser((user, cb) => {
process.nextTick(function () {
return cb(null, user);
});
});
passport.deserializeUser(function (
user: any,
cb: (arg0: null, arg1: any) => any
) {
process.nextTick(function () {
return cb(null, user);
});
});
middleware/auth.ts
const auth = nextConnect()
.use(
session({
secret: process.env.NEXTAUTH_SECRET,
cookie: {
maxAge: 60 * 60 * 8, // 8 hours,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
},
resave: false,
saveUninitialized: true,
})
)
.use((req, res, next) => {
// Initialize mocked database
// Remove this after you add your own database
req.session.users = req.session.users || [];
next();
});
api/auth/callback/twitter.ts
import nextConnect from 'next-connect';
import passport from '#/lib/passport-twitter';
import auth from '#/middleware/auth';
const handler = nextConnect();
handler
.use(auth)
.use(passport.session())
.get((req, res) => {
passport.authenticate('twitter', async () => {
// I tried to set header here
res.setHeader('Set-Cookie', 'test');
res.end();
});
});
And when I try to log in additionally, where can I check the client request's cookies? (for access_token verification).
Lack of knowledge may lead to a lack of explanation and a wrong approach.
I want to use authentication information to store user information according to the situation. For example, if you have another account stored in the same email (Facebook), you can send a specific message without creating a user.

Passport local authentication fails on request after authenticated

I want to authenticate using the Passport local strategy. To be safe, I store the session in MongoDB.I am authenticating with passportjs for the first time, so if you can help me I would be very grateful.
I'm having a hard time because Factory Pattern is used in the backend I'm working on. First, the user signs up, then after the login process, the session belonging to the user is created in MongoDB. At the same time, a cookie is set on the client-side. There is no problem so far. However, when I send a request to the protected route that I created for the authenticated user, it gives an error before reaching the route. I can't find where the error is coming from.
ExpressFactory is like that
import express from "express";
import { AppContextType } from "../src/types/configTypes";
const passport = require("passport");
const session = require("express-session");
const mongoDbSession = require("connect-mongodb-session")(session);
export default async (appContext: AppContextType) => {
const app = express();
const { config } = appContext;
app.use(express.json());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
res.header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, Content-Length, X-Requested-With"
);
res.setHeader("Access-Control-Allow-Credentials", "true");
if ("OPTIONS" === req.method) {
res.sendStatus(200);
} else {
next();
}
});
const store = new mongoDbSession({
uri: config.db,
collection: "sessions",
});
app.use(
session({
secret: "very secret this is",
resave: false,
saveUninitialized: false,
store: store,
})
);
// Passport initialize
app.use(passport.initialize());
app.use(passport.session());
app.use(
"/message",
require("../src/controllers/messageController")(appContext)
);
app.use("/user", require("../src/controllers/userController")(appContext));
//response handler, if no middleware handles
app.use((req, res) => {
res.status(404).send(Object.assign(res as any, { success: false }));
});
return new Promise((resolve) => {
const httpServer = app.listen(5000, () => {
console.log("Connected Server Port : 5000");
resolve(httpServer);
});
});
};
Then I created passportAuthFactory to create the passportjs strategy and passed the appContext inside it to connect to the database collection.Like this :
import expressFactory from "../config/expressFactory";
import configFactory from "../config/configFactory";
const clientListenerFactory = require("../config/clientListenerFactory");
const passportAuthFactory = require("../config/passportAuthFactory");
import {
EnvironmentType,
ConfigType,
AppContextType,
} from "./types/configTypes";
const config = configFactory(process.env.NODE_ENV as EnvironmentType);
import appContextFactory from "../config/appContextFactory";
const appContextPromise = appContextFactory(config as ConfigType);
module.exports = appContextPromise.then(async (appContext: AppContextType) => {
const httpServer = await expressFactory(appContext);
await clientListenerFactory(httpServer, appContext);
passportAuthFactory(appContext);
});
My PassportJs File looks like this
const bcrypt = require("bcrypt");
const passport = require("passport");
const LocalStrategy = require("passport-local").Strategy;
import { AppContextType } from "../src/types/configTypes";
module.exports = (appContext: AppContextType) => {
const { userService } = appContext;
passport.use(
new LocalStrategy(
{ usernameField: "email" },
(email: any, password: any, done: any) => {
userService
.getUser(email)
.then((user: any) => {
if (!user) {
return done(null, false, { message: "User not found" });
}
bcrypt.compare(
password,
user.password,
(err: any, isMatch: any) => {
if (err) throw err;
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: "Wrong password" });
}
}
);
})
.catch((err: any) => {
return done(null, false, { message: err });
});
}
)
);
passport.serializeUser((user: any, done: any) => {
done(null, user._id);
});
passport.deserializeUser(function (id: any, done: any) {
userService
.getUserById(id)
.then((user) => {
done(user);
console.log("Deserialize");
console.log(user);
})
.catch((err) => {
done(err);
});
});
};
Controller File as
const authMiddleware = require("../middlewares/authMiddleware");
const passport = require("passport");
require("../../config/passportAuthFactory");
module.exports = (appContext: AppContextType) => {
const { userService } = appContext;
const router = express.Router();
router.get(
"/login",
passport.authenticate("local"),
function (req: Request, res: Response) {
try {
res.status(200).send(
Object.assign(
{
isAuthenticated: req.isAuthenticated(),
user: req.user,
},
{ success: true }
)
);
} catch (err) {
res.status(400).send(Object.assign(err as any, { success: false }));
}
}
There is no problem in the first signup and the first login, but then when I test the protected delete route with the person who has the authentication, I get the error.The error I got on the second request is as follows:
500 Internal Server Error
and also this is shown on the vscode terminal console
[object Object]
What could be causing this error I would be grateful if you could help me I'm about to go crazy I've been trying to figure this out for 3 days

setting cookie after google passport callback

I am using facebook and google oauth2 login using passport js, with this flow
User clicked the login button
Redirects to facebook/google auth page (depending on what login the user chooses)
The auth page redirects back to a callback page (/auth/callback/[provider])
A passport express middleware will catch it to parse some data and then send it to a remote api of myown to sign the user in
The auth remote api will send a response back consisting the user token
A custom express middleware will catch the response to set cookie on the server
the express chain ends by route it to /profile (cookie with token is set on the browser)
/profile will then checks if there is a token, if there is not: it will redirect to /
Doing this flow on facebook login is fine, the user is successfully redirected to /profile, with all of its data and token, the google oauth2 login however seems to be doing the redirect to /profile then setting the token (step #7 then #6), so everytime the user is using google oauth2 login, its always gonna be redirected back to / since by the time it arrives at /profile, it doesnt have the token
here's the code on the above's flow
#./server.js
const express = require('express')
const next = require('next')
const Passport = require('./server/middleware/passport')
const Api = require('./server/api')
const port = parseInt(process.env.PORT, 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app
.prepare()
.then(() => {
const server = express()
// ... other unrelated things
server.use(Passport.initialize())
Api.passport.facebook(server)
Api.passport.facebookCallback(server)
Api.passport.google(server)
Api.passport.googleCallback(server)
// ... other unrelated things
server.all('*', (req, res) => handle(req, res))
server.listen(port, (error) => {
if (error) throw error
// ... other unrelated things
})
})
#./server/api.js
const Passport = require('middleware/passport')
function setCookie(req, res, next) {
res.cookie('token', req.user.auth.token, {
httpOnly: true,
sameSite: 'strict',
path: '/',
secure: process.env.NODE_ENV !== 'development',
})
next()
}
function facebook(app) {
return app.get('/auth/facebook', (req, res, next) => {
Passport.authenticate('facebook', {
scope: ['email', 'public_profile']
})(req, res, next)
})
}
function facebookCallback(app) {
return app.get(
'/auth/callback/facebook',
Passport.authenticate('facebook', { session: false, failureRedirect: '/' }),
setCookie,
(req, res) => {
res.redirect('/profile')
},
)
}
function google(app) {
return app.get('/auth/google', (req, res, next) => {
Passport.authenticate('google', {
scope: [
'https://www.googleapis.com/auth/userinfo.email ',
'https://www.googleapis.com/auth/userinfo.profile ',
],
prompt: 'consent',
authType: 'rerequest',
accessType: 'offline',
})(req, res, next)
})
}
function googleCallback(app) {
return app.get(
'/auth/callback/google',
Passport.authenticate('google', { failureRedirect: '/', session: false }),
setCookie,
(req, res) => {
res.redirect('/profile')
},
)
}
module.exports = {
passport: {
facebook,
facebookCallback,
google,
googleCallback,
}
}
#./server/middleware/passport.js
const axios = require('axios')
const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const FacebookStrategy = require('passport-facebook').Strategy
passport.serializeUser((user, done) => {
done(null, user)
})
passport.deserializeUser((obj, done) => {
done(null, obj)
})
function verifyCallback(req, ... , done) {
process.nextTick(async () => {
try {
const options = {
baseURL: baseUrl, // My remote api url
method: 'POST',
url: '/auth/signin',
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify({
// email, fullname, etc
}),
}
const response = await axios(options)
return done(null, response.data)
} catch (error) {
const { response } = error
return done(JSON.stringify(response.data, null, 2), null)
}
})
}
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: callbackURLGoogle,
passReqToCallback: true,
}, verifyCallback))
passport.use(new FacebookStrategy({
clientID: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackURL: callbackURLFacebook,
enableProof: true,
profileFields: ['id', 'name', 'email', 'picture.type(large)'],
passReqToCallback: true,
}, verifyCallback))
module.exports = passport
I console.log() things, just to figure out if it falls to the correct sequence of flow, the console doesn't seem to log anything suspicious, is there's something i am missing here?
PS: i am also using next js with custom server
I was facing the same problem and was able to send cookies by using custom callback.
router.get('/google/callback', (req, res) => {
passport.authenticate('google', {session: false, failureRedirect:'/auth/google/failure'},
async(err, user) => {
// You can send cookies and data in response here.
})(req, res)
})
Please refer custom callback section in documentation for explanation.

How to setup authentication with graphql, and passport but still use Playground

After adding authentication to our backend Graphql server the "Schema" and "Docs" are no longer visible in the Graphql Playground. Executing queries when adding a token to the "HTTP HEADERS" in the Playground does work correctly when authenticated and not when a user isn't authenticated, so that's ok.
We disabled the built-in Playground from Apollo-server and used the middleware graphql-playground-middleware-express to be able to use a different URL and bypass authentication. We can now browse to the Playground and use it but we can't read the "Schema" or "Docs" there.
Trying to enable introspection didn't fix this. Would it be better to call passport.authenticate() in the Context of apollo-server? There's also a tool called passport-graphql but it works with local strategy and might not solve the problem. I've also tried setting the token in the header before calling the Playground route, but that didn't work.
We're a bit lost at this. Thank you for any insights you could give us.
The relevant code:
// index/ts
import passport from 'passport'
import expressPlayground from 'graphql-playground-middleware-express'
const app = express()
app.use(cors({ origin: true }))
app.get('/playground', expressPlayground({ endpoint: '/graphql' }))
app.use(passport.initialize())
passport.use(bearerStrategy)
app.use(
passport.authenticate('oauth-bearer', { session: false }),
(req, _res, next) => { next() }
)
;(async () => {
await createConnections()
const server = await new ApolloServer({
schema: await getSchema(),
context: ({ req }) => ({ getUser: () => req.user, }),
introspection: false,
playground: false,
})
server.applyMiddleware({ app, cors: false })
app.listen({ port: ENVIRONMENT.port }, () => { console.log(`Server ready`) })
})()
// passport.ts
import { IBearerStrategyOptionWithRequest, BearerStrategy, ITokenPayload } from passport-azure-ad'
import { Account } from '#it-portal/entity/Account'
export const bearerStrategy = new BearerStrategy( config,
async (token: ITokenPayload, done: CallableFunction) => {
try {
if (!token.oid) throw 'token oid missing'
const knownAccount = await Account.findOne({ accountIdentifier: token.oid })
if (knownAccount) return done(null, knownAccount, token)
const account = new Account()
account.accountIdentifier = token.oid
account.name = token.name
account.userName = (token as any).preferred_username
const newAccount = await account.save()
return done(null, newAccount, token)
} catch (error) {
console.error(`Failed adding the user to the request object: ${error}`)
}
}
)
I figured it out thanks to this SO answer. The key was not to use passport as middleware on Express but rather use it in the Graphql Context.
In the example code below you can see the Promise getUser, which does the passport authentication, being used in the Context of ApolloServer. This way the Playground can still be reached and the "Schema" end "Docs" are still accessible when run in dev mode.
This is also the preferred way according to the Apollo docs section "Putting user info on the context".
// apollo.ts
passport.use(bearerStrategy)
const getUser = (req: Express.Request, res: Express.Response) =>
new Promise((resolve, reject) => {
passport.authenticate('oauth-bearer', { session: false }, (err, user) => {
if (err) reject(err)
resolve(user)
})(req, res)
})
const playgroundEnabled = ENVIRONMENT.mode !== 'production'
export const getApolloServer = async () => {
return new ApolloServer({
schema,
context: async ({ req, res }) => {
const user = await getUser(req, res)
if (!user) throw new AuthenticationError('No user logged in')
console.log('User found', user)
return { user }
},
introspection: playgroundEnabled,
playground: playgroundEnabled,
})
}
The best thing is that you only need two functions for this to work: passport.use(BearerStrategy) and passport.authenticate(). This is because sessions are not used so we don't need to add it as Express middleware.
// index/ts
const app = express()
app.use(cors({ origin: true }))
;(async () => {
await createConnections()
const server = await getApolloServer()
server.applyMiddleware({ app, cors: false })
app.listen({ port: ENVIRONMENT.port }, () => { console.log(`Server ready`) })
})()
I hope this helps others with the same issues.

How can I share data between routes on the request object in Express?

I'm working on an application that authenticates with Spotify's API. I am using passport-spotify to do so. I need to be able to access a session ID a my root route (/).
While I'm able to set the session id during the /callback after authentication with Spotify, I can't then access the session id at /. Can someone please explain to me how to pass data between routes in Express so that I can access req.session.id in / after I've authenticated?
I'll share my endpoints here:
/
app.get('/', cors(), (req, res) => {
if (req.session.id != null) {
res.json({isAuthenticated: true })
} else {
res.json({isAuthenticated: false, message: 'Please log in.' })
}
})
Passport Strategy
passport.use(new SpotifyStrategy({
clientID: clientId,
clientSecret: clientSecret,
callbackURL: CALLBACK_URL
},
(accessToken, refreshToken, profile, done) => {
process.nextTick(function () {
let user = { spotifyId: profile.id, access_token: accessToken,
refresh_token: refreshToken }
return done(null, user)
})
}))
/auth/spotify
app.get('/auth/spotify',
passport.authenticate('spotify', {scope: ['user-read-email', 'user-
read-private'], showDialog: true}),
(req, res) => {
})
/callback
app.get('/callback', passport.authenticate('spotify', {
failureRedirect: '/', successRedirect: FRONTEND_URL }), (req, res,
next) => {
req.session.id = req.user.spotifyId
localStorage.setItem('access_token_' + req.session.id,
req.user.access_token)
localStorage.setItem('refresh_token_' + req.session.id,
req.user.refresh_token)
return next(null, req.session.id)
})
There is no way to share data in the way you asked your question, and the reason thereof is very simple, they are 2 different req Objects.
The what you can do, is define a separate middlware function, something like this :
function middlware(req, res, next) {
req.session.id = req.user.spotifyId;
next();
}
And you call that function in both of your routs as a middlware after the Spotify middlware.