Authentication Session Has Expired Please Login Again

banner image

The Ultimate Guide to handling JWTs on frontend clients (GraphQL)

JWTs (JSON Web Token, pronounced 'jot') are becoming a pop way of handling auth.
This post aims to demystify what a JWT is, discuss its pros/cons and cover best practices in implementing JWT on the client-side, keeping security in heed.
Although, we've worked on the examples with a GraphQL clients, just the concepts utilise to any frontend client.

Note: This guide was originally published on September ninth, 2019. Last updated on January quaternary, 2022.

Table of Contents

  • Table of Contents
  • Introduction: What is a JWT?
    • Security Considerations
    • JWT Structure
  • Basics: Login
  • Basics: 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)
  • Attempt it out!
  • References
  • Summary
  • Changelog

Introduction: What is a JWT?

For a detailed, technical description 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 information specific to the user. This token can be used past clients when talking to APIs (past sending information technology along as an HTTP header) so that the APIs tin can identify the user represented by the token, and take user specific action.

Security Considerations

But tin can't a client just create a random JSON payload an impersonate a user?

Good question! That's why a JWT besides contains a signature. This signature is created past the server that issued the token (allow's say your login endpoint) and any other server that receives this token tin can independently verify the signature to ensure that the JSON payload was not tampered with, and has information that was issued by a legitimate source.

But if I have a valid and signed JWT and someone steals information technology from the customer, can't they use my JWT forever?

Aye! If a JWT is stolen, then the thief tin can 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 way of knowing if this was a stolen token! This is why JWTs have an expiry value. And these values are kept brusque. Common practice is to continue it around 15 minutes, so that whatsoever leaked JWTs will finish to be valid fairly quickly. Just also, make sure that JWTs don't get leaked.

These 2 facts result in nearly all the peculiarities most treatment JWTs! The fact that JWTs shouldn't get stolen and that they demand to have brusk decease times in instance they do get stolen.

That'southward why it'due south besides really of import not to shop the JWT on the client persistently. Doing and so you brand 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 Construction

So does a JWT have a specific kind of construction? What does it look similar?

A JWT looks something similar this, when information technology's serialized:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

If you decode that base64, you'll get JSON in 3 of import parts: header, payload and signature.

JWT Claims Example from JWT.io

The iii parts of a JWT (based on epitome taken from jwt.io)
The serialized form is in the following format:

[ base64UrlEncode(header) ] . [ base64UrlEncode(payload) ] . [ signature ]

A JWT is not encrypted. It is based64 encoded and signed. So anyone can decode the token and utilise its data. A JWT'due south 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 telephone call to another service (/api) in a nutshell:

JWT End-to-End Workflow
A workflow of how a JWT is issued and then used

Ugh! This seems complicated. Why shouldn't I stick to good onetime session tokens?

This is a painful word on the Internet. Our brusque (and opinionated answer) is that backend developers similar using JWTs because a) microservices b) non needing a centralized token database.

In a microservices setup, each microservice can independently verify that a token received from a client is valid. The microservice tin further decode the token and excerpt relevant information without needing to take access to a centralized token database.

This is why API developers like JWTs, and we (on the customer-side) need to figure out how to use information technology. Yet, if you can go away with a session token issued by your favourite monolithic framework, y'all're totally skillful to go and probably don't demand JWTs!


Basics: Login

Now that nosotros have a bones understanding what a JWT is, let's create a simple login flow and extract the JWT. This is what we want to achieve:

A login flow for getting a JWT

So how do nosotros start?

The login process doesn't really change from what you'd usually exercise. For example, here'south a login course 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 actually doesn't thing, as long as the customer finally gets a JWT token in the response of the concluding login success step.

First, nosotros'll build a unproblematic login form to ship the username and password to our login server. The server will result JWT token and we volition store it in retentivity. In this tutorial we won't focus on auth server backend, but you lot're welcome to cheque information technology out in example repo for this blogpost.

This is what the handleSubmit handler for a login button might look like:

          async role handleSubmit () {   //...   // Make the login API telephone call   const response = look fetch(`/auth/login`, {     method: 'POST',     trunk: JSON.stringify({ username, password })   })   //...   // Extract the JWT from the response   const { jwt_token } = look response.json()   //...   // Exercise something the token in the login method   wait login({ jwt_token }) }                  

The login API returns a token so nosotros laissez passer this token to a login function from /utils/auth where we can determine what to do with the token once we have it.

          import { login } from '../utils/auth' await login({ jwt_token })                  

So we've got the token, now where practice we store this token?

We need to relieve our JWT token somewhere, then that we can forward information technology to our API as a header. Y'all might exist tempted to persist it in localstorage; don't do it! This is prone to XSS attacks.

What about saving it in a cookie?

Creating cookies on the client to salvage the JWT will too be prone to XSS. If information technology tin can be read on the client from Javascript exterior of your app - it can exist stolen. You might retrieve an HttpOnly cookie (created past the server instead of the client) will assistance, just cookies are vulnerable to CSRF attacks. It is important to note that HttpOnly and sensible CORS policies cannot prevent CSRF form-submit attacks and using cookies crave a proper CSRF mitigation strategy.

Note that a SameSite cookie will brand Cookie based approaches safe from CSRF attacks. Information technology might not be a solution if your Auth and API servers are hosted on different domains, but it should piece of work really well otherwise!

Where practice we save it 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 Customer Side and Token Sidejacking issues in the JWT Cheatsheet, and chapters three (Session Management) and 8 (Data Protection) of ASVS.

From the Cheatsheet, Upshot: Token Storage on the Client Side:

"This occurs when an application stores the token in a manner exhibiting the following beliefs:"

  • Automatically sent past the browser (Cookie storage).
  • Retrieved fifty-fifty if the browser is restarted (Use of browser localStorage container).
  • Retrieved in example of XSS issue (Cookie attainable to JavaScript lawmaking or Token stored in browser local/session storage).

"How to Prevent:"

  • Store the token using the browser sessionStorage container.
  • Add it as a Bearer HTTP Authentication header with JavaScript when calling services.
  • Add together fingerprint information to the token.

By storing the token in browser sessionStorage container it exposes the token to beingness stolen through a XSS attack. Withal, fingerprints added to the token prevent reuse of the stolen token by the attacker on their car. To close a maximum of exploitation surfaces for an aggressor, add a browser Content Security Policy to harden the execution context.

Where a fingerprint is the implementation of the post-obit guidelines from the Token Sidejacking issue:

"Symptom:"

This attack occurs when a token has been intercepted/stolen by an assailant and they apply information technology to gain access to the arrangement using targeted user identity.

How to Forestall:

A way to prevent it is to add together a "user context" in the token. A user context will exist composed of the post-obit data:

  • A random string that will exist generated during the authentication phase. It will be sent to the customer as an hardened cookie (flags: HttpOnly + Secure + SameSite + cookie prefixes).
  • A SHA256 hash of the random string volition be stored in the token (instead of the raw value) in order to forestall any XSS issues allowing the assailant to read the random string value and setting the expected cookie.

IP addresses should non be used because there are some legitimate situations in which the IP address tin modify during the same session. For example, when an user accesses an application through their mobile device and the mobile operator changes during the exchange, and so the IP address may (ofttimes) change. Moreover, using the IP address can potentially crusade problems with European GDPR compliance.

During token validation, if the received token does not contain the right context (for example, if it has been replayed), then information technology must be rejected.


An implementation of this on the client side may look like:

          // Short duration JWT token (5-10 min) export function getJwtToken() {     render sessionStorage.getItem("jwt") }  export function setJwtToken(token) {     sessionStorage.setItem("jwt", token) }  // Longer duration refresh token (30-60 min) export function getRefreshToken() {     render sessionStorage.getItem("refreshToken") }  export function setRefreshToken(token) {     sessionStorage.setItem("refreshToken", token) }  function handleLogin({ email, 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({ electronic mail, password })   setJwtToken(jwtToken)   setRefreshToken(refreshToken)    // If you like, you may redirect the user now   Router.push button("/some-url") }                  

Yes, the token volition be nullified when the user switches between tabs, but we volition deal with that later.

Ok! Now that we accept the token what tin can we do with it?

  • Using in our API client to pass it as a header to every API call
  • Bank check if a user is logged in past seeing if the JWT variable is set up.
  • Optionally, we tin even decode the JWT on the client to admission data in the payload. Allow'south say nosotros need the user-id or the username on the customer, which we can extract from the JWT.

How practice nosotros check 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('/login') }                  

Basics: Client setup

Now it's fourth dimension to fix up our GraphQL customer. The thought is to become the token from the variable we prepare, and if it's there, nosotros pass information technology to our GraphQL client.


Using the JWT in a GraphQL customer

Assuming your GraphQL API accepts a JWT auth token as an Authorization header, all you lot demand to do is setup your customer to fix an HTTP header past 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, Functioning } from "@apollo/client" import { getMainDefinition } from "@apollo/client/utilities" import { WebSocketLink } from "@apollo/client/link/ws" import merge from "deepmerge"  let apolloClient  function getHeaders() {     const headers = {} as HeadersInit     const token = getJwtToken()     if (token) headers["Authority"] = `Bearer ${token}`     return headers }  role operationIsSubscription(operation: Operation): boolean {     const definition = getMainDefinition(functioning.query)     const isSubscription = definition.kind === "OperationDefinition" && definition.operation === "subscription"     return isSubscription }  let wsLink office getOrCreateWebsocketLink() {     wsLink ??= new WebSocketLink({         uri: process.env["NEXT_PUBLIC_HASURA_ENDPOINT"].replace("http", "ws").replace("https", "wss"),         options: {             reconnect: true,             timeout: 30000,             connectionParams: () => {                 return { headers: getHeaders() }             },         },     })     return wsLink }  function createLink() {     const httpLink = new HttpLink({         uri: process.env["NEXT_PUBLIC_HASURA_ENDPOINT"],         credentials: "include",     })      const authLink = new ApolloLink((operation, forward) => {         operation.setContext(({ headers = {} }) => ({             headers: {                 ...headers,                 ...getHeaders(),             },         }))         return frontwards(operation)     })      if (typeof window !== "undefined") {         return ApolloLink.from([             authLink,             // Use "getOrCreateWebsocketLink" to init WS lazily             // otherwise WS connectedness will exist created + used even if using "query"             ApolloLink.dissever(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 = zippo) {     const _apolloClient = apolloClient ?? createApolloClient()      // If your page has Next.js information fetching methods that utilize Apollo Client, the initial country     // get hydrated here     if (initialState) {         // Get existing cache, loaded during client side data fetching         const existingCache = _apolloClient.extract()          // Merge the existing cache into information passed from getStaticProps/getServerSideProps         const data = merge(initialState, existingCache)          // Restore the cache with the merged data         _apolloClient.cache.restore(data)     }      // For SSG and SSR e'er create a new Apollo Client     if (typeof window === "undefined") render _apolloClient     // Create the Apollo Client once in the client     if (!apolloClient) apolloClient = _apolloClient      return _apolloClient }  export function useApollo(initialState) {     const store = useMemo(() => initializeApollo(initialState), [initialState])     render store }                  

As yous can see from the code, whenever there is a token, it's passed as a header to every request.

But what volition happen if there is no token?

Information technology depends on the menstruation in your application. Let'due south say you redirect the user back to the login page:

          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 get an fault from our API denying our asking (permit's say a 401: Unauthorized fault). Remember that every service that knows how to utilize a JWT can independently verify it and bank check whether it has expired or not.

Let's add together error handling to our app to handle this case. We'll write code that volition run for every API response and check the error. When nosotros 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-error';  const logoutLink = onError(({ networkError }) => {     if (networkError.statusCode === 401) logout(); })  if (typeof window !== "undefined") {     return ApolloLink.from([         logoutLink,         authLink,         ApolloLink.split(operationIsSubscription, getOrCreateWebsocketLink, httpLink),     ]) } else {     return ApolloLink.from([       logoutLink,       authLink,       httpLink     ]) }                  

You may observe that this will upshot in a fairly sucky user experience. The user will keep getting asked to re-authenticate every fourth dimension 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 below!

Basics: Logout

With JWTs, a "logout" is simply deleting the token on the client side then that information technology can't be used for subsequent API calls.

check-token-flow

And so...is there no /logout API phone call at all?

A logout endpoint is not really required, because any microservice that accepts your JWTs will go on accepting it. If your auth server deletes the JWT, it won't matter because the other services will keep accepting information technology anyway (since the whole point of JWTs was to not require centralised coordination).

The token is still valid and can be used. What if I need to ensure that the token cannot be used ever again?

This is why keeping JWT expiry values to a minor value is important. And this is why ensuring that your JWTs don't get stolen is even more important. The token is valid (fifty-fifty after you delete it on the client), but only for short menstruum to reduce the probability of it being used maliciously.

In addition, you tin can add a deny-list workflow to your JWTs. In this example, yous can accept a /logout API call and your auth server puts the tokens in a "invalid list". Notwithstanding, all the API services that consume the JWT now need to add together an additional step to their JWT verification to check with the centralized "deny-list". This introduces central state again, and brings u.s. back to what nosotros had before using JWTs at all.

jwt-blogpost-3-check-token-flow-serverside

Doesn't deny-listing negate the benefit of JWT not needing any central storage?

In a fashion it does. It'south an optional precaution that y'all tin take if you lot are worried that your token can become stolen and misused, simply information technology too increases the corporeality of verification that has to be done. Equally you tin can imagine, this had led to much gnashing of teeth on the internet.

What will happen if I am logged in on different tabs?

One way of solving this is by introducing a global consequence listener on localstorage. Whenever we update this logout key in localstorage on one tab, the listener volition burn on the other tabs and trigger a "logout" also and redirect users to the login screen.

          window.addEventListener('storage', this.syncLogout)   //....  syncLogout (event) {   if (effect.cardinal === 'logout') {     console.log('logged out from storage!')     Router.push('/login')   } }                  

These are the two things we now demand to do on logout:

  1. Nullify the token
  2. Fix logout particular 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 then that Apollo doesn't endeavour to use them         setJwtToken("")         setRefreshToken("")         // Tell Apollo to reset the store         // Finally, redirect the user to the domicile folio         signOut().then(() => {             // to support logging out from all windows             window.localStorage.setItem('logout', Date.now())             customer.resetStore().and 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 burn in all other tabs and redirect them to login screen.

This works across tabs. But how do I "force logout" of all sessions on unlike devices?!

We cover this topic in a piffling more detail in a department afterward: Force logout.

Silent refresh

There are ii major problems that users of our JWT based app will even so face up:

  1. Given our brusk expiry times on the JWTs, the user volition exist logged out every 15 minutes. This would be a adequately terrible feel. Ideally, we'd probably want our user to be logged in for a long time.
  2. If a user closes their app and opens it once more, they'll need to login once more. Their session is not persisted because we're not saving the JWT token on the client anywhere.

To solve this trouble, most JWT providers, provide a refresh token. A refresh token has ii properties:

  1. It can exist used to make an API call (say, /refresh_token) to fetch a new JWT token earlier the previous JWT expires.
  2. It can be safely persisted across sessions on the client!

How does a refresh token work?

This token is issued as part of authentication process along with the JWT. The auth server should saves this refresh token and associates it to a particular user in its own database, so that it can handle the renewing JWT logic.

On the customer, before the previous JWT token expires, we wire upwards our app to make a /refresh_token endpoint and grab a new JWT.

How is a refresh token safely persisted on the customer?

We follow the guidelines in the OWASP JWT Guide to prevent issues with customer-side storage of a token.

Improper client-side storage occurs when "an application stores the token in a manner 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 instance of XSS issue (Cookie attainable to JavaScript code or Token stored in browser local/session storage).

To prevent this, the following steps are taken:

  • Store the token using the browser sessionStorage container.
  • Add information technology as a Bearer HTTP Authentication header with JavaScript when calling services.
  • Add fingerprint information to the token.

By storing the token in browser sessionStorage container it exposes the token to existence stolen through a XSS attack. 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 the implementation of a fingerprint also serves to forestall Token Sidejacking from occuring, and is done according to the guidelines here

So what does the new "login" process look like?

Cypher much changes, except that a refresh token gets sent forth with the JWT. Let's take a wait a diagram of login process once more, but now with refresh_token functionality:

Login with refresh token

  1. The user logs in with a login API call.
  2. Server generates JWT token and refresh_token, and a fingerprint
  3. The server returns the JWT token, refresh token, and a SHA256-hashed version of the fingerprint in the token claims
  4. The un-hashed version of the generated fingerprint is stored as a hardened, HttpOnly cookie on the customer
  5. 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 look like?

Silent refresh workflow

Here's what happens:

  1. The refresh endpoint must bank check for the existence 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
  2. If either of these weather are not met, the refresh request is rejected
  3. Otherwise the refresh token is accustomed, and a fresh JWT access token is granted, resetting the silent refresh process

An implementation of this workflow using the apollo-link-token-refresh package, is something similar the below.
Using this every bit a non-terminating link will automatically bank check the validity of our JWT, and effort 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() {     return new TokenRefreshLink({         // Indicates the current country of access token expiration         // If token not however expired or user doesn't have a token (guest) true should be returned         isTokenValidOrUndefined: () => {             const token = getJwtToken()              // If there is no token, the user is not logged in             // We return true here, because in that location is no demand to refresh the token             if (!token) return truthful              // Otherwise, we check if the token is expired             const claims: JwtPayload = decodeJWT(token)             const expirationTimeInSeconds = claims.exp * 1000             const now = new Date()             const isValid = expirationTimeInSeconds >= now.getTime()              // Return true if the token is yet 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 = await fetch(process.env["NEXT_PUBLIC_HASURA_ENDPOINT"], {                 method: "Postal service",                 headers: {                     "Content-Blazon": "application/json",                 },                 torso: JSON.stringify({                     query: `                   query RefreshJwtToken($refreshToken: Cord!, $fingerprintHash: Cord!) {                     refreshJwtToken(refreshToken: $refreshToken, fingerprintHash: $fingerprintHash) {                       jwt                     }                   }                 `,                     variables: {                         refreshToken,                         fingerprintHash,                     },                 }),             })              return request.json()         },         // Callback which receives a fresh token from Response.         // From here nosotros can save token to the storage         handleFetch: (accessToken) => {             setJwtToken(accessToken)         },         handleResponse: (performance, accessTokenField) => (response) => {             // hither you tin parse response, handle errors, prepare returned token to             // farther operations             // returned object should be like this:             // {             //    access_token: 'token cord hither'             // }             return { access_token: response.refreshToken.jwt }         },         handleError: (err) => {             console.warn("Your refresh token is invalid. Endeavor to reauthenticate.")             panel.mistake(err)             // Remove invalid tokens             localStorage.removeItem("jwt")             localStorage.removeItem("refreshToken")         },     }) }                  

Referring back to the section addressing: "What will happen if I'm logged in on multiple tabs?", using sessionStorage for this ways nosotros won't be authenticated in new tabs (if they weren't created using "Duplicate tab") or windows.

A potential solution to this, while still remaining secure, is to use localStorage every bit an consequence-emitter again and sync sessionStorage between tabs of the aforementioned base URL on load.

This can be accomplished past using a script such as this on your pages:

          if (!sessionStorage.length) {     // Ask other tabs for session storage     localStorage.setItem("getSessionStorage", String(Date.at present())) }  window.addEventListener("storage", (outcome) => {     if (event.cardinal == "getSessionStorage") {         // Some tab asked for the sessionStorage -> send it         localStorage.setItem("sessionStorage", JSON.stringify(sessionStorage))         localStorage.removeItem("sessionStorage")     } else if (event.key == "sessionStorage" && !sessionStorage.length) {         // sessionStorage is empty -> fill information technology         const data = JSON.parse(event.newValue)         for (let primal in information) {             sessionStorage.setItem(key, data[key])         }     } })                  

Persisting sessions

Persisting sessions is against the OWASP security guidelines for clients and token authentication:

"... Retrieved even if the browser is restarted (Use of browser localStorage container)."

There is (at the time of writing) no way deemed acceptable that allows for a persistent user session after a browser has been fully closed and re-opened, unless the browser implementation retains tab session land (sessionStorage).

You may cull to store your token in localStorage or a Cookie instead, in guild to have persistent sessions across browser restarts, but doing so is at your discretion.

Note: For an ongoing discussion of this topic, meet https://github.com/OWASP/ASVS/issues/1141


Force 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 trouble that we need to worry almost: Force logout or, logging out of all sessions and devices.

The refresh token implementations from the sections above, evidence us that nosotros can persist sessions and stay logged in.

In this example, a unproblematic 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 demand whatsoever special treatment 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 want:

  1. The browser makes a request to a app URL
  2. The SSR server renders the page based on the user'south identity
  3. The user gets the rendered folio and so 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'south identity to the SSR server. The only mode to do this is via a cookie.

Since we've already implemented refresh token workflows via cookies, when nosotros make a request to the SSR server, we demand to brand sure that the refresh-token is also sent along.

Note: 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!

jwt-blogpost-6-ssr1

This is what the SSR server does:

  1. Upon receiving a request to render a particular page, the SSR server captures the refresh_token cookie.
  2. The SSR server uses the refresh_token cookie to get a new JWT for the user
  3. The SSR server uses the new JWT token and makes all the authenticated GraphQL requests to fetch the right data

Tin the user proceed making authenticated API requests once the SSR page has loaded?

Nope, not without some boosted fiddling around unfortunately!

Once the SSR server returns the rendered HTML, the only identification left on the browser well-nigh the user'south identity is the old refresh token cookie that has already been used by the SSR server!

If our app code tries to use this refresh token cookie to fetch a new JWT, this asking will fail and the user will become logged out.

To solve this, the SSR server after rendering the folio needs to ship the latest refresh token cookie, so that the browser can employ it!

The unabridged SSR flow, cease to finish:

jwt-blogpost-7-ssr2


Code from this blogpost (finished application)

Sample lawmaking for this blogpost with an terminate to end working app, with SSR capabilities is available here.

https://github.com/hasura/jwt-guide

The repository also contains the sample auth backend code.


Effort it out!

Ready upward a costless GraphQL backend with Hasura Cloud to try it out for yourself!

Brand sure you're on version 1.three or above and you're good to get.


References

  • JWT.io
  • OWASP notes on XSS, CSRF and similar things
    • OWASP JWT Cheatsheet
    • OWASP Awarding Security Verification Standard, v5
  • The Parts of JWT Security Nobody Talks About | Philippe De Ryck
    Lots of other awesome stuff on the web

Summary

Once you've worked through all the sections above, your app should now take all the capabilities of a modernistic app, using a JWT and should exist secure from the common major security gotchas that JWT implementations take!

Allow us know on twitter or in the comments below if you have any questions, suggestions or feedback!

Changelog

  • (12/28/2021) Recommendation to store token in Cookie changed to sessionStorage, per OWASP JWT guidelines to address Issue: Token Storage on Customer Side 0
  • (12/28/2021) Adopted OWASP Application Security Verification Standard v5 6 L1-L2 guidelines
    • Of note: Capacity 3 (Session Management) and eight (Data Protection)
  • (12/28/2021) Alter section on Persisting Sessions to contain OWASP guidelines on this
  • (12/28/2021) Sample application repo lawmaking updated 1
    • Update from Next.js 9 -> 12, update @apollo libraries to v3.x
    • Password hashing algorithm changed from bcrypt to native Node.js crypto.scrypt per OWASP guidelines 2 and to reduce number of external dependencies
    • Hallmark on frontend and backend modified to make use of a user fingerprint in improver to a token, per OWASP guidelines on preventing Token Sidejacking three
    • Example usage of TokenRefreshLink iv to manage silent refresh workflow added
    • Server endpoints integrated through Hasura Actions 5 rather than directly invoking from client
    • Adopt recommended use of crypto.timingSafeEqual() to forestall timing attacks

stallingsfortainge.blogspot.com

Source: https://hasura.io/blog/best-practices-of-using-jwt-with-graphql/

0 Response to "Authentication Session Has Expired Please Login Again"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel