identity & security

Refresh Token Security: Detecting Hijacking and Misuse with Auth0

Learn how to improve refresh token security. Explore how to implement advanced token misuse and hijacking detection rules using the Auth0 Detection Catalog.

Refresh tokens solve one of the fundamental dilemmas in modern application security: how to maintain a good user experience while rigorously enforcing security using short-lived access tokens. We don't want users to re-login interactively every time an access token expires. Instead, we use the refresh token to seamlessly fetch a new one behind the scenes.

Thus, refresh tokens are essential for balancing user experience and security, but they also introduce a critical security risk: long-term persistence for attackers. This article details three new detection methods in the Auth0 Detection Catalog for combating refresh token misuse.

A threat actor who hijacks a refresh token can simply keep exchanging it for new access tokens, maintaining long-term persistence and potentially staying undetected. This risk is compounded by two factors: refresh tokens often have very long lifespans in the context of Customer Identity and Access Management (CIAM), and Auth0's recent capability enables refresh tokens to unlock access to multiple resources with Multi-Resource Refresh Tokens (MRRT).

We clearly need robust mitigation strategies against the risk of a refresh token being compromised. Luckily, these strategies exist:

  • Storing refresh tokens securely in HTTP-Only cookies: When refresh tokens are stored in browser local storage, client-side cross-site scripting (XSS) is the primary risk. To avoid this, rigorous content security policies, strict input validation, and the use of modern JavaScript frameworks that mitigate XSS by default are all crucial.
  • Enabling refresh token rotation: When enabled, every time a valid refresh token is exchanged for a new access token, the old refresh token is immediately revoked and a brand new refresh token is issued. This also enables storing refresh tokens in local browser storage by mitigating the potential effect of XSS.
  • Automatic reuse detection: The idea behind this protection is simple: if a threat actor and the legitimate application are both using the token, they will inevitably overwrite each other. The first entity to use the token gets a new one, and the second entity (whether the threat actor or the legitimate user) tries to use the already revoked token. This results in a rotation policy violation. Auth0 recognizes an attempt to reuse a token and immediately invalidates the refresh token family.

To bolster existing prevention strategies, we introduce new detection guards offering an additional layer of protection against refresh token misuse. Given that refresh tokens are intended for long-term use, it is vital to have detection strategies in place to identify when a compromised token is actively being hijacked and misused.

To address this, the Auth0 Detection Catalog introduces three new strong and practical detection rules, all designed to signal a compromise related to refresh token misuse.

Common refresh token misuse indicators

1. Refresh token reuse detection

Detection rule: detections/refresh_token_reuse.yml

This detection utilizes automatic detection reuse mechanism when refresh token rotation is enabled. Logically, when this mechanism is triggered consistently in your Auth0 environment, leading to a rotation policy violation, it's a strong indicator for your security operations, and you’ll want to act upon these security signals. However, first, you need to detect it.

You can check your tenant logs directly with a Lucene query to see if you are exposed to this problem in your Auth0 Dashboard:

type: "ferrt" AND description: "Unsuccessful Refresh Token exchange, reused 
refresh token detected"

But the real power comes from adopting a Security Information and Event Management (SIEM) detection that calculates additional metrics. For example a query, calculating a number of affected users, can be generated with Sigma CLI from the provided YAML specification in any of the supported backends:

sigma convert \
--target splunk \
--pipeline splunk_windows \
detections/refresh_token_reuse.yml
Note: All three detections provided in this article leverage an advanced Sigma feature called correlation supported by three query languages. This Sigma feature allows calculating additional metrics by aggregating data (e.g. counting by event types and users). A simpler query can be generated with Sigma by removing the correlation part of the YAML file, i.e. coming after the “---” separator.

The Splunk query, available in the detection, calculates the number of policy violations in addition to the number of affected users, flagging them if the volume of failed exchanges exceeds a defined threshold:

data.type=ferrt data.description="Unsuccessful Refresh Token exchange, 
reused refresh token detected"
| fields data.user_id data.type
| stats count(data.type) as detected_reuse_counter by data.user_id
| where detected_reuse_counter > {excessive_reuse_threshold}
| eventstats dc(data.user_id) as affected_users_total_count
| where affected_users_total_count > {effected_users_threshold}
| table data.user_id detected_reuse_counter

Once you see spikes of refresh token rotation policy violations, you need to account for potential false positives:

  • Misconfiguration: This is the most common cause. Applications sometimes incorrectly cache refresh tokens, or they might send concurrent or near-concurrent requests for access tokens, resulting in a race condition where the first request invalidates the token used by the second. You must investigate the client application code and fix the caching or concurrency issues.
  • Inadequate leeway/overlap period: Some applications are designed to serve users with intermittent or unstable connections. In this case, an application can try to reuse a refresh token; this will be a legitimate case and should not cause a rotation policy violation. In this scenario, you need to configure an adequate leeway interval.

2. Refresh token exchange from excessive locations

Detection rule: detections/refresh_token_from_too_many_locations.yml

This indicator operates on the principle that a legitimate user’s refresh token activity should typically originate from a constrained set of network locations over its long life. Thus, this detection monitors the number of unique IP addresses that a single refresh token family is being used from successful exchange events. If a single refresh token is suddenly observed from a globally diverse set of IPs within a short time frame, this suggests the token has been leaked and misused.

The provided Splunk query covers two strategies for identifying anomalous location usage in addition to the Sigma correlation.

data.type IN (seacft, sertft)
    | fields data.ip data.user_id data.user_name data.client_id 
data.details.familyId
    | stats dc(data.ip) as rt_ip_count values(data.ip) as observed_ips
            by data.user_name data.user_id data.client_id data.details.familyId
    ``` Option 1 - Check the volume of used IPs (Suits SPA and Native apps)```
    | where rt_ip_count > {threshold_ips_count}
    ``` Option 2 - Check for unapproved IPs (Suits RWA)```
    ```
    | eval total_ip_count = mvcount(observed_ips)
    | eval approved_ip = mvfilter(match(observed_ips, {list_of_allowed_ips}))
    | eval approved_ip_count = mvcount(approved_ip)
    | where approved_ip_count != total_ip_count
    ```
    ``` Print the suspicious records ```
    | table data.user_name data.user_id data.client_id data.details.familyId 
rt_ip_count observed_ips
  • Option 1 (Volume Check): Checks if the count of distinct source IPs exceeds a static threshold. This is better suited for a Single-Page Application (SPA) or a Native Application where token exchange occurs client-side. This logic is also implemented with Sigma correlation in the provided YAML specification.
  • Option 2 (Allowed IPs Check): Identifies exchanges originating from IP addresses not present in a defined allowed-list. This is better suited for a Regular Web Application where the /token endpoint is called from a backend.

This detection can also account for advanced attacks where a threat actor is waiting for an application to be shut down before using the refresh token, thereby avoiding reuse detection as explained and demonstrated by Philippe De Ryck.

3. Refresh token exchange from multiple user agents

Detection rule: detections/refresh_token_from_multiple_user_agent.yml

This detection continues to build up on the foundation that a refresh token should be bound to a device, by examining the User-Agent request header.

When a refresh token is issued, it is initially associated with a single client application and device, identified by the HTTP User-Agent string. A legitimate application should consistently use the same, or a limited number of, User-Agent strings throughout the token's lifetime. If a single refresh token family is observed being exchanged by multiple distinct User-Agent strings, it is a strong indicator of compromise. For example, an attacker who hijacks a token via XSS might try to use it with a simple script or a common cURL command, immediately resulting in a different user agent than the legitimate mobile or web application.

The Splunk query provides two options for tailoring this detection to your application:

data.type IN (seacft, sertft)
    | fields data.user_agent data.user_id data.user_name data.client_id 
data.details.familyId
    | stats dc(data.user_agent) as ua_count values(data.user_agent) as 
observed_uas
            by data.user_name data.user_id data.client_id data.details.familyId
    ``` Option 1 - check the volume of used UA, suits SPA that can be loaded in 
different user agents```
    | where ua_count > {threshold_ua_count}
    ``` Option 2 - check for a particular allowed user agents, suits for RWA 
and Native Apps```
    ```
    | eval total_ua_count = mvcount(uas)
    | eval approved_ua = mvfilter(match(uas, "(?i){list_of_allowed_ua}"))
    | eval approved_ua_count = mvcount(approved_ua)
    | where approved_ua_count != total_ua_count
    ```
    ``` Print the suspicious records ```
    | table data.user_name data.user_id data.client_id data.details.familyId 
ua_count observed_uas

  • Option 1 (Volume check): This is the default approach in the query. It checks the volume of unique user agents and better suits SPAs that can be legitimately loaded in different user agents, for example, due to a browser update. This logic is also implemented with Sigma correlation in the provided YAML specification.
  • Option 2 (Allow-list check): This indicator fits scenarios where a legitimate User-Agent string can be predicted and specific. For example, for native applications or regular web application backends, you can enforce a strict rule by checking for unapproved user agents against a specific allowed list, i.e. {list_of_allowed_ua}. As an alternative to listing user agents, consider specifying a minimum allowed user agent version or specific key components, as threat actors often use outdated or generic user agents.

For all three detections, to determine a baseline of normal behavior and setting up thresholds, it is highly recommended to run the queries (excluding the where clauses) for some period to calculate thresholds based on data.

Proactive refresh token security with post-login Actions

The core response to all three types of risky behavior (reuse, location hopping, and multiple user agents) can be mitigated proactively using Auth0's extensibility features, specifically a post-login Action that fires during the refresh token exchange flow.

The key to this strategy is checking the token’s history against its current usage. Auth0 exposes critical fields on the event.refresh_token.device object that capture the initial and last network characteristics of the token's use. It also provides the core response mechanism, i.e., the api.refreshToken.revoke(reason) method.

The Action strategy:

  1. IP/ASN: Compare the IP/ASN of the initial issuance of a refresh token (event.refresh_token.device.initial_asn or initial_ip) against the IP/ASN of the current exchange request (event.refresh_token.device.last_asn or last_ip).
  2. User agent: Compare the user agent of initial issuance (event.refresh_token.device.initial_user_agent) against the user agent of the current exchange request (event.refresh_token.device.last_user_agent).

If any of these constraints are violated, the Action revokes the refresh tokens, and optionally the whole session can be terminated (affects all related applications). This measure is highly effective against sophisticated attacks, especially those where an adversary avoids rotation policy violations by timing their attacks.

Example Action script:

/**
* Handler that will be called during the execution of a PostLogin flow.
*
* @param {Event} event - Details about the user and the context in which 
they are logging in.
* @param {PostLoginAPI} api - Interface whose methods can be used to change the 
behavior of the login.
*/
exports.onExecutePostLogin = async (event, api) => {
  // 1. Ensure this Action only runs during a Refresh Token exchange
  if (event.transaction.protocol !== 'oauth2-refresh-token') {
    return;
  }

  const tokenDevice = event.refresh_token.device;
  const initialUA = tokenDevice.initial_user_agent;
  const lastUA = tokenDevice.last_user_agent;
  const initialIP = tokenDevice.initial_ip;
  const lastIP = tokenDevice.last_ip;
  const initialASN = tokenDevice.initial_asn;
  const lastASN = tokenDevice.last_asn;
  const userId = event.user.user_id;

  // --- Option 1. User Agent Check ---
  // A small deviation is acceptable (e.g., version bump), but a major change 
(like browser change) is suspicious.
  // This example uses a strict check for demonstration. You may need fuzzy 
matching.
  if (initialUA && lastUA && initialUA !== lastUA) {
    console.log(`[RT Policy Violation] User ${userId} - UA mismatch. Initial: 
${initialUA}, Last: ${lastUA}`);

    // Terminate session and revoke refresh tokens for this user
    api.refreshToken.revoke("Misuse detected.");

    // Optionally revoke the whole session and refresh tokens altogether: 
refresh_token_hijacked_ua_mismatch
    // api.session.revoke('Misuse detected.');
  }

  // --- Option 2. IP Check (Using IP for network-level stability) ---
  // If the initial and current IP are different, it indicates a location hop.
  if (initialIP && lastIP && initialIP !== lastIP) {
    console.log(`[RT Policy Violation] User ${userId} - IP mismatch. Initial: 
${initialIP}, Last: ${lastIP}`);

    // Terminate session and revoke refresh tokens for this user
    api.refreshToken.revoke("Misuse detected.");

    // Optionally revoke the whole session and refresh tokens altogether: 
refresh_token_hijacked_ip_mismatch
    // api.session.revoke('Misuse detected.');
  }

    // --- Option 3. ASN Check (Using ASN for network-level stability) ---
  // If the initial and current ASN are different, it indicates a large 
location hop.
  if (initialASN && lastASN && initialASN !== lastASN) {
    console.log(`[RT Policy Violation] User ${userId} - ASN mismatch. Initial: 
${initialASN}, Last: ${lastASN}`);

    // Terminate session and revoke refresh tokens for this user
    api.refreshToken.revoke("Misuse detected.");

    // Optionally revoke the whole session and refresh tokens altogether: 
refresh_token_hijacked_asn_mismatch
    // api.session.revoke('Misuse detected.');
  }

};

Raising the bar for access token security

While securing refresh tokens is crucial, their primary role is to allow for short-lived access tokens, which boosts security without sacrificing a high-level user experience. The use of short-lived access tokens mitigate three core security risks:

  1. Possession risk: Access tokens are bearer tokens—anyone possessing them can use them.
  2. Revocation risk: Access tokens are self-contained JWTs—we cannot instantly revoke them upon a security event.
  3. Outdated permissions risk: Access tokens are self-contained JWTs—the lifetime of the token dictates how long outdated permissions remain active.

Previously, all three risks were mitigated by assigning a very short lifetime to the token. However, we now have additional powerful and specialized techniques to address these risks:

  • Sender-constrained flows (Possession risk): Tokens can be cryptographically bound to the client that requests them. Auth0 supports mTLS sender constraining and Demonstrating Proof-of-Possession (DPoP).
  • OIDC Back-Channel Logout (Revocation risk): This mechanism offers an immediate, out-of-band method to notify clients and resource servers. It enables the revocation of tokens and sessions without delay, bypassing the need to wait for access tokens to expire.
  • Fine-Grained Authorization (FGA) (Outdated permissions risk): This enables management of highly dynamic and rapidly changing permissions within a centralized, real-time authorization service, rather than including them as claims in access tokens.

Next steps

Securing the long-lived refresh token is a non-negotiable step in building a resilient CIAM platform. By combining the best practices of secure storage with proactive, data-driven detection methods—like those for refresh token reuse, location hopping, and user agent changes—you can significantly raise the cost and complexity for any attacker.

The Auth0 Detection Catalog is a living library of security indicators that will continue to grow as the threat landscape evolves. We invite you to explore the existing detections and contribute your insights to help the entire community stay one step ahead of the next attack.