From b8e4b33421993d91d5a42621f0063f12475680db Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Wed, 10 Mar 2021 18:54:20 +1100 Subject: [PATCH] Tokens are tentatively working correctly, read more They also seem to be refreshing correctly too, when tokenFetch() is used. --- client/client.jsx | 5 +- client/components/pages/login.jsx | 39 ++++---- client/components/pages/signup.jsx | 16 ++-- client/components/panels/header.jsx | 61 ++++++------- .../components/utilities/token-provider.jsx | 88 +++++++++++++++++++ client/utilities/token-client.js | 62 ------------- 6 files changed, 143 insertions(+), 128 deletions(-) create mode 100644 client/components/utilities/token-provider.jsx delete mode 100644 client/utilities/token-client.js diff --git a/client/client.jsx b/client/client.jsx index e98275d..1b52ed2 100644 --- a/client/client.jsx +++ b/client/client.jsx @@ -5,8 +5,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/app'; +import TokenProvider from './components/utilities/token-provider'; ReactDOM.render( - , + + + , document.querySelector('#root') ); diff --git a/client/components/pages/login.jsx b/client/components/pages/login.jsx index 08c6a46..94a2ea5 100644 --- a/client/components/pages/login.jsx +++ b/client/components/pages/login.jsx @@ -1,18 +1,14 @@ -import React, { useState, useRef } from 'react'; +import React, { useContext, useRef } from 'react'; import { Redirect } from 'react-router-dom'; -import { setToken, getToken } from '../../utilities/token-client'; +import { TokenContext } from '../utilities/token-provider'; const LogIn = props => { - //if logged in - const [tok, setTok] = useState(null); + //context + const authTokens = useContext(TokenContext); - getToken() - .then(token => setTok(token)) - .catch(e => console.error(e)) - ; - - if (tok) { + //misplaced? + if (authTokens.accessToken) { return ; } @@ -27,13 +23,16 @@ const LogIn = props => { async evt => { //on submit evt.preventDefault(); - const [result, redirect] = await handleSubmit(emailRef.current.value, passwordRef.current.value); - if (result) { - alert(result); + const [err, newTokens] = await handleSubmit(emailRef.current.value, passwordRef.current.value); + if (err) { + alert(err); } - //redirect - if (redirect) { + //save auth tokens and redirect + if (newTokens) { + authTokens.setAccessToken(newTokens.accessToken); + authTokens.setRefreshToken(newTokens.refreshToken); + props.history.push('/'); } } @@ -71,18 +70,16 @@ const handleSubmit = async (email, password) => { }) }); + //handle errors if (!result.ok) { const err = `${result.status}: ${await result.text()}`; console.error(err); return [err, false]; } - //save the auth tokens - const authTokens = await result.json(); - - await setToken(authTokens.accessToken, authTokens.refreshToken); - - return [null, true]; + //return the new auth tokens + const newTokens = await result.json(); + return [null, newTokens]; }; export default LogIn; \ No newline at end of file diff --git a/client/components/pages/signup.jsx b/client/components/pages/signup.jsx index 6621857..905e389 100644 --- a/client/components/pages/signup.jsx +++ b/client/components/pages/signup.jsx @@ -1,22 +1,18 @@ -import React, { useState, useRef } from 'react'; +import React, { useContext, useRef } from 'react'; import { Redirect } from 'react-router-dom'; -import { getToken } from '../../utilities/token-client'; +import { TokenContext } from '../utilities/token-provider'; //utilities const validateEmail = require('../../../common/utilities/validate-email.js'); const validateUsername = require('../../../common/utilities/validate-username.js'); const SignUp = props => { - //if logged in - const [tok, setTok] = useState(null) + //context + const authTokens = useContext(TokenContext); - getToken() - .then(token => setTok(token)) - .catch(e => console.error(e)) - ; - - if (tok) { + //misplaced? + if (authTokens.accessToken) { return ; } diff --git a/client/components/panels/header.jsx b/client/components/panels/header.jsx index 7e8fadd..e420a23 100644 --- a/client/components/panels/header.jsx +++ b/client/components/panels/header.jsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import React, { useContext } from 'react'; import { Link } from 'react-router-dom'; -import { getToken, getRefreshToken, clearToken } from '../../utilities/token-client'; +import { TokenContext } from '../utilities/token-provider'; const Visitor = () => { return ( @@ -14,51 +14,44 @@ const Visitor = () => { }; const Member = () => { + const authTokens = useContext(TokenContext); + return (
Account - - Log out + { /* Logout button logs you out of the server too */ } + { + const result = await authTokens.tokenFetch(`${process.env.AUTH_URI}/logout`, { //NOTE: this gets overwritten as a bugfix + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + }, + body: JSON.stringify({ + token: authTokens.refreshToken + }) + }); + + //any problems? + if (!result.ok) { + console.error(await result.text()); + } else { + authTokens.setAccessToken(''); + authTokens.setRefreshToken(''); + } + }}>Log out
); }; -const logout = async () => { - console.log('loging out') - const token = getToken(); - - //send to the auth server - const result = await fetch(`${process.env.AUTH_URI}/logout`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ - token: getRefreshToken() - }) - }); - - if (result.ok) { - await clearToken(); - } else { - console.error(await result.text()); - } -}; - const Header = () => { - const [tok, setTok] = useState(null); - - getToken() - .then(token => setTok(token)) - .catch(e => console.error(e)) - ; + const authTokens = useContext(TokenContext); return (

MERN Template

- { tok ? : } + { authTokens.accessToken ? : }
); }; diff --git a/client/components/utilities/token-provider.jsx b/client/components/utilities/token-provider.jsx new file mode 100644 index 0000000..1788707 --- /dev/null +++ b/client/components/utilities/token-provider.jsx @@ -0,0 +1,88 @@ +import React, { useState, useEffect, createContext } from 'react'; +import decode from 'jwt-decode'; + +export const TokenContext = createContext(); + +const TokenProvider = props => { + 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") || ''); + }, []) + + React.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) { + //ping the auth server for a new token + const response = await fetch(`${process.env.AUTH_URI}/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(); + + setAccess(newAuth.accessToken); + setRefresh(newAuth.refreshToken); + bearer = newAuth.accessToken; + + //BUGFIX: logging out correctly requires the new refresh token + if (url == `${process.env.AUTH_URI}/logout`) { + console.log(`logging out with refresh token: ${newAuth.refreshToken}`) + return fetch(`${process.env.AUTH_URI}/logout`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + 'Authorization': `Bearer ${bearer}` + }, + body: JSON.stringify({ + token: newAuth.refreshToken + }) + }); + } + } + + //finally, delegate to fetch + return fetch(url, { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${bearer}` + } + }); + }; + + return ( + + {props.children} + + ) +}; + +export default TokenProvider; diff --git a/client/utilities/token-client.js b/client/utilities/token-client.js deleted file mode 100644 index 626358c..0000000 --- a/client/utilities/token-client.js +++ /dev/null @@ -1,62 +0,0 @@ -import decode from 'jwt-decode'; -import Cookies from 'universal-cookie'; - -export async function setToken(access, refresh) { - const cookies = new Cookies(); - cookies.set('access', access, { path: '/' }); - cookies.set('refresh', refresh, { path: '/' }); -}; - -export async function getToken() { - const cookies = new Cookies(); - - try { - const access = cookies.get('access'); - const refresh = cookies.get('refresh'); - - if (!access || !refresh) { - return null; - } - - //if expired, refresh - if (new Date(decode(access).exp * 1000) < Date.now()) { - const result = await fetch(`${process.env.AUTH_URI}/token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - }, - body: JSON.stringify({ - token: refresh - }) - }); - - const authTokens = await result.json(); - - await setToken(authTokens.accessToken, authTokens.refreshToken); - - return authTokens.accessToken; - } else { - return access; - } - } - catch (e) { - console.error(e); - return null; - } -}; - -export async function getRefreshToken() { - const cookies = new Cookies(); - - const refresh = cookies.get('refresh'); - - return refresh || null; -}; - -export async function clearToken() { - const cookies = new Cookies(); - - cookies.remove('access'); - cookies.remove('refresh'); -} \ No newline at end of file