June 1, 2021
This blog details usage of cookies on a Postgraphile-based application. We will be using Postgraphile with Express for processing the cookies, but any similar library can be used.
Cookies can be a very safe method for storage on the client side. They can be set as:
We will start off with a base Express setup generated with express-generator.
const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const app = express();
require("dotenv").config();
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, "public")));
// Use secret key to sign the cookies on creation and parsing
app.use(cookieParser(process.env.SECRET_KEY));
// Catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// Error handler
app.use(function (err, req, res) {
// Set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// Render the error page
res.status(err.status || 500);
res.render("error");
});
module.exports = app;
From Postgraphile's usage library page for adding Postgraphile to an express app:
app.use(
postgraphile(
process.env.DATABASE_URL || "postgres://user:pass@host:5432/dbname",
"public",
{
watchPg: true,
graphiql: true,
enhanceGraphiql: true,
}
)
);
Now for the table setup. We need a private user_accounts
table and a method
named authenticate_user
that will return a JWT token of the form:
{
token: 'jwt_token_here',
username: '',
...anyOtherDetails
}
We will not be detailing table creation or authentication as there are many ways to go about it. But if you need help, Postgraphile security is the page to rely on.
To attach a cookie to the request, we will use the @graphile/operation-hooks
library which is open-sourced
on Github.
npm install @graphile/operation-hooks
# OR
yarn add @graphile/operation-hooks
To add the library to the app:
const { postgraphile, makePluginHook } = require("postgraphile");
const pluginHook = makePluginHook([
require("@graphile/operation-hooks").default,
// Any more PostGraphile server plugins here
]);
app.use(
postgraphile(
process.env.DATABASE_URL || "postgres://user:pass@host:5432/dbname",
"public",
{
watchPg: true,
graphiql: true,
enhanceGraphiql: true,
pluginHook,
appendPlugins: [
// You will be adding the hooks here
],
}
)
);
The plugin allows for two different types of hooks:
Since accessing cookies is a JavaScript operation, we will be concentrating on the second type.
To hook the plugin into the build system, we can use the addOperationHook
method.
module.exports = function OperationHookPlugin(builder) {
builder.hook("init", (_, build) => {
// Register our operation hook (passing it the build object):
// setAuthCookie is a function we will define later.
build.addOperationHook(useAuthCredentials(build));
// Graphile Engine hooks must always return their input or a derivative of
// it.
return _;
});
};
If this is contained in a file named set-auth-cookie.js
, then the plugin can
be added to the append plugins array as follows:
{
appendPlugins: [
require('./set-auth-cookie.js'),
],
}
The function to be executed receives two arguments: build
process and the
current fieldContext
.
The fieldContext
consists of fields that can be used to narrow down the
mutation or query that we want to target; e.g. if the hook is to run only on
mutations, we can use the fieldContext.isRootMutation
field.
const useAuthCredentials = build => fieldContext => {
const { isRootMutation } = fieldContext;
if (!isRootMutation) {
// No hook added here
return null;
}
};
To direct the system on usage of the plugin, we have to return an object with
before
, after
or error
fields. Here is how these keywords can be used:
(comments are from the example repository)
return {
// An optional list of callbacks to call before the operation
before: [
// You may register more than one callback if you wish. They will be mixed in with the callbacks registered from other plugins and called in the order specified by their priority value.
{
// Priority is a number between 0 and 1000. If you're not sure where to put it, then 500 is a great starting point.
priority: 500,
// This function (which can be asynchronous) will be called before the operation. It will be passed a value that it must return verbatim. The only other valid return is `null` in which case an error will be thrown.
callback: logAttempt,
},
],
// As `before`, except the callback is called after the operation and will be passed the result of the operation; you may return a derivative of the result.
after: [],
// As `before`; except the callback is called if an error occurs; it will be passed the error and must return either the error or a derivative of it.
error: [],
};
Since we want our action to happen after we get result from the mutation, we
will add it to the after
array.
const useAuthCredentials = build => fieldContext => {
const { isRootMutation, pgFieldIntrospection } = fieldContext;
if (!isRootMutation) {
// No hook added here
return null;
}
if (
!pgFieldIntrospection ||
// Name of the mutation is authenticateUser
pgFieldIntrospection.name !== "authenticateUser"
) {
// narrowing the scope down to the mutation we want
return null;
}
return {
before: [],
after: [
{
priority: 1000,
callback: (result, args, context) => {
// The result is here, so we can access accessToken and username.
console.log(result);
},
},
],
error: [],
};
};
Since the functionality is inside the plugin hook, we do not have the express result to set the cookie 😞.
But we do have an escape hatch with the third argument: context
. Postgraphile
allows us to pass functions or values into the context variable from the
postgraphile instance.
app.use(
postgraphile(process.env.DATABASE_URL, "public", {
async additionalGraphQLContextFromRequest(req, res) {
return {
// Function to set the cookie passed into the context object
setAuthCookie: function (authCreds) {
res.cookie("app_creds", authCreds, {
signed: true,
httpOnly: true,
secure: true,
// Check if you want to include SameSite cookies here, depending on your hosting.
});
},
};
},
})
);
We can now set the cookie inside the plugin hook.
{
priority: 1000,
callback: (result, args, context) => {
// This function is passed from additionalGraphQLContextFromRequest as detailed in the snippet above
context.setAuthCookie(result);
}
}
We have already added the cookieParser
with SECRET_KEY
, so express will
parse the cookies for us.
But we probably want them to be accessible inside SQL functions for
Postgraphile. That is how we can determine if the user is signed in or what
their permissions are. To do that, Postgraphile provides a pgSettings
object.
app.use(
postgraphile(process.env.DATABASE_URL, "public", {
pgSettings: async req => ({
user: req.signedCookies["app_creds"],
}),
})
);
Inside an SQL function, the variables passed from settings can be accessed like this:
current_setting('user')
That's all 🎉. We can store any details in cookies, retrieve them on the Express end and use them inside Postgres functions for authentication or authorization.
Check out operation-hooks plugin for more details.
If this blog was helpful, check out our full blog archive.