Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
// MIT License
// Copyright (c) 2020 Szabolcs Gelencsér
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
const express = require('express');
const next = require('next');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const fetch = require('isomorphic-unfetch');
const { NODE_ENV, API_URL, PORT } = process.env;
const rtCookieName = 'refreshToken';
const catchErrors = (fn) => async (req, res) => {
try {
await fn(req, res);
} catch (e) {
console.log(new Date(Date.now()).toISOString(), e);
res.status(500);
res.send(e);
}
};
const fetchAPI = async (path, body, headers) => {
const res = await fetch(`${API_URL}/api/v1/${path}`, {
method: 'post',
headers: {
'content-type': 'application/json',
...headers,
},
body: JSON.stringify(body),
});
return {
body: await res.text(),
status: res.status,
headers: res.headers,
};
};
const forwardHeader = (res, apiRes, header) => {
if (apiRes.headers.get(header)) {
res.set(header, apiRes.headers.get(header));
}
};
const forwardResponse = (res, apiRes) => {
forwardHeader(res, apiRes, 'content-type');
forwardHeader(res, apiRes, 'www-authenticate');
// additional whitelisted headers here
res.status(apiRes.status);
res.send(apiRes.body);
};
const writeRefreshCookie = (res, refreshToken, refreshAge) => {
res.cookie(rtCookieName, refreshToken, {
path: '/api/v1/token',
// received in second, must be passed in as nanosecond
expires: new Date(Date.now() + refreshAge * 1000 * 1000).toUTCString(),
maxAge: refreshAge * 1000, // received in second, must be passed in as millisecond
httpOnly: true,
secure: NODE_ENV !== 'dev',
sameSite: 'Strict',
});
};
const forwardRefreshToken = (res, apiRes) => {
try {
const { refreshToken, refreshAge } = JSON.parse(apiRes.body);
writeRefreshCookie(res, refreshToken, refreshAge);
} catch { }
};
const nextApp = next({ dev: NODE_ENV === 'dev' });
nextApp.prepare().then(() => {
const server = express();
server.use(bodyParser.urlencoded({ extended: true }));
server.use(bodyParser.json());
server.use(cookieParser());
server.post('/api/v1/login', catchErrors(async (req, res) => {
const apiRes = await fetchAPI('login', req.body);
forwardRefreshToken(res, apiRes);
forwardResponse(res, apiRes);
}));
server.post('/api/v1/token/refresh', catchErrors(async (req, res) => {
const refreshToken = req.cookies[rtCookieName];
const apiRes = await fetchAPI('token/refresh', { refreshToken });
forwardRefreshToken(res, apiRes);
forwardResponse(res, apiRes);
}));
server.post('/api/v1/token/invalidate', catchErrors(async (req, res) => {
const refreshToken = req.cookies[rtCookieName];
const apiRes = await fetchAPI('token/invalidate', { refreshToken });
writeRefreshCookie(res, '', -1);
forwardResponse(res, apiRes);
}));
server.post('/api/v1/graphql', catchErrors(async (req, res) => {
const apiRes = await fetchAPI('graphql', req.body, {
Authorization: req.header('Authorization'),
});
forwardResponse(res, apiRes);
}));
server.all('*', nextApp.getRequestHandler());
server.listen(PORT, (err) => {
if (err) {
throw err;
}
console.log(`> Ready on port ${PORT}`);
});
})
.catch((err) => {
console.log('An error occurred, unable to start the server');
console.log(err);
});
@rtang03

This comment has been minimized.

Copy link

@rtang03 rtang03 commented May 4, 2020

Great work. The BBF pattern like this, is good.

@h3yduck

This comment has been minimized.

Copy link
Owner Author

@h3yduck h3yduck commented May 7, 2020

Glad you liked it, thank you!

@aravindarc

This comment has been minimized.

Copy link

@aravindarc aravindarc commented Jun 23, 2020

Excellent work, not well known, followed existing misleading tutorials into storing jwt-token in cookie, yours is the most secure way to go

@h3yduck

This comment has been minimized.

Copy link
Owner Author

@h3yduck h3yduck commented Jun 24, 2020

Thank you very much! I'm happy to receive such feedback.

@pherm

This comment has been minimized.

Copy link

@pherm pherm commented Aug 29, 2020

Good, a question :) this code is a client implementation in nexts js api folder or in back end side?

@h3yduck

This comment has been minimized.

Copy link
Owner Author

@h3yduck h3yduck commented Aug 30, 2020

Thanks! :) It's in the client repository like this:

server.js (bff_example.js here)
src/
    components/...
    pages/...

Next.js API routes would have worked as well I guess (didn't know them when this solution was implemented).
My only concern about Next.js API routes now is building the Next.js app once and re-using it among different environments (I've written about it in this post).

@pherm

This comment has been minimized.

Copy link

@pherm pherm commented Aug 30, 2020

Thank you for your reply ;) @h3yduck

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.