JSON Web Token (JWT) is a standard used to create access tokens digitally signed with JSON Web Signature (JWS).
For the purpose of authentication, a JWT is issued by the server. The token has a JSON payload that contains information specific to the user. This token can be used by clients when talking to APIs (by sending it along as an HTTP header) so that the APIs can identify the user from the token and take user specific action.
Potential problems storing JWT
We need to save our JWT token, so that we can forward it to our API as a header. You might be tempted to persist it in local storage, but this is prone to XSS attacks.
Creating cookies on the client to save the JWT will also be prone to XSS. If it can be read on the client from Javascript outside your app – it can be stolen. You might think an HttpOnly cookie will help, but cookies are vulnerable to CSRF attacks. It is important to note that HttpOnly and sensible CORS policies cannot prevent CSRF form-submit attacks.
Where do we save it then?
In memory.
To demonstrate, we are going to develop a simple React app, that will help us check the validity of our authorization token.
This tutorial assumes that you already have at least a basic knowledge of React and that you are familiar with JSON Web Token and its concept, thus we will only concentrate on how to store JWT in our React app.
Setup
The focus of this tutorial is to show how to properly store JWT tokens on client-side apps. The backend tech stack is irrelevant, and in our case, we will use Node.js with Express.
To initialize our app we are going to use the create-react-app cli:
create-react-app jwt-storing-tutorial
Now that we have our template app, we can start it with:
npm start
Great! Lets get started!
First things first: Lets create in memory storage
Lets add the following code that shows a way to store JWT in memory:
/src/core/services/inMemoryJwtService.js
const inMemoryJWTManager = () => { let inMemoryJWT = null; const getToken = () => inMemoryJWT; const setToken = (token) => { inMemoryJWT = token; return true; }; const deleteToken = () => { inMemoryJWT = null; return true; }; return { getToken, setToken, deleteToken }; }; export default inMemoryJWTManager();
We are using a closure to instantiate inMemoryJWT variable, which holds out JWT token.
Our main component
Now let’s create a component which will hold the main logic of our app. This component will be able to validate the token we possess, check if we are logged in or not, visualize the proper JSX code according to that and also sign us out. Add the following code:
/src/core/components/validator/Validator.jsx
import React, { useState, useEffect } from 'react'; import Login from './../login/Login'; import inMemoryJwt from './../../services/inMemoryJwtService'; import { ajaxValidateToken } from './../../services/authenticationService'; const Validator = () => { const [showSuccessMsg, setShowSuccessMsg] = useState(false); const [showErrorMsg, setShowErrorMsg] = useState(false); const [isUserLogged, setIsUserLogged] = useState(false); useEffect(() => { if (inMemoryJwt.getToken()) { setIsUserLogged(true); } }, []); const logoutHandler = event => { // TODO: implement later }; const checkTokenValidityHandler = event => { event.preventDefault(); ajaxValidateToken() .then(() => { setShowSuccessMsg(true); setTimeout(() => { setShowSuccessMsg(false); }, 3000); }).catch(err => { console.error(err); setShowErrorMsg(true); setTimeout(() => { setShowErrorMsg(false); }, 3000); }); }; return ( <div className="Validator"> <button type="button" onClick={checkTokenValidityHandler}>Check token validity</button> { isUserLogged ? <button type="button" onClick={logoutHandler}>Logout</button> : <Login setIsUserLogged={setIsUserLogged} /> } { showSuccessMsg ? <div>Success: Token is valid</div> : null } { showErrorMsg ? <div>Error: Token is invalid</div> : null } </div> ); } export default Validator;
The ajaxValidateToken() is the main function of our app. It makes a request to our API with the JWT token in its headers, the API validates the token and returns a response. Let’s update our App.js with the newly created component. At the end, it should look something like this:
/src/App.js
import logo from './logo.svg'; import './App.css'; import Validator from './core/components/validator/Validator'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <Validator /> </header> </div> ); } export default App;
Login form
Now we implement our login form. Add the following code:
/src/core/components/login/Login.jsx
import React, { useState } from 'react'; import { ajaxLogin } from './../../services/authenticationService'; import inMemoryJwt from './../../services/inMemoryJwtService'; const Login = ({ setIsUserLogged }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showSuccessMsg, setShowSuccessMsg] = useState(false); const [showErrorMsg, setShowErrorMsg] = useState(false); const onChangeHandler = event => { const value = event.target.value; switch(event.target.name) { case 'username': setUsername(value); break; case 'password': setPassword(value); break; default: break; } }; const onSubmitHandler = event => { event.preventDefault(); const user = { username, password }; ajaxLogin(user) .then(res => { setShowSuccessMsg(true); const { token } = res.data; inMemoryJwt.setToken(token); setTimeout(() => { setIsUserLogged(true); }, 3000); }).catch(err => { console.error(err); setShowErrorMsg(true); setTimeout(() => { setShowErrorMsg(false); }, 3000); }); }; return ( <div className="Login"> <form onSubmit={onSubmitHandler}> <div> <label htmlFor="username">Username:</label> <input name="username" value={username} onChange={onChangeHandler} /> </div> <div> <label htmlFor="password">Password:</label> <input name="password" value={password} type="password" onChange={onChangeHandler} /> </div> <button type="submit">Login</button> </form> { showSuccessMsg ? <div>Success</div> : null } { showErrorMsg ? <div>Error</div> : null } </div> ); } export default Login;
Currently, you should have an app which you can log in to and validate your token. Good job!
It looks ugly!
Okay, let’s add some styles:
/src/App.css
.App { text-align: center; } .App-logo { height: 40vmin; pointer-events: none; } @media (prefers-reduced-motion: no-preference) { .App-logo { animation: App-logo-spin infinite 20s linear; } } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #61dafb; } @keyframes App-logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .Validator { display: flex; flex-direction: column; justify-content: center; align-items: center; } .Login { display: flex; flex-direction: column; } .Login div { display: flex; flex-direction: column; } input { height: 2rem; text-align: center; width: 14rem; } input:focus { outline: 0.2rem solid #61dafb; } button { width: 150px; background-color: #61dafb; height: 45px; color: white; }
That’s better!
Short-lived tokens for better security
When securing JWT, it is essential to have a limited lifetime for the token. In our example 15 minutes. By doing that, you make sure that even if the token is stolen, it would not be valid for a long time.
How are we going to achieve the desired level of security, without asking the user to login again and again every 15 minutes? With a secure cookie, an httpOnly cookie. We will use the secured cookie to get a new JWT, before the current one expires.
The API should be updated in the following manner:
- /login should be updated to return the JWT along with its lifetime
- /login should also set an httpOnly cookie with the refresh token, which should be updated every time the JWT is updated
- /refresh-token endpoint should be created. It should return a new JWT, to replace the old one, if the refresh token cookie is still valid
Let’s see how this should look on the client side:
/src/core/services/inMemoryJwtService.js
import { ajaxRefreshToken } from './authenticationService'; const inMemoryJWTManager = () => { let inMemoryJWT = null; let refreshTimeoutId; const refreshToken = expiration => { const delay = new Date(expiration).getTime() - new Date().getTime(); // Fire five seconds before JWT expires const timeoutTrigger = delay - 5000; refreshTimeoutId = window.setTimeout(() => { ajaxRefreshToken() .then(res => { const { token, tokenExpiration } = res.data; setToken(token, tokenExpiration); }).catch(console.error); }, timeoutTrigger); }; const getToken = () => inMemoryJWT; const setToken = (token, tokenExpiration) => { inMemoryJWT = token; refreshToken(tokenExpiration); return true; }; const deleteToken = () => { inMemoryJWT = null; return true; }; return { getToken, setToken, deleteToken }; }; export default inMemoryJWTManager();
/src/core/components/login/Login.jsx
import React, { useState } from 'react'; import { ajaxLogin } from './../../services/authenticationService'; import inMemoryJwt from './../../services/inMemoryJwtService'; const Login = ({ setIsUserLogged }) => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showSuccessMsg, setShowSuccessMsg] = useState(false); const [showErrorMsg, setShowErrorMsg] = useState(false); const onChangeHandler = event => { const value = event.target.value; switch(event.target.name) { case 'username': setUsername(value); break; case 'password': setPassword(value); break; default: break; } }; const onSubmitHandler = event => { event.preventDefault(); const user = { username, password }; ajaxLogin(user) .then(res => { setShowSuccessMsg(true); const { token, tokenExpiration } = res.data; inMemoryJwt.setToken(token, tokenExpiration); setTimeout(() => { setIsUserLogged(true); }, 3000); }).catch(err => { console.error(err); setShowErrorMsg(true); setTimeout(() => { setShowErrorMsg(false); }, 3000); }); }; return ( <div className="Login"> <form onSubmit={onSubmitHandler}> <div> <label htmlFor="username">Username:</label> <input name="username" value={username} onChange={onChangeHandler} /> </div> <div> <label htmlFor="password">Password:</label> <input name="password" value={password} type="password" onChange={onChangeHandler} /> </div> <button type="submit">Login</button> </form> { showSuccessMsg ? <div>Success</div> : null } { showErrorMsg ? <div>Error</div> : null } </div> ); } export default Login;
Okay, 5 seconds before the token expires, a request is made to refresh it. We should not forget to refresh the httpOnly cookie as well.
Adding a proper logout
So far, so good! Still, we do not have a working logout. The API should expose an endpoint which allows us to invalidate the refresh token associated with the current user. Client side would like something like:
/src/core/components/validator/Validator.jsx
import React, { useState, useEffect } from 'react'; import Login from './../login/Login'; import inMemoryJwt from './../../services/inMemoryJwtService'; import { ajaxValidateToken, ajaxLogout } from './../../services/authenticationService'; const Validator = () => { const [showSuccessMsg, setShowSuccessMsg] = useState(false); const [showErrorMsg, setShowErrorMsg] = useState(false); const [isUserLogged, setIsUserLogged] = useState(false); useEffect(() => { if (inMemoryJwt.getToken()) { setIsUserLogged(true); } }, []); const logoutHandler = event => { event.preventDefault(); ajaxLogout() .then(() => { inMemoryJwt.deleteToken(); setIsUserLogged(false); }).catch(console.error); }; const checkTokenValidityHandler = event => { event.preventDefault(); ajaxValidateToken() .then(() => { setShowSuccessMsg(true); setTimeout(() => { setShowSuccessMsg(false); }, 3000); }).catch(err => { console.error(err); setShowErrorMsg(true); setTimeout(() => { setShowErrorMsg(false); }, 3000); }); }; return ( <div className="Validator"> <button type="button" onClick={checkTokenValidityHandler}>Check token validity</button> { isUserLogged ? <button type="button" onClick={logoutHandler}>Logout</button> : <Login setIsUserLogged={setIsUserLogged} /> } { showSuccessMsg ? <div>Success: Token is valid</div> : null } { showErrorMsg ? <div>Error: Token is invalid</div> : null } </div> ); } export default Validator;
We also need to stop the timeout that updates the JWT right before its expiration.
/src/core/services/inMemoryJwtService.js
import { ajaxRefreshToken } from './authenticationService'; const inMemoryJWTManager = () => { let inMemoryJWT = null; let refreshTimeoutId; const refreshToken = expiration => { const delay = new Date(expiration).getTime() - new Date().getTime(); // Fire five seconds before JWT expires const timeoutTrigger = delay - 5000; refreshTimeoutId = window.setTimeout(() => { ajaxRefreshToken() .then(res => { const { token, tokenExpiration } = res.data; setToken(token, tokenExpiration); }).catch(console.error); }, timeoutTrigger); }; const abortRefreshToken = () => { if (refreshTimeoutId) { window.clearTimeout(refreshTimeoutId); } }; const getToken = () => inMemoryJWT; const setToken = (token, tokenExpiration) => { inMemoryJWT = token; refreshToken(tokenExpiration); return true; }; const deleteToken = () => { inMemoryJWT = null; abortRefreshToken(); return true; }; return { getToken, setToken, deleteToken }; }; export default inMemoryJWTManager();
Our logout is done.
Current limitations
Now, the user experience is still not good enough. For instance, if the user reloads or closes the page they will be instantly logged out. If two or more tabs are open and the user logs out through one of them, the rest of the tabs will still have a valid token in their memory.
We shall address these.
Persisting the JWT across reloads
Currently, it is not possible to maintain sessions across reloads.
With the newly added refresh token logic, it would not be hard to persist our JWT across reloads. We just need to make a call to the /refresh-token endpoint when our app loads. To get the desired result, we need to update App.js:
/src/App.js
import { useState, useEffect } from 'react'; import logo from './logo.svg'; import './App.css'; import Validator from './core/components/validator/Validator'; import { ajaxRefreshToken } from './core/services/authenticationService'; import inMemoryJwtService from './core/services/inMemoryJwtService'; function App() { const [isAppReady, setIsAppReady] = useState(false); useEffect(() => { ajaxRefreshToken() .then(res => { const { token, tokenExpiration } = res.data; inMemoryJwtService.setToken(token, tokenExpiration); setIsAppReady(true); }).catch(() => { setIsAppReady(true); }); }, []); return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> { isAppReady ? <Validator /> : <div>Loading...</div> } </header> </div> ); } export default App;
Now we persist the JWT across page reloads. Good!
Persisting several tabs
When we store the JWT in memory, each instance of our app will manage the storage of the token independently from each other. The solution that we are going to use is with local storage.
Here’s the code that will handle the potential problem:
/src/core/services/inMemoryJwtService.js
import { ajaxRefreshToken } from './authenticationService'; const inMemoryJWTManager = () => { let inMemoryJWT = null; let refreshTimeoutId; const storageKey = 'logout'; window.addEventListener('storage', event => { if (event.key === storageKey) { inMemoryJWT = null; } }); const refreshToken = expiration => { const delay = new Date(expiration).getTime() - new Date().getTime(); // Fire five seconds before JWT expires const timeoutTrigger = delay - 5000; refreshTimeoutId = window.setTimeout(() => { ajaxRefreshToken() .then(res => { const { token, tokenExpiration } = res.data; setToken(token, tokenExpiration); }).catch(console.error); }, timeoutTrigger); }; const abortRefreshToken = () => { if (refreshTimeoutId) { window.clearTimeout(refreshTimeoutId); } }; const getToken = () => inMemoryJWT; const setToken = (token, tokenExpiration) => { inMemoryJWT = token; refreshToken(tokenExpiration); return true; }; const deleteToken = () => { inMemoryJWT = null; abortRefreshToken(); window.localStorage.setItem(storageKey, Date.now()); return true; }; return { getToken, setToken, deleteToken }; }; export default inMemoryJWTManager();
We simply add an event listener that gets triggered when there is an update in the local storage and then we trigger it inside deleteToken.
Conclusion
The example described in this article allows us to manage authentication using JSON Web Token in a secure way. The case is inspired by the Hasura team and their article.
The complexity of the illustrated code brings out the question: Do I really need JSON Web Token to manage the authentication of my app? Well, that you can answer for yourself…
You can find the complete code in this repository.
Written by Alex Bogdanov for Motion Software