From 7af87824e3cd67b85d79cf46794b55fd36132afd Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Thu, 23 Dec 2021 14:24:13 +0000 Subject: [PATCH] Added the TokenProvider as a useful tool --- tools/react/README.md | 53 ++++++++++++++ tools/react/token-provider.jsx | 127 +++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 tools/react/README.md create mode 100644 tools/react/token-provider.jsx diff --git a/tools/react/README.md b/tools/react/README.md new file mode 100644 index 0000000..8d85b13 --- /dev/null +++ b/tools/react/README.md @@ -0,0 +1,53 @@ +# TokenProvider + +The MERN-template utilizes React's `useContext()` hook to share the auth-server's access token, effectively globally. Here is a quick rundown of how it works. + +# Enabling TokenProvider + +To enable the TokenProvider component, wrap your App component with it, like so: + +```jsx +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './pages/app'; +import TokenProvider from './pages/utilities/token-provider'; + +ReactDOM.render( + + + , + document.querySelector('#root') +); +``` + +# Accessing The Access Token + +To access the access token from your app, you simply use the `useContext` hook, like so: + +```jsx +import React, { useContext } from 'react'; +import { TokenContext } from '../utilities/token-provider'; + +const Component = props => { + //context + const authTokens = useContext(TokenContext); + + //use the access token + console.log(authTokens.accessToken); + + return
; +}; + +export default Component; +``` + +# Most Useful Features Provided + +The most useful features provided by TokenProvider are: + +* `tokenFetch()`, which wraps the `fetch()` API to ensure that your access token is valid +* `tokenCallback()`, which passes the authTokens as a parameter to any function passed into it +* `getPayload()`, which returns the payload of the accessToken (including as "email", "username", "admin", and "mod") +* `accessToken`, this will be falsy if the user is not logged in + diff --git a/tools/react/token-provider.jsx b/tools/react/token-provider.jsx new file mode 100644 index 0000000..da3ab63 --- /dev/null +++ b/tools/react/token-provider.jsx @@ -0,0 +1,127 @@ +import React, { useState, useEffect, createContext } from 'react'; +import decode from 'jwt-decode'; + +export const TokenContext = createContext(); + +//DOCS: tokenFetch() and tokenCallback() are actually closures here + +const TokenProvider = props => { + //state to be used + const [accessToken, setAccessToken] = useState(''); + const [refreshToken, setRefreshToken] = useState(''); + + //make the access and refresh tokens persist between reloads + useEffect(() => { + setAccessToken(localStorage.getItem("accessToken") || ''); + setRefreshToken(localStorage.getItem("refreshToken") || ''); + }, []); + + //update the stored copies + useEffect(() => { + localStorage.setItem("accessToken", accessToken); + localStorage.setItem("refreshToken", refreshToken); + }, [accessToken, refreshToken]); + + //wrap the default fetch function + const tokenFetch = async (url, options) => { + //use this? + let bearer = accessToken; + + //if expired (10 minutes, normally) + const expired = new Date(decode(accessToken).exp * 1000) < Date.now(); + + if (expired) { + //BUGFIX: if logging out, just skip over the refresh token + if (url === `${process.env.AUTH_URI}/auth/logout`) { + return fetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Authorization': `Bearer ${bearer}` + }, + body: JSON.stringify({ + token: refreshToken + }) + }); + } + + //ping the auth server for a new token + const response = await fetch(`${process.env.AUTH_URI}/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify({ + token: refreshToken + }) + }); + + //any errors, throw them + if (!response.ok) { + throw `${response.status}: ${await response.text()}`; + } + + //save the new auth stuff (setting bearer as well) + const newAuth = await response.json(); + + setAccessToken(newAuth.accessToken); + setRefreshToken(newAuth.refreshToken); + bearer = newAuth.accessToken; + } + + //finally, delegate to fetch + return fetch(url, { + ...(options || {}), + headers: { + ...(options || { headers: {} }).headers, + 'Authorization': `Bearer ${bearer}` + } + }); + }; + + //access the refreshed token via callback + const tokenCallback = async (cb) => { + //if expired (10 minutes, normally) + const expired = new Date(decode(accessToken).exp * 1000) < Date.now(); + + if (expired) { + //ping the auth server for a new token + const response = await fetch(`${process.env.AUTH_URI}/auth/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify({ + token: refreshToken + }) + }); + + //any errors, throw them + if (!response.ok) { + throw `${response.status}: ${await response.text()}`; + } + + //save the new auth stuff (setting bearer as well) + const newAuth = await response.json(); + + setAccessToken(newAuth.accessToken); + setRefreshToken(newAuth.refreshToken); + + //finally + return cb(newAuth.accessToken); + } else { + return cb(accessToken); + } + }; + + return ( + decode(accessToken) }}> + {props.children} + + ) +}; + +export default TokenProvider;