I have started using graphql in my express js project but i am wondering how to protect some of my GraphQL query.
Previously i used passport js(JWT) for this and that works great. It was really easy to secure route but with graphql(express-graphql) i couldn't find any solution.
Furthermore it will be nice to have some kind of role based solution to protect particular fields. Is there any good tutorial how to secure graphQL ?
Last I checked there weren't any really good tutorials out there that show how to secure a GraphQL endpoint. However, the consensus in the community (GraphQL and Apollo slack channels) is that it's best to do Authentication separate from GraphQL (eg. using Passport) and do authorization in your resolve functions, possibly by decorating them with some role-based auth.
The best link I can provide at the moment is this post I wrote a while ago about setting up Authentication for a GraphQL endpoint with Passport.js. I hope it helps!
I'm currently working on a Full-stack GraphQL tutorial for React + Node.js with Apollo for which I'm planning to do a part about Auth. I'll try to update this answer as soon as I've published it.
I'm currently evaluating the potential of authorization over resolvers with express, passport and jwt. It's not fully tested, but it works.
For this to work, you need to pass at least the request in the context:
const graphql = graphqlExpress((req, res) => {
return ({
schema,
rootValue: resolver,
// For query authorization. Ideally, Passport will handle all requests and authenticate
// each one for the current user. The queries will fetch data exclusively related to that user.
context: { req, res },
});
});
// The api
app.use('/api', graphql);
In this case I promisified the passport authentication:
const auth = (req, res) => new Promise((resolve, reject) => {
passport.authenticate('jwt', { session: false }, (err, user) => {
if (err) reject(err);
if (user) resolve(user);
else reject('Unauthorized');
})(req, res);
});
const resolver = {
users: (root, ctx) => auth(ctx.req, ctx.res)
.then(() => User.find({}, (err, res) => res))
.catch((err) => {
throw new Error(err);
}),
};
Since there's not that many examples on how to cover this, I've struggled to make it simple, but I think I did it well.
Here are the resources I used to get to this point:
how to get passport.authenticate local strategy working with async/await pattern
https://matoski.com/article/jwt-express-node-mongoose/
https://dev-blog.apollodata.com/auth-in-graphql-part-2-c6441bcc4302
I modified the code as below in order to get correct errors.
const getUser = (req: Express.Request, res: Express.Response) =>
new Promise((resolve, reject) => {
passport.authenticate('jwt', {session: false}, (err, payload, info) => {
if (err) reject(info)
if (payload) resolve(payload)
else reject(info)
})(req, res)
})
const getApolloServer = async () => {
return new ApolloServer({
schema: await buildSchema({
resolvers: [] // Your resolvers
}),
context: async ({ req, res }) => {
try {
const user = await getUser(req, res)
return { user }
} catch(err: any) {
console.log(err)
res.status(401).json(err.message)
}
},
introspection: true,
playground: true,
})
}
Related
I've been trying to make an express middleware that sends an email using Nodemailer after the previous middleware finishes. I've come up with a few different designs, but ultimately each different version has it's drawback.
Ultimately, I would like the middleware to have a response from the previous middleware. If it is a success, then send a success email, otherwise, send an error email.
I came up with a dual design where one variation pushes to an error middleware, and a success leads to the next middleware. This contains some slight issues of sending multiple headers, specifically on an the second middleware erroring. I could say, if the mail errors out, do nothing. But that doesn't seem right. If anyone has any suggestions on a good design, that would be great.
From what you described, I would suggest not to create different middleware for that, but to just create one generic email function that would handle different type of messages. Then, just use that function in the first middleware and pass different parameters based on use case (success/error).
email-controller.js
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: process.env.EMAIL_PORT,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
exports.send_email_message = (send_to, subject, message) => {
return new Promise((resolve, reject) => {
const email_message = {
from: { name: process.env.EMAIL_FRIENDLY_NAME },
to: send_to,
subject: subject,
text: message
};
transporter.sendMail(email_message).then(() => {
resolve(true);
}).catch((error) => {
reject(false);
});
})
}
custom-router.js
const { send_email_message } = require('./email-controller');
const express = require('express');
const router = express.Router();
router.get('/custom-middleware', async (req, res, next) => {
try {
// You can calculate "success" variable based on your custom logic
if(success){
await send_email_message('example#gmail.com', 'Success', 'This is body of success message.');
return res.status(200).json({ success: true });
} else {
await send_email_message('example#gmail.com', 'Error', 'This is body of error message.');
return res.status(400).json({ success: false });
}
} catch(error) {
return res.status(400).json({ success: false });
}
});
module.exports = router;
I was trying to make a routes for each ID I using a forEach loop but It stay loading until timeout reaches, all expected values are in place, all good but the second route is not running, I was fighting it despretly until now. I made sure there is a problem.
server.js
const router = require('express').Router();
function isAuthorized(req, res, next) {
if (req.user) {
next();
}
else {
res.redirect('/login')
}
}
let myguild = [];
router.get(`*`, isAuthorized, (req, res) => {
res.status(200);
console.log("wow");
console.log(req.user.guilds.length)
req.user.guilds.forEach(guild => {
myguild.push(guild);
})
console.log("Finished");
myguild.forEach(guild => {
console.log('Started')
router.get(guild.id, (req, res) => { // here is the problem
console.log("uh")
res.send("HAMBURGER")
console.log(req, res, guild)
})
console.log("Outed")
})
});
module.exports = router;
output:
wow
23
Finished
Started
Outed
Started
Outed
Started
Outed
Star... 'there is more but this is enough'
It should behave and run within server/${guild.id} but got (failed) request
Any Ideas?
You might need to redesign the API to better fit what you're trying to accomplish. If you already know which guilds are available then you'd need to create those before the server is initialized.
Even if they come from a database or are dynamic, you can loop through the guild "options" and create endpoints then provide access to them only if the user is qualified.
const { guilds } = require('./config')
const guildHandler = (req, res) => {
// Assuming you're doing more here
res.send('Hamburger')
}
guilds.forEach(guild => router.get(`/guilds/${guildId}`, guildHandler)
Or if you are NOT doingg something different in the middleware for each guild then you could just have a single route for guild.
router.get('/guilds/:guildId, guildHandler)
Not really sure what you're trying to accomplish but checkout out the Express docs. They solve most use cases fairly easily.
https://expressjs.com/en/api.html#req
You never call res.end() from your outer res.get() handler, so the request never completes.
And, with respect, creating route handlers like that in a loop is a mistake. It will lead to real performance trouble when your app gets thousands of guilds.
You'll want to use just one route, with a named route parameter, something like this.
const createError = require('http-errors')
router.get(':guildid', isAuthorized, (req, res, next) => {
const guildid = req.params.guildid
if (req.user.guilds.includes(guild)) {
console.log("uh")
res.send("HAMBURGER").end()
console.log(req, res, guildid)
} else {
next(createError(404, guildId + ' not found'))
}
})
Thanks for everyone helped.
Inspired answer
Final Result:
server.js
router.get('/:guildid', isAuthorized, (req, res, next) => {
console.log('started')
const guildid = req.params.guildid
if (req.user.guilds.some(guild => guild.id === guildid)) {
console.log('uh')
res.send("HAMBURGER").end()
} else {
res.sendStatus(404);
}
})
I've build an KeystoneJS v5 app with a custom Express instance to serve data.
My data is stored in a Postgres database which contains the following model :
CREATE TABLE "Link" (
id integer DEFAULT PRIMARY KEY,
customer text,
slug text
);
I have built dynamic routes based on slug attributes :
knex('Link').select('slug').then(function(result){
const data = result.map(x => x.slug)
data.forEach(url => {
express.get(`/${url}`, function (req, res) {
res.render('index');
})
});
});
Everything works as expected but I have to restart my node server each time I insert new slug in the Link table.
Do you know how to avoid this ?
Thanks!
This might not be the complete answer but here's something along the lines of what you're looking for.
Ensure we accept any slug on the route
I've used an async function to await the results from the DB
Render the index view (if the slug was found) or reply with a 404 if no entry was found within the table
Word of warning, I have not used keystoneJS or this knexjs package, so it might not be 100% correct, but should be a good example of what we're trying to achieve. I'm assuming knexjs rejects the promise if no results are found, but I'm not sure.
express.get(`/:slug`, async function (req, res) {
try {
const result = await knex('Link')
.where({ slug: req.params.slug })
.select('id');
return res.render('index');
} catch {
return res.status(404).send({ message: "Not found" });
}
});
If you're running an older version of Node.js, here's a version without async.
express.get(`/:slug`, function (req, res) {
knex('Link')
.where({ slug: req.params.slug })
.select('id')
.then((result) => {
return res.render('index');
})
.catch(() => {
return res.status(404).send({ message: "Not found" });
});
});
I'm not a pro in any way but I've started and ApolloServer/Express backend to host a site where I will have public parts and private parts for members. I am generating at JWT token in the login mutation and get's it delivered to the client.
With context I want to check if the token is set or not and based on this handle what GraphQL queries are allowed. My Express/Apollo server looks like this at the moment.
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
// get the user token from the headers
const token = (await req.headers.authorization) || '';
if (token) {
member = await getMember(token);
}
}
});
The problem is that this locks down the GraphQL API from any queries and I want/need to reach signup/login mutations for example.
Could anyone spread some light on this to help me understand what I need to do to get this to work.
the way i am doing it is that i will construct auth middleware even before graphql server as sometimes is needed to have information about authenticated user also in other middlewares not just GraphQL schema. Will add some codes, that you need to get it done
const auth = (req, res, next) => {
if (typeof req.headers.authorization !== 'string') {
return next();
}
const header = req.headers.authorization;
const token = header.replace('Bearer ', '');
try {
const jwtData = jwt.verify(token, JWT_SECRET);
if (jwtData && jwtData.user) {
req.user = jwtData.user;
} else {
console.log('Token was not authorized');
}
} catch (err) {
console.log('Invalid token');
}
return next();
};
This way i am injecting the user into each request if the right token is set. Then in apollo server 2 you can do it as follows.
const initGraphQLserver = () => {
const graphQLConfig = {
context: ({ req, res }) => ({
user: req.user,
}),
rootValue: {},
schema,
};
const apolloServer = new ApolloServer(graphQLConfig);
return apolloServer;
};
This function will initiate ApolloServer and you will apply this middleware in the right place. We need to have auth middleware before applyin apollo server 2
app.use(auth);
initGraphQLserver().applyMiddleware({ app });
assuming the app is
const app = express();
Now you will have user from user jwtData injected into context for each resolver as "user", or in req.user in other middlewares and you can use it for example like this. This is me query for saying which user is authenticated or not
me: {
type: User,
resolve: async (source, args, ctx) => {
const id = get(ctx, 'user.id');
if (!id) return null;
const oneUser = await getOneUser({}, { id, isActive: true });
return oneUser;
},
},
I hope that everything make sense even with fractionized code. Feel free to ask any more questions. There is definitely more complex auth, but this basic example is usually enough for simple app.
Best David
I'm authenticating calls to my express API using passport. I have a pretty standard setup:
/* Passport Setup */
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
secretOrKey: config.auth.passport.key,
}
passport.use(
'jwt',
new JWT.Strategy(jwtOptions, (payload, done) => {
console.log('Using JWT Strategy')
User.findOne({ email: payload.email }, (err, user) => {
if (err) {
return done(err, false)
}
if (user) {
done(null, user)
} else {
done(null, false)
}
})
}),
)
/* Middleware */
const checkToken = passport.authenticate('jwt', { session: false })
const logAuthInfo = (req, res, next) => {
console.log(req.headers)
console.log(req.user)
}
/* Routes */
app.use(passport.initialize())
app.use('/graphql', checkToken, logAuthInfo, graphqlHTTP(graphQLConfig))
// other REST routes, including login
My login route returns a JWT, and when a request is made to /graphql with this token, everything works. But, an unauthenticated request (with no token) returns a 401. What I'd like to do differently is use the checkToken middleware on all requests, assigning req.user to either the authenticated user data or false. I'd then handle any authorization elsewhere.
When I make a request without a token, I don't see 'Using JWT Strategy' log to the console, so that middleware isn't even running.
Any ideas?
Ok, I figured this out shortly after posting this. For anyone coming here with the same question -- the solution is not using passport-jwt to achieve this, but rather the underlying jsonwebtoken.
My working middleware now looks like:
const jwt = require('jsonwebtoken')
const PassportJwt = require('passport-jwt')
const getUserFromToken = (req, res, next) => {
const token = PassportJwt.ExtractJwt.fromAuthHeaderWithScheme('Bearer')(req)
jwt.verify(token, jwtSecret, (err, decoded) => {
if (err) {
req.user = false
next()
return
}
req.user = decoded
next()
})
}
app.use(getUserFromToken)
app.use('/graphql', graphqlHTTP(graphQLConfig))
// Elsewhere, in my GraphQL resolvers
const userQuery = (obj, args, request, info) => {
// ^^^^^^^
// I've also seen this parameter referred to as 'context'
console.log(request.user) // either 'false' or the serialized user data
if (req.user) {
// do things that this user is allowed to do...
} else {
// user is not logged in, do some limited things..
}
}