-
-
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); | |
| }); |
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.
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?