-
-
Save h3yduck/ff401b32b92c14ef66879c52135b11d7 to your computer and use it in GitHub Desktop.
// 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); | |
}); |
Thank you for your reply ;) @h3yduck
Hey, i haven't understood, where do you store a refresh token on Auth service?
Hey, I stored it in a DB with a TTL: https://stackoverflow.com/questions/59511628/is-it-secure-to-store-a-refresh-token-in-the-database-to-issue-new-access-toke/59511813#59511813
Hey, I stored it in a DB with a TTL: https://stackoverflow.com/questions/59511628/is-it-secure-to-store-a-refresh-token-in-the-database-to-issue-new-access-toke/59511813#59511813
thank you !
Hey, I am trying to implement this pattern in a React app but I'm finding it difficult to create a sever like this in same domain as my react app. Can you give me an example of how to implement this pattern in React?
hey @SpecterHunt 👋 this pattern must be implemented on the server side (and React is a client-side framework).
The example server hosts a React app as well with Next.js, but the code itself (doing the proxying) runs on the server side.
hey @SpecterHunt 👋 this pattern must be implemented on the server side (and React is a client-side framework).
The example server hosts a React app as well with Next.js, but the code itself (doing the proxying) runs on the server side.
Hi @szabolcsgelencser thank you for the reply. What I meant to ask was, to implement this pattern does your proxy server and client resides in same domain/subdomain?(because of HttpOnly and same site cookies)
Ah, I see, sorry I misunderstood 🙂
Yes they do, I solved it by hosting the React app from the proxy as well (but you can solve it several different ways based on your hosting environment - eg. Kubernetes Ingress also supports routing to various services based on HTTP path prefix having the same domain).
Thank you. Now I have understood the implementation. @szabolcsgelencser I have few more things to ask. Can we connect on LinkedIn?
I've requested you for the connection at LinkedIn. My profile is named Ayush Tripathi.
Thanks! :) It's in the client repository like this:
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).