identity & security

Best Practices for Application Session Management

Let's see how to maintain application sessions in different scenarios

Jan 25, 202217 min read

Managing application sessions when an Identity Provider (IdP) is involved may be challenging. The solution to these challenges may seem simple at first, but upon closer examination, we may discover problems that are not so simple to solve. For example, an application using Auth0 as its IdP can determine the validity of an Auth0 session via the use of

/authorize
endpoint. Customers often implement some kind of polling against Auth0
/authorize
endpoint to determine the session validity, and this may not be viable given the potential impact of hitting rate limits, ITP, and third-party cookie issues. This article describes scenarios on how to avoid such issues and implement approaches to maintain app sessions.

Sessions

A session identifies the user to the app after they have logged in and is valid for a period of time during which the user can perform a set of interactions within that application. A single session can contain multiple activities (such as page views, events, social interactions, and e-commerce transactions), all of which are kept in the session storage temporarily while the user is connected.

Session layers

In the Auth0 context, there are basically three layers of sessions:

  • Application session. Though the application uses Auth0 to authenticate users, it may still need to track that the user has logged in to the application. For this, it may have to create a session (for example, by using cookies or depending on its access token expiration).
  • Auth0 session. Auth0 also keeps a session for the user and stores their information inside a cookie. The next time a user is redirected to the Auth0 login page, the user's information will be inferred.
  • IdP session: This session is involved when Auth0 is federating to another third-party IdP, such as Google, Twitter, and so on.

Sessions vs. tokens

In addition to the above session layers, the application also has to be aware of token expirations, especially in OIDC flows.

When a user successfully logs in, a session is created and maintained by Auth0 and indicates that the user has logged in and does not need to re-authenticate for the duration of that session. The session will last until a set expiration time, or the user logs out, or the SSO session cookie is deleted from the user’s browser. So if a user leaves an application but later returns and attempts to log in before the session expires, they will not have to enter their credentials again.

On the other hand, tokens are signed information that Auth0 sends back to the client application in a way to securely exchange the user authentication and authorization decisions with the client applications. Access tokens represent authorization and are intended to grant the bearer access to an API either on behalf of a user or an application. ID tokens represent authentication, contain information about the user who authenticated, and are intended for the application the user is using.

Read this article to learn more about the difference between ID and access tokens.

SSO and logout

Single Sign-on (SSO) occurs when a user logs in to one application and is then signed in to other applications in that agent/browser automatically. The user signs in only one time, hence the name of the feature (Single Sign-on).

Whenever users go to a domain that requires authentication, they are redirected to the centralized authentication domain where they may be asked to log in. If the user is already logged in at the authentication domain, the central authentication server will automatically re-authorize and re-consent (if required) the user’s request to the application and then the user can be immediately redirected to the original domain without signing in again.

Logout in the context of Auth0 implementation is the act of terminating an authenticated session. Auth0 provides tools to help you give users the ability to log out. This includes options for providing different levels of logout and also determining where the user will land after the logout is complete:

  • Application Session Layer Logout. Logging users out of your applications typically results in their application session being cleared, and this should be handled by your application. For the Application Session Layer, there is nothing within your Auth0 tenant that you need to use to facilitate session termination.
  • Auth0 Session Layer Logout. You can log users out of the Auth0 session layer by redirecting them to the Auth0 logout endpoint so Auth0 can clear the SSO cookie.
  • Identity Provider Session Layer Logout. It is not necessary to log the users out of this session layer, but you may be able to use Auth0 to force the logout, if required, by passing a parameter to the Auth0 logout endpoint (if supported by third-party IdP). This applies to SAML scenarios and other scenarios covered in the next sections.

Sessions in SPAs and Regular Web Apps

Let's dig deeper into two specific application types: Regular Web Applications and Single Page Applications (SPAs).

App session with a backend

Applications with a backend may track sessions with a server-side application session (a server-side cookie). This cookie may track its expiration based on the token response from Auth0. If the cookie expires, the backend has a few options to get the token response from Auth0 and then slide the app session:

  • Perform a redirect (302 HTTP status code) to the Auth0
    /authorize
    endpoint. Here, if the session is still valid on Auth0, then new tokens are replied back on the callback service without asking the user to re-login.
  • During initial authentication, request for a refresh token. Later, the backend can use this refresh token to get the new tokens when the app session expires. This approach makes your backend a stateful service and can be extended to be a centralized session management service. The session cookie may only store an anchor (an identifier stored in it), and every time the front end makes a call to the backend, the cookie is associated with the request. The backend will get the refresh token (if required) from the storage using the mapped identifier (or anchor) in the app session cookie. However, this solution will be useful only when the frontend and backend are tied to the same TLD.

Review our Regular Web App Quickstarts & SDKs for the implementation details.

Read this article to learn more about refresh tokens.

App session on SPA

Our SDKs follow the current best practices for SPAs. When the application loads, our SDKs check for a valid refresh token (if there was a previous valid rotating refresh token issued to the SPA). As a fallback mechanism if no valid refresh token exists, the SDK does a silent authentication (

prompt=none
call). Only if all these steps fail, a complete redirect to Auth0 happens to check for SSO sessions.

If any of the above validations succeed (i.e., refresh token validation OR silent authentication OR stateful request to Auth0 cookie), then the user is logged in to the application automatically. Otherwise, the app can show a public page, or the Auth0 SDK will redirect the user to the Universal login page to complete an interactive authentication flow.

Client applications can still perform a

prompt=none
request only when there is an active user in that app and the client app is unable to rehydrate the cache with an existing rotating refresh token. A typical case is when the rotating refresh token is stored in memory and is lost on page refresh: in this case, the fallback is a
prompt=none
call to rehydrate the app session.

This approach will avoid multiple unnecessary calls to Auth0 and will have a better user experience.

SPA Session Issues with Silent Authentication

Every SPA application, if guarded with the Auth0 SDK, makes a

prompt=none
call to Auth0 when the app is loaded except when there is a valid rotating refresh token in the browser storage. This is required to seamlessly load the app session by checking for a valid Auth0 SSO cookie on that browser.

However, in a few cases, this can lead to rate limits if the client application is continuously polling against Auth0’s

/authorize
endpoint with
prompt=none
. For example:

  • The client application might have kept its local app session short to follow best practices. Therefore the client application keeps refreshing its local session by continuously polling for the validity of the Auth0 SSO cookie with
    prompt=none
    iframe calls.
  • Poor client app implementation can lead the above polling to happen even when the user remains inactive (i.e., in the background).
  • If the user remained inactive beyond the inactivity SSO timeout in Auth0, then Auth0 responds with “login_required”. However, the polling would continue in the background regardless of the error and therefore increase unwarranted traffic to Auth0 and the client application.

Removing polling on client applications is necessary to reduce a significant amount of invalid network calls against Auth0 as well as the client app. This is also required to avoid rate limits on the Auth0 Authentication API.

A Multi-Domain SPA Use Case

If you have made it this far, I believe you already know the best practices for maintaining app sessions. Most of the client implementations are not complex and might be able to establish sessions using Auth0 SDKs, as I described in the previous sections. However, it is not always simple and I want to cover a complex use case.

Let’s consider a scenario where multiple Single Page Applications are hosted on different domains (e.g., travel0.us, travel0.de, travel0.uk, etc.), and Auth0 is tied to the login.travel0.com domain. The requirement is for users to be able to use Single Sign-on (SSO) and Single Logout (SLO) across all these SPAs seamlessly.

The issues with multiple domains

In this use case, Auth0 supports SSO out of the box using Universal Login. However, every SPA application in our above example should do a redirect to Auth0 to be able to load its local session. At times, customers want their users to seamlessly land on a logged-in page for every SPA on that browser if there is a valid session without redirects. This seamless landing on a logged-in page will require an iframe

prompt=none
call to Auth0. Unfortunately, because SPA and Auth0 are tied to different top-level domains, this will not work in Safari due to ITP and, eventually, in Chrome due to Google’s efforts to phase out the third-party cookies.

On the other side, the Auth0 logout endpoint does not cover all scenarios where users need to be signed out of all of the applications they used on that browser. Other than when Auth0 is using SAML, it does not natively support Single Logout (SLO).

A few solutions could be implemented to achieve the above scenarios and can be categorized into two patterns: push and poll.

Polling-Based Solutions

Polling happens when the client application polls against an endpoint to determine the validity of a session. We can use a few alternative approaches, as it will be shown in the following.

Poll against Auth0 using silent authentication

A common approach is where all the client applications periodically poll against Auth0 with

prompt=none
iframe calls to check if the user has initiated a logout request to Auth0 from any one of the apps, and if so, it performs a local logout.

This is commonly known as silent authentication: if the response is login_required from Auth0, the user will be logged out from the app; otherwise, the application will receive a new set of tokens to re-establish its local session.

Note that this approach will not work in Safari and in Chrome when SPAs and Auth0 are tied to different top-level domains for the reason we explained earlier.

This also has a major impact on the application and end-users if the Auth0 authentication API rate limits are not considered.

Poll against a client-side endpoint

You can consider polling on the client side to track logout. Set a client-side SLO cookie (e.g.,

slo_flag: true
) associated with the SPA domain when the user initiates a logout. Every other app on that browser can poll for that client-side cookie to verify if there has been a logout issued against Auth0 and then kill its local session.

This approach can also be extended to determine if the login has happened on a different SPA in the same browser using a different flag,

sso_flag: true
. Only upon detecting that flag the current application can make a
prompt=none
call to establish its local session. This will reduce the number of silent authentications to Auth0. A similar SSO approach is used by Auth0 SDKs.

This approach also has the same impact discussed earlier in Safari and Chrome when SPAs and Auth0 are tied to different top-level domains.

Redirect and chain login/logout

To avoid ITP issues when dealing with multiple TLDs and as an alternative to client-side cookie polling via iframe, you can implement a redirect using a common server-side login/logout endpoint. This common endpoint can be called by Auth0 redirect rules or redirect Actions if it is to chain login requests. Alternatively, the common endpoint can itself be the application callback, i.e., the

redirect_uri
/
returnTo
parameters that Auth0 redirects the user to after successful login/logout. This callback chains the login/logout session to every other app in that browser.

This is similar to Google/YouTube approach, where a common endpoint on Google redirects to YouTube to set its session.

This approach could become complex if there are many applications in the ecosystem. The number of redirects will be equal to the number of applications and could lead to user experience issues if one of the redirects fails.

This implementation also depends on customer needs and the actual behavior of the browser.

Consider race conditions on how the

/authorize
calls are initiated against Auth0.

Push Events

The push approach is when every application calls a common logout route that pushes logout events to all the apps in the ecosystem. This approach can solve problems like rate limiting and third-party cookie restrictions with browsers, giving a better UX. Let's explore a couple of possible implementations.

Using WebSocket

A WebSocket API can be used to push the SLO event across apps that have subscribed to that SLO event and are open within a user-agent. Here is a breakdown of its behavior:

  1. On logout, all SPA applications can call the common logout URL (e.g.,
    https://logout.common.com/logout
    ).
  2. The common logout URL is a backend service in this example and will push an event to all SPA applications. Here is a basic code sample for a logout service:
/*
*NOTE: This is sample code that references to an open library `pusher` as an example. The customer is free to use any messaging websocket service.
*The code cannot be used in production code without testing & security review. This may need alteration to be production ready
*/
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const Pusher = require("pusher");
const app = express();

const corsOptions = {
    origin: '<CALLING APP URL>'
  };
app.use(cors(corsOptions));

// create application/json parser
let jsonParser = bodyParser.json()
const pusher = new Pusher({
  appId: "YOUR_PUSHER_APP_ID",
  key: "YOUR_PUSHER_APP_KEY",
  secret: "YOUR_PUSHER_APP_SECRET",
  cluster: "YOUR_PUSHER_APP_CLUSTER",
  useTLS: true
});

app.get('/', (request, response) => {
});

//Protect this endpoint with an access_token
app.post('/api/v1/trigger/slo-event', jsonParser, function (req, res) {
    pusher.trigger(req.body.channelName, "slo-event", {
       slo: true
      });
});

app.listen(3002, () => {
  console.log('Server running on localhost:3002');
});
  1. The client SPA will call logout by subscribing to the channel event, as shown in the following code snippet:
/*
*NOTE: This is sample code that references to an open library `pusher` as an example. The customer is free to use any messaging websocket service.
*The code cannot be used in production code without testing & security review. This may need alteration to be production ready
*/

//..other code on the client SPA

let channelName = '';

async function postData(url = '', data = {}) {    
    const response = await fetch(url, {
      method: 'POST',
      mode: 'cors'
      cache: 'no-cache',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json'
      },
      redirect: 'follow',
      referrerPolicy: 'no-referrer',
      body: JSON.stringify(data)
    });
    return response.json();
  }  

//SLO event triggered within the app on visiting logout route or clicking a button
const triggerSLO = () => {
  //Invoking below endpoint will send SLO notification to other apps
  postData('<SERVER-URL>/api/v1/trigger/slo-event', { channelName: channelName })
    .then(data => {
      localLogout();
    });
}

const localLogout = () => {
  try {
    console.log("local Logging out");
    auth0.logout({
      returnTo: window.location.origin
    });
  } catch (err) {
    console.log("Log out failed", err);
  }
};

const pusher;
function setupPusher(channelName) {
  pusher = new Pusher(YOUR_PUSHER_APP_KEY, {
    cluster: YOUR_PUSHER_APP_CLUSTER,
    //OTHER_AUTH_PARAMS
  });
  var channel = pusher.subscribe(channelName);
  channel.bind('slo-event', (data) => {
    if (data.slo === true) {
      localLogout();
    }
  })
}

//Get fingerPrint of the user-agent to identify the user logged in applications
  const fpPromise = import('https://openfpcdn.io/fingerprintjs/v3')
                    .then(FingerprintJS => FingerprintJS.load());

  async function getVisitorData() {
    const fp = await import('https://openfpcdn.io/fingerprintjs/v3')
      .then(FingerprintJS => FingerprintJS.load({ token: '<TOKEN>' }));
    return await fp.get({ extendedResult: true });
  }

// Will run when page finishes loading
window.onload = async () => {    
  const { visitorId, incognito} = await getVisitorData();
  if (incognito) {
    channelName = visitorId + '_incognito';    
  } 
  setupPusher(channelName);
};

Using iFrame postMessage

In this alternative approach, every application will call a common logout route which in turn pushes messages to all apps using

window.postMessage
via iFrame. Each app can subscribe to the message event. Here are the steps of this flow:

  1. On logout, all applications would use the common logout URL (e.g.,
    https://logout.common.com/logout?slo
    ).
  2. The common logout URL will call each SPA's logout route in iframes. The common logout service can be registered with a list of allowed application logout URLs in the ecosystem or get the list of clients the user authenticated in the token from an Auth0 rule using
    context.sso.current_clients
    .
/*
*NOTE: This is sample code and can not be used in production code without testing and may need alteration to be production ready
*/
if (window.location.search.includes('slo')) {
   var iframe = document.createElement('iframe');
   iframe.src = 'https://child.anotherdomain.com/logout?slo';
   iframe.width = 0;
   iframe.height = 0;
   document.body.appendChild(iframe);
 };
  1. The child SPA will clear its local session and send a message to the parent window
/*
*NOTE: This is sample code and can not be used in production code without testing and may need alteration to be production ready
*/
if (window.location.search.includes('slo')) {
   return logout();
 }
const logout = () => {
 try {
   console.log("local Logging out");
   auth0.logout({
     localOnly: true
   });
   window.parent.postMessage("logout from child.anotherdomain.com", 'https://logout.common.com/logout');
 } catch (err) {
   console.log("Log out failed", err);
 }
};
  1. On successfully receiving a message from the child frame, the common logout URL will call Auth0 logout and finally return to the calling application.
/*
*NOTE: This is sample code and can not be used in production code without testing and may need alteration to be production ready
*/
window.addEventListener("message", (event) => {
 if (event.origin !== "https://child.anotherdomain.com")
   return;
 logout(event.origin);
}, false);

const logout = (targetRedirect) => {
 try {
   auth0.logout({
     returnTo: targetRedirect
   });
 } catch (err) {
   console.log("Log out failed", err);
 }
};

In the above example, Auth0 logout is only called after all SPA local sessions are removed. Depending on the use case, the client can call Auth0 logout first and, on logout, redirect to

returnTo
. The common logout endpoint can then chain the logout requests to other SPAs via iFrames.

Consider race conditions where the logout is in progress, and the user tries to log in at the same.

Summary

This article explored the different approaches to managing application sessions with Auth0 authentication. The different use cases depend on the specific application type (SPA or Regular Web App), the particular scenario, and the customer's requirements.

The following image summarizes the approaches discussed in this article and the issues they are able to overcome or not:

A summary of different approaches to managing sessions