If My Tag Expires Do I Have to Register It Again
The Ultimate Guide to handling JWTs on frontend clients (GraphQL)
JWTs (JSON Web Token, pronounced 'jot') are becoming a popular way of handling auth.
This post aims to demystify what a JWT is, talk over its pros/cons and comprehend all-time practices in implementing JWT on the client-side, keeping security in mind.
Although, we've worked on the examples with a GraphQL clients, simply the concepts apply to whatever frontend client.
Note: This guide was originally published on September 9th, 2019. Concluding updated on Jan fourth, 2022.
Table of Contents
- Table of Contents
- Introduction: What is a JWT?
- Security Considerations
- JWT Structure
- Nuts: Login
- Nuts: Client setup
- Basics: Logout
- Silent refresh
- Persisting sessions
- Force logout, aka Logout of all sessions/devices
- Server side rendering (SSR)
- How does the SSR server know if the user is logged in?
- Code from this blogpost (finished application)
- Try information technology out!
- References
- Summary
- Changelog
Introduction: What is a JWT?
For a detailed, technical clarification of JWTs refer to this article.
For the purposes of auth, a JWT is a token that is issued by the server. The token has a JSON payload that contains data specific to the user. This token can be used by clients when talking to APIs (past sending information technology forth as an HTTP header) so that the APIs can identify the user represented past the token, and take user specific activeness.
Security Considerations
But tin't a client but create a random JSON payload an impersonate a user?
Proficient question! That'due south why a JWT as well contains a signature. This signature is created by the server that issued the token (allow's say your login endpoint) and whatsoever other server that receives this token tin independently verify the signature to ensure that the JSON payload was not tampered with, and has information that was issued by a legitimate source.
Merely if I have a valid and signed JWT and someone steals it from the client, tin can't they use my JWT forever?
Yeah! If a JWT is stolen, then the thief can tin can keep using the JWT. An API that accepts JWTs does an contained verification without depending on the JWT source so the API server has no mode of knowing if this was a stolen token! This is why JWTs have an expiry value. And these values are kept short. Mutual practice is to keep it around 15 minutes, so that any leaked JWTs will cease to be valid adequately quickly. Only too, brand sure that JWTs don't get leaked.
These 2 facts result in most all the peculiarities about handling JWTs! The fact that JWTs shouldn't get stolen and that they need to accept short expiry times in case they do get stolen.
That's why it'south also actually of import not to store the JWT on the client persistently. Doing so you make your app vulnerable to CSRF & XSS attacks, by malicious forms or scripts to use or steal your token lying around in cookies or localStorage.
JWT Structure
So does a JWT have a specific kind of structure? What does it look similar?
A JWT looks something like this, when it's serialized:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o
If y'all decode that base64, you lot'll get JSON in 3 important parts: header, payload and signature.
The 3 parts of a JWT (based on epitome taken from jwt.io)
The serialized class is in the following format:
[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [ signature ]
A JWT is not encrypted. Information technology is based64 encoded and signed. And so anyone can decode the token and use its data. A JWT's signature is used to verify that it is in fact from a legitimate source.
Here is the diagram of how a JWT is issued (/login
) and and so used to make an API call to another service (/api
) in a nutshell:
A workflow of how a JWT is issued and then used
Ugh! This seems complicated. Why shouldn't I stick to good erstwhile session tokens?
This is a painful word on the Cyberspace. Our brusque (and opinionated answer) is that backend developers like using JWTs considering a) microservices b) not needing a centralized token database.
In a microservices setup, each microservice tin can independently verify that a token received from a client is valid. The microservice tin can further decode the token and excerpt relevant information without needing to have admission to a centralized token database.
This is why API developers like JWTs, and we (on the client-side) need to effigy out how to utilize it. Nonetheless, if yous can get away with a session token issued by your favourite monolithic framework, you're totally good to get and probably don't need JWTs!
Basics: Login
Now that we have a basic understanding what a JWT is, allow's create a simple login flow and extract the JWT. This is what we desire to reach:
And so how exercise we start?
The login process doesn't really change from what you lot'd usually exercise. For case, here's a login form that submits a username/password to an auth endpoint and grabs the JWT token from the response. This could be login with an external provider, an OAuth or OAuth2 step. It really doesn't matter, every bit long equally the client finally gets a JWT token in the response of the final login success step.
First, we'll build a simple login form to send the username and countersign to our login server. The server will upshot JWT token and we will store it in retentiveness. In this tutorial we won't focus on auth server backend, but you're welcome to bank check it out in instance repo for this blogpost.
This is what the handleSubmit
handler for a login button might look like:
async function handleSubmit () { //... // Make the login API call const response = await fetch(`/auth/login`, { method: 'Mail service', body: JSON.stringify({ username, password }) }) //... // Excerpt the JWT from the response const { jwt_token } = look response.json() //... // Exercise something the token in the login method look login({ jwt_token }) }
The login API returns a token so we laissez passer this token to a login
role from /utils/auth
where we can decide what to do with the token once we have information technology.
import { login } from '../utils/auth' await login({ jwt_token })
And then nosotros've got the token, now where do we shop this token?
We need to save our JWT token somewhere, then that we can forwards it to our API as a header. Yous might be tempted to persist it in localstorage; don't do it! This is decumbent to XSS attacks.
What about saving it in a cookie?
Creating cookies on the client to save the JWT will too exist prone to XSS. If information technology tin be read on the client from Javascript exterior of your app - information technology tin be stolen. You might think an HttpOnly cookie (created by the server instead of the client) will help, but cookies are vulnerable to CSRF attacks. It is important to notation that HttpOnly and sensible CORS policies cannot prevent CSRF class-submit attacks and using cookies crave a proper CSRF mitigation strategy.
Note that a SameSite cookie will make Cookie based approaches prophylactic from CSRF attacks. Information technology might not be a solution if your Auth and API servers are hosted on different domains, but information technology should piece of work really well otherwise!
Where do we relieve information technology and so?
The OWASP JWT Cheatsheet and OWASP ASVS (Application Security Verification Standard) prescribe guidelines for handling and storing tokens.
The sections that are relevant to this are the Token Storage on Client Side
and Token Sidejacking
problems in the JWT Cheatsheet, and chapters 3 (Session Management
) and 8 (Information Protection
) of ASVS.
From the Cheatsheet, Issue: Token Storage on the Client Side
:
"This occurs when an application stores the token in a fashion exhibiting the post-obit behavior:"
- Automatically sent by the browser (Cookie storage).
- Retrieved even if the browser is restarted (Utilize of browser localStorage container).
- Retrieved in case of XSS issue (Cookie accessible to JavaScript lawmaking or Token stored in browser local/session storage).
"How to Preclude:"
- Store the token using the browser
sessionStorage
container. - Add it every bit a Bearer HTTP
Authentication
header with JavaScript when calling services. - Add
fingerprint
information to the token.
By storing the token in browser sessionStorage container information technology exposes the token to existence stolen through a XSS assault. However, fingerprints added to the token prevent reuse of the stolen token by the attacker on their machine. To close a maximum of exploitation surfaces for an attacker, add a browser Content Security Policy
to harden the execution context.
Where a fingerprint
is the implementation of the following guidelines from the Token Sidejacking
issue:
"Symptom:"
This set on occurs when a token has been intercepted/stolen by an attacker and they use it to proceeds access to the organization using targeted user identity.
How to Prevent:
A way to prevent it is to add a "user context"
in the token. A user context volition be equanimous of the post-obit data:
- A random string that will be generated during the authentication stage. Information technology volition be sent to the customer as an hardened cookie (flags:
HttpOnly
+Secure
+SameSite
+ cookie prefixes). - A SHA256 hash of the random cord volition be stored in the token (instead of the raw value) in order to prevent any XSS issues allowing the attacker to read the random string value and setting the expected cookie.
IP addresses should not be used because at that place are some legitimate situations in which the IP address can change during the aforementioned session. For example, when an user accesses an application through their mobile device and the mobile operator changes during the exchange, then the IP address may (often) change. Moreover, using the IP address can potentially cause issues with European GDPR compliance.
During token validation, if the received token does not incorporate the correct context (for case, if information technology has been replayed), so information technology must exist rejected.
An implementation of this on the customer side may look similar:
// Short elapsing JWT token (5-x min) consign function getJwtToken() { render sessionStorage.getItem("jwt") } export function setJwtToken(token) { sessionStorage.setItem("jwt", token) } // Longer duration refresh token (xxx-sixty min) consign function getRefreshToken() { render sessionStorage.getItem("refreshToken") } export function setRefreshToken(token) { sessionStorage.setItem("refreshToken", token) } function handleLogin({ electronic mail, password }) { // Call login method in API // The server handler is responsible for setting user fingerprint cookie during this as well const { jwtToken, refreshToken } = await login({ email, password }) setJwtToken(jwtToken) setRefreshToken(refreshToken) // If y'all like, you may redirect the user now Router.push("/some-url") }
Yes, the token will be nullified when the user switches between tabs, but we will bargain with that afterwards.
Ok! Now that we have the token what can nosotros practice with it?
- Using in our API client to laissez passer it every bit a header to every API phone call
- Bank check if a user is logged in by seeing if the JWT variable is set.
- Optionally, we can even decode the JWT on the customer to access data in the payload. Allow'south say we need the user-id or the username on the client, which nosotros can extract from the JWT.
How do we cheque if our user is logged in?
Nosotros check in our if the token variable is set and if information technology isn't - redirect to login page.
const jwtToken = getJwtToken(); if (!jwtToken) { Router.push button('/login') }
Basics: Customer setup
At present it'southward time to fix our GraphQL client. The thought is to become the token from the variable we set, and if it's there, nosotros pass it to our GraphQL client.
Using the JWT in a GraphQL client
Bold your GraphQL API accepts a JWT auth token as an Authorisation
header, all you need to do is setup your client to ready an HTTP header by using the JWT token from the variable.
Here's what a setup with the Apollo GraphQL client using an ApolloLink
middleware.
import { useMemo } from "react" import { ApolloClient, InMemoryCache, HttpLink, ApolloLink, Operation } from "@apollo/customer" import { getMainDefinition } from "@apollo/client/utilities" import { WebSocketLink } from "@apollo/client/link/ws" import merge from "deepmerge" allow apolloClient function getHeaders() { const headers = {} as HeadersInit const token = getJwtToken() if (token) headers["Potency"] = `Bearer ${token}` return headers } function operationIsSubscription(functioning: Operation): boolean { const definition = getMainDefinition(operation.query) const isSubscription = definition.kind === "OperationDefinition" && definition.operation === "subscription" return isSubscription } let wsLink function getOrCreateWebsocketLink() { wsLink ??= new WebSocketLink({ uri: process.env["NEXT_PUBLIC_HASURA_ENDPOINT"].supervene upon("http", "ws").supplant("https", "wss"), options: { reconnect: true, timeout: 30000, connectionParams: () => { return { headers: getHeaders() } }, }, }) render wsLink } function createLink() { const httpLink = new HttpLink({ uri: process.env["NEXT_PUBLIC_HASURA_ENDPOINT"], credentials: "include", }) const authLink = new ApolloLink((operation, forward) => { performance.setContext(({ headers = {} }) => ({ headers: { ...headers, ...getHeaders(), }, })) return forward(performance) }) if (typeof window !== "undefined") { return ApolloLink.from([ authLink, // Use "getOrCreateWebsocketLink" to init WS lazily // otherwise WS connectedness volition be created + used fifty-fifty if using "query" ApolloLink.split up(operationIsSubscription, getOrCreateWebsocketLink, httpLink), ]) } else { return ApolloLink.from([authLink, httpLink]) } } function createApolloClient() { return new ApolloClient({ ssrMode: typeof window === "undefined", link: createLink(), cache: new InMemoryCache(), }) } export function initializeApollo(initialState = null) { const _apolloClient = apolloClient ?? createApolloClient() // If your folio has Adjacent.js data fetching methods that apply Apollo Client, the initial state // become hydrated here if (initialState) { // Become existing cache, loaded during client side data fetching const existingCache = _apolloClient.extract() // Merge the existing cache into data passed from getStaticProps/getServerSideProps const data = merge(initialState, existingCache) // Restore the cache with the merged data _apolloClient.enshroud.restore(data) } // For SSG and SSR e'er create a new Apollo Client if (typeof window === "undefined") render _apolloClient // Create the Apollo Customer in one case in the client if (!apolloClient) apolloClient = _apolloClient return _apolloClient } consign function useApollo(initialState) { const store = useMemo(() => initializeApollo(initialState), [initialState]) render store }
As you can come across from the code, whenever in that location is a token, information technology's passed equally a header to every request.
But what will happen if at that place is no token?
It depends on the menstruation in your application. Let's say you redirect the user back to the login folio:
else { Router.push('/login') }
What happens if a token expires as we're using information technology?
Let's say our token is only valid for 15 minutes. In this case we'll probably become an fault from our API denying our request (let'due south say a 401: Unauthorized
fault). Remember that every service that knows how to use a JWT tin independently verify it and check whether it has expired or non.
Let'south add error handling to our app to handle this example. We'll write code that will run for every API response and check the error. When we receive the token expired/invalid error from our API, we trigger the logout or the redirect to login workflow.
Here's what the code looks like if we're using the Apollo client:
import { onError } from 'apollo-link-fault'; const logoutLink = onError(({ networkError }) => { if (networkError.statusCode === 401) logout(); }) if (typeof window !== "undefined") { return ApolloLink.from([ logoutLink, authLink, ApolloLink.dissever(operationIsSubscription, getOrCreateWebsocketLink, httpLink), ]) } else { return ApolloLink.from([ logoutLink, authLink, httpLink ]) }
You may notice that this will result in a fairly sucky user experience. The user will keep getting asked to re-authenticate every time the token expires. This is why apps implement a silent refresh workflow that keeps refreshing the JWT token in the background. More on this in the next sections beneath!
Basics: Logout
With JWTs, a "logout" is simply deleting the token on the client side so that information technology can't exist used for subsequent API calls.
Then...is at that place no /logout
API call at all?
A logout
endpoint is not really required, because whatever microservice that accepts your JWTs volition keep accepting it. If your auth server deletes the JWT, information technology won't matter because the other services volition keep accepting it anyway (since the whole point of JWTs was to not require centralised coordination).
The token is withal valid and can exist used. What if I need to ensure that the token cannot exist used e'er again?
This is why keeping JWT death values to a small value is important. And this is why ensuring that your JWTs don't get stolen is even more of import. The token is valid (even later you delete it on the client), simply only for brusque period to reduce the probability of it being used maliciously.
In addition, you lot tin can add together a deny-listing workflow to your JWTs. In this case, you tin can take a /logout
API call and your auth server puts the tokens in a "invalid listing". However, all the API services that eat the JWT at present need to add an boosted stride to their JWT verification to check with the centralized "deny-list". This introduces fundamental state over again, and brings the states back to what we had before using JWTs at all.
Doesn't deny-list negate the benefit of JWT not needing any central storage?
In a way it does. It'south an optional precaution that y'all can take if y'all are worried that your token tin can get stolen and misused, but information technology also increases the amount of verification that has to be done. Every bit you can imagine, this had led to much gnashing of teeth on the internet.
What volition happen if I am logged in on different tabs?
Ane way of solving this is past introducing a global event listener on localstorage. Whenever nosotros update this logout key in localstorage on one tab, the listener will burn on the other tabs and trigger a "logout" likewise and redirect users to the login screen.
window.addEventListener('storage', this.syncLogout) //.... syncLogout (event) { if (event.key === 'logout') { panel.log('logged out from storage!') Router.push('/login') } }
These are the 2 things we now need to do on logout:
- Nullify the token
- Prepare
logout
item in local storage
import { useEffect } from "react" import { useRouter } from "next/router" import { gql, useMutation, useApolloClient } from "@apollo/client" import { setJwtToken, setRefreshToken } from "../lib/auth" const SignOutMutation = gql` mutation SignOutMutation { signout { ok } } ` function SignOut() { const client = useApolloClient() const router = useRouter() const [signOut] = useMutation(SignOutMutation) useEffect(() => { // Clear the JWT and refresh token so that Apollo doesn't try to apply them setJwtToken("") setRefreshToken("") // Tell Apollo to reset the shop // Finally, redirect the user to the home page signOut().then(() => { // to support logging out from all windows window.localStorage.setItem('logout', Date.now()) client.resetStore().then(() => { router.push("/signin") }) }) }, [signOut, router, client]) return <p>Signing out...</p> }
In that case whenever you log out from one tab, event listener will fire in all other tabs and redirect them to login screen.
This works across tabs. But how practice I "force logout" of all sessions on different devices?!
Nosotros cover this topic in a little more particular in a section later on: Force logout.
Silent refresh
At that place are ii major bug that users of our JWT based app will still face:
- Given our brusque expiry times on the JWTs, the user will be logged out every xv minutes. This would be a fairly terrible experience. Ideally, we'd probably desire our user to be logged in for a long time.
- If a user closes their app and opens it again, they'll need to login once more. Their session is not persisted because we're non saving the JWT token on the client anywhere.
To solve this problem, almost JWT providers, provide a refresh token. A refresh token has 2 backdrop:
- It can be used to make an API call (say,
/refresh_token
) to fetch a new JWT token before the previous JWT expires. - It can be safely persisted beyond sessions on the client!
How does a refresh token work?
This token is issued as role of authentication process along with the JWT. The auth server should saves this refresh token and associates it to a item user in its own database, then that information technology can handle the renewing JWT logic.
On the client, before the previous JWT token expires, we wire up our app to brand a /refresh_token
endpoint and take hold of a new JWT.
How is a refresh token safely persisted on the client?
We follow the guidelines in the OWASP JWT Guide to forestall issues with client-side storage of a token.
Improper client-side storage occurs when "an application stores the token in a way exhibiting the following behavior":
- Automatically sent by the browser (Cookie storage).
- Retrieved even if the browser is restarted (Use of browser localStorage container).
- Retrieved in example of XSS issue (Cookie accessible to JavaScript lawmaking or Token stored in browser local/session storage).
To forbid this, the following steps are taken:
- Store the token using the browser
sessionStorage
container. - Add it as a Bearer HTTP
Authentication
header with JavaScript when calling services. - Add
fingerprint
information to the token.
Past storing the token in browser sessionStorage
container it exposes the token to being stolen through a XSS attack. Yet, fingerprints
added to the token preclude reuse of the stolen token by the aggressor on their machine. To close a maximum of exploitation surfaces for an attacker, add a browser Content Security Policy
to harden the execution context.
Where the implementation of a fingerprint
too serves to foreclose Token Sidejacking from occuring, and is done according to the guidelines here
So what does the new "login" procedure look like?
Nothing much changes, except that a refresh token gets sent along with the JWT. Permit'south have a look a diagram of login process again, simply now with refresh_token
functionality:
- The user logs in with a login API call.
- Server generates JWT token and
refresh_token
, and afingerprint
- The server returns the JWT token, refresh token, and a
SHA256
-hashed version of the fingerprint in the token claims - The un-hashed version of the generated fingerprint is stored as a hardened,
HttpOnly
cookie on the client - When the JWT token expires, a silent refresh will happen. This is where the client calls the
/refresh
token endpoint
And now, what does the silent refresh expect like?
Hither's what happens:
- The refresh endpoint must check for the beingness of the fingerprint cookie, and validate that the comparison of the hashed value in the token claims is identical to the unhashed value in the cookie
- If either of these conditions are not met, the refresh request is rejected
- Otherwise the refresh token is accepted, and a fresh JWT access token is granted, resetting the silent refresh procedure
An implementation of this workflow using the apollo-link-token-refresh
bundle, is something like the below.
Using this as a non-terminating link volition automatically check the validity of our JWT, and attempt a silent refresh if needed when any operation is run.
import { TokenRefreshLink } from "apollo-link-token-refresh" import { JwtPayload } from "jwt-decode" import { getJwtToken, getRefreshToken, setJwtToken } from "./auth" import decodeJWT from "jwt-decode" export function makeTokenRefreshLink() { render new TokenRefreshLink({ // Indicates the electric current country of access token expiration // If token not yet expired or user doesn't accept a token (invitee) true should exist returned isTokenValidOrUndefined: () => { const token = getJwtToken() // If there is no token, the user is non logged in // We return true here, because in that location is no need to refresh the token if (!token) render truthful // Otherwise, we check if the token is expired const claims: JwtPayload = decodeJWT(token) const expirationTimeInSeconds = claims.exp * g const now = new Appointment() const isValid = expirationTimeInSeconds >= now.getTime() // Render true if the token is even so valid, otherwise false and trigger a token refresh render isValid }, // Responsible for fetching refresh token fetchAccessToken: async () => { const jwt = decodeJWT(getJwtToken()) const refreshToken = getRefreshToken() const fingerprintHash = jwt?.["https://hasura.io/jwt/claims"]?.["X-User-Fingerprint"] const request = expect fetch(process.env["NEXT_PUBLIC_HASURA_ENDPOINT"], { method: "POST", headers: { "Content-Type": "awarding/json", }, trunk: JSON.stringify({ query: ` query RefreshJwtToken($refreshToken: String!, $fingerprintHash: Cord!) { refreshJwtToken(refreshToken: $refreshToken, fingerprintHash: $fingerprintHash) { jwt } } `, variables: { refreshToken, fingerprintHash, }, }), }) return asking.json() }, // Callback which receives a fresh token from Response. // From hither we can salvage token to the storage handleFetch: (accessToken) => { setJwtToken(accessToken) }, handleResponse: (performance, accessTokenField) => (response) => { // here yous tin can parse response, handle errors, ready returned token to // further operations // returned object should be like this: // { // access_token: 'token string hither' // } render { access_token: response.refreshToken.jwt } }, handleError: (err) => { console.warn("Your refresh token is invalid. Try to reauthenticate.") panel.error(err) // Remove invalid tokens localStorage.removeItem("jwt") localStorage.removeItem("refreshToken") }, }) }
Referring back to the section addressing: "What will happen if I'thousand logged in on multiple tabs?"
, using sessionStorage for this means we won't be authenticated in new tabs (if they weren't created using "Duplicate tab"
) or windows.
A potential solution to this, while withal remaining secure, is to use localStorage
as an event-emitter again and sync sessionStorage
betwixt tabs of the same base URL on load.
This can exist accomplished past using a script such equally this on your pages:
if (!sessionStorage.length) { // Ask other tabs for session storage localStorage.setItem("getSessionStorage", String(Appointment.now())) } window.addEventListener("storage", (event) => { if (effect.key == "getSessionStorage") { // Some tab asked for the sessionStorage -> send it localStorage.setItem("sessionStorage", JSON.stringify(sessionStorage)) localStorage.removeItem("sessionStorage") } else if (result.key == "sessionStorage" && !sessionStorage.length) { // sessionStorage is empty -> fill it const data = JSON.parse(event.newValue) for (let primal in data) { sessionStorage.setItem(key, data[cardinal]) } } })
Persisting sessions
Persisting sessions is against the OWASP security guidelines for clients and token hallmark:
"... Retrieved fifty-fifty if the browser is restarted (Employ of browser localStorage
container)."
There is (at the fourth dimension of writing) no way deemed adequate that allows for a persistent user session after a browser has been fully airtight and re-opened, unless the browser implementation retains tab session country (sessionStorage
).
You lot may choose to store your token in localStorage
or a Cookie instead, in order to have persistent sessions across browser restarts, but doing and so is at your discretion.
Note: For an ongoing discussion of this topic, see https://github.com/OWASP/ASVS/issues/1141
Strength logout, aka Logout of all sessions/devices
Now that are users are logged in forever and stay logged in across sessions, at that place's a new problem that we need to worry almost: Force logout or, logging out of all sessions and devices.
The refresh token implementations from the sections in a higher place, prove us that we tin can persist sessions and stay logged in.
In this instance, a simple implementation of "force logout" is asking the auth server to invalidate all refresh tokens associated for a particular user.
This is primarily an implementation on the auth server backend, and doesn't need whatsoever special handling on the client. Autonomously from a "Force Logout" button on your app perhaps :)
Server side rendering (SSR)
In server side rendering there are additional complexities involved when dealing with JWT tokens.
This is what we desire:
- The browser makes a request to a app URL
- The SSR server renders the page based on the user's identity
- The user gets the rendered page and then continues using the app as an SPA (single page app)
How does the SSR server know if the user is logged in?
The browser needs to send some information about the current user's identity to the SSR server. The only style to do this is via a cookie.
Since nosotros've already implemented refresh token workflows via cookies, when we make a asking to the SSR server, we need to make sure that the refresh-token is also sent forth.
Notation: For SSR on authenticated pages, it is vital that that the domain of the auth API (and hence the domain of the
refresh_token
cookie) is the same as the domain of the SSR server. Otherwise, our cookies won't be sent to the SSR server!
This is what the SSR server does:
- Upon receiving a asking to render a particular page, the SSR server captures the refresh_token cookie.
- The SSR server uses the refresh_token cookie to get a new JWT for the user
- The SSR server uses the new JWT token and makes all the authenticated GraphQL requests to fetch the right data
Can the user continue making authenticated API requests one time the SSR folio has loaded?
Nope, not without some additional piddling around unfortunately!
Once the SSR server returns the rendered HTML, the only identification left on the browser about the user's identity is the quondam refresh token cookie that has already been used by the SSR server!
If our app lawmaking tries to use this refresh token cookie to fetch a new JWT, this asking will fail and the user will get logged out.
To solve this, the SSR server after rendering the page needs to send the latest refresh token cookie, so that the browser tin can use it!
The unabridged SSR flow, end to terminate:
Code from this blogpost (finished awarding)
Sample code for this blogpost with an end to end working app, with SSR capabilities is bachelor hither.
https://github.com/hasura/jwt-guide
The repository also contains the sample auth backend code.
Try it out!
Set up a free GraphQL backend with Hasura Cloud to try it out for yourself!
Make certain you're on version 1.3 or to a higher place and yous're skillful to go.
References
- JWT.io
- OWASP notes on XSS, CSRF and similar things
- OWASP JWT Cheatsheet
- OWASP Application Security Verification Standard, v5
- The Parts of JWT Security Nobody Talks Most | Philippe De Ryck
Lots of other awesome stuff on the web
Summary
Once yous've worked through all the sections higher up, your app should at present have all the capabilities of a modern app, using a JWT and should be secure from the common major security gotchas that JWT implementations accept!
Let us know on twitter or in the comments below if you have whatever questions, suggestions or feedback!
Changelog
- (12/28/2021) Recommendation to store token in Cookie changed to
sessionStorage
, per OWASP JWT guidelines to addressEvent: Token Storage on Customer Side
0 - (12/28/2021) Adopted OWASP Application Security Verification Standard v5 6 L1-L2 guidelines
- Of note: Chapters 3 (
Session Management
) and viii (Data Protection
)
- Of note: Chapters 3 (
- (12/28/2021) Modify section on
Persisting Sessions
to contain OWASP guidelines on this - (12/28/2021) Sample application repo code updated 1
- Update from Next.js 9 -> 12, update
@apollo
libraries tov3.x
- Password hashing algorithm changed from
bcrypt
to native Node.jscrypto.scrypt
per OWASP guidelines two and to reduce number of external dependencies - Authentication on frontend and backend modified to make use of a user fingerprint in addition to a token, per OWASP guidelines on preventing
Token Sidejacking
iii - Example usage of
TokenRefreshLink
4 to manage silent refresh workflow added - Server endpoints integrated through Hasura Actions 5 rather than directly invoking from client
- Prefer recommended use of
crypto.timingSafeEqual()
to prevent timing attacks
- Update from Next.js 9 -> 12, update
Source: https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/