Manage User Sessions

Firebase Authentication sessions are long lived. Every time a user signs in, the user credentials are sent to the Firebase Authentication backend and exchanged for a Firebase ID token (a JWT) and refresh token. Firebase ID tokens are short lived and last for an hour; the refresh token can be used to retrieve new ID tokens. Refresh tokens expire only when one of the following occurs:

  • The user is deleted
  • The user is disabled
  • A major account change is detected for the user. This includes events like password or email address updates.

The Firebase Admin SDK provides the ability to revoke refresh tokens for a specified user. In addition, an API to check for ID token revocation is also made available. With these capabilities, you have more control over user sessions. The SDK provides the ability to add restrictions to prevent sessions from being used in suspicious circumstances, as well as a mechanism for recovery from potential token theft.

Revoke refresh tokens

Use the revokeRefreshToken() method to revoke the active refresh tokens of a given user.

You might revoke a user's existing refresh token when a user reports a lost or stolen device. Similarly, if you discover a general vulnerability or suspect a wide-scale leak of active tokens, you can use the listUsers API to look up all users and revoke their tokens for the specified project.

Password resets also revoke a user's existing tokens; however, the Firebase Authentication backend handles the revocation automatically in that case. On revocation, the user is signed out and prompted to reauthenticate.

Here is an example Cloud Function implementation that uses the Admin SDK to revoke the refresh token of a given user. In addition to revoking the token, it also updates some user-specific metadata in Firebase Realtime Database.

const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

// Revoke all refresh tokens for a specified user for whatever reason.
function revokeUserTokens(uid: string): Promise<void> {
  return admin.auth().revokeRefreshTokens(uid)
    .then(() => {
      // Get user's tokensValidAfterTime.
      Return admin.auth().getUser(uid);
    })
    .then((userRecord) => {
      // Convert to seconds as the auth_time in the token claims is in seconds too.
      const utcRevocationTimeSecs = new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
      // Save the refresh token revocation timestamp. This is needed to track ID token
      // revocation via Firebase rules.
      const metadataRef = admin.database().ref("metadata/" + userRecord.uid);
      return metadataRef.set({revokeTime: utcRevocationTimeSecs});
    });
}

Detect ID token revocation

Because Firebase ID tokens are stateless JWTs, you can determine a token has been revoked only by requesting the token's status from the Firebase Authentication backend. For this reason, performing this check on your server is an expensive operation, requiring an extra network round trip. You can avoid making this network request by setting up Firebase Rules that check for revocation rather than using the Admin SDK to make the check.

To enforce this check, set up a rule with no client write access to store the revocation time per user. This can be updated with the UTC timestamp of the last revocation time as shown in the previous examples:

{
  "rules": {
    "metadata": {
      "$user_id": {
        // this could be false as it is only accessed from backend or rules.
        ".read": "$user_id === auth.uid",
        ".write": "false",
      }
    }
  }
}

Any data that requires authenticated access must have the following rule configured. This logic only allows authenticated users with unrevoked ID tokens to access the protected data:

{
  "rules": {
    "users": {
      "$user_id": {
        ".read": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)",
        ".write": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)"
      }
    }
  }
}

In your backend, implement the following logic for refresh token revocation and ID token validation:

  • Any time a user's refresh tokens are revoked, the tokensValidAfterTime UTC timestamp is saved in the database node (as shown in examples above), so the check can also be enforced in the Database rules.
  • When a user's ID token is to be verified, the additional checkRevoked boolean flag has to be passed to verifyIdToken. If the user's token is revoked, the user should be signed out on the client or asked to reauthenticate using reauthentication APIs provided by the Firebase Authentication client SDKs.
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

// Verify ID token.
function verifyIdToken(idToken: string): Promise<Object> {
  // Verify the ID token while checking if the token is revoked by passing checkRevoked
  // as true.
  return admin.auth().verifyIdToken(idToken, true)
    .then(payload => {
      // Token is valid.
    })
    .catch(error => {
      // Invalid token or token was revoked:
      if (error.code == 'auth/id-token-revoked') {
        // When this occurs, inform the user to reauthenticate or signOut() the user.
        // Firebase Auth offers API to reauthenticateWithCredential /reauthenticateWithPopup /reauthenticateWithRedirect
      }
    });
}

Respond to token revocation on the client

If the token is revoked via the Admin SDK, the client is informed of the revocation and the user is expected to reauthenticate or is signed out:

function onIdTokenRevocation() {
  // For an email/password user. Prompt the user for the password again.
  let password = prompt('Please provide your password for reauthentication');
  let credential  = firebase.auth.EmailAuthProvider.credential(
      firebase.auth().currentUser.email, password);
  firebase.auth().currentUser.reauthenticateWithCredential(credential)
    .then(result => {
      // User successfully reauthenticated. New ID tokens should be valid.
    })
    .catch(error => {
      // An error occurred.
    });
}

Advanced Security: Enforce IP address restrictions

A common security mechanism for detecting token theft is to keep track of request IP address origins. For example, if requests are always coming from the same IP address (server making the call), single IP address sessions can be enforced. Or, you might revoke a user's token if you detect that the user's IP address suddenly changed geolocation or you receive a request from a suspicious origin.

To perform security checks based on IP address, for every authenticated request inspect the ID token and check if the request's IP address matches previous trusted IP addresses or is within a trusted range before allowing access to restricted data. For example:

app.post('/getRestrictedData', (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;
  // Verify the ID token, check if revoked and decode its payload.
  admin.auth().verifyIdToken(idToken, true).then((claims) => {
    // Get the user's previous IP addresses, previously saved.
    return getPreviousUserIpAddresses(claims.sub);
  }).then(previousIpAddresses => {
    // Get the request IP address.
    const requestIpAddress = req.connection.remoteAddress;
    // Check if the request IP address origin is suspicious relative to previous
    // IP addresses. The current request timestamp and the auth_time of the ID
    // token can provide additional signals of abuse especially if the IP address
    // suddenly changed. If there was a sudden location change in a
    // short period of time, then it will give stronger signals of possible abuse.
    if (!isValidIpAddress(previousIpAddresses, requestIpAddress)) {
      // Invalid IP address, take action quickly and revoke all user's refresh tokens.
      revokeUserTokens(claims.uid).then(() => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      }, error => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      });
    } else {
      // Access is valid. Try to return data.
      getData(claims).then(data => {
        res.end(JSON.stringify(data);
      }, error => {
        res.status(500).send({ error: 'Server error!' })
      });
    }
  });
});

Send feedback about...

Need help? Visit our support page.