Firebaseui-web: How can I refresh firebaseAccessToken from the browser?

Created on 12 May 2017  路  9Comments  路  Source: firebase/firebaseui-web

Hello, could someone point me how to refresh firebaseAccessToken from the browser?

I am using firebaseUI to auth in the browser, and send firebaseAccessToken to my hosted server via the cookie.

It is done by including the following script on pages where require authorization. Currently it redirects users to login page when the firebaseAccessToken is expired, and the token is refreshed on the login page within the firebaseUI's sign-in box.

What I would like to achieve is to refresh the token within this script WITHOUT redirecting to the login page. I looked into the docs but could not find exact answer as I probably am not fully understand how the firebaseUI handles the token generation.

It would be awesome if someone could point me to any example or source code to refer to.
Thanks a lot in advance!

<script>
  initApp = function() {
    firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        // User is signed in.
        user.getToken().then(function(accessToken) {
          document.cookie = "firebaseAccessToken=" + accessToken + '; path=/';
          if (document.getElementById("firebaseAccessToken")) {
            document.getElementById("firebaseAccessToken").value = accessToken;
          }
        });
      } else {
        // console.log('user signed-out');
        location.href = '/users/login?signInSuccessUrl=' + encodeURIComponent(window.location.pathname);
      }
    }, function(error) {
      console.log(error);
      // console.log(document.cookie);
    });
  };
   window.addEventListener('load', function() {
    initApp();
  });
</script>

Most helpful comment

Hey @goodpic you seem to be adding a firebase.auth().onAuthStateChanged (without unsubscribing it) on each poll which is not efficient. When a token change is detected, all previous listeners will trigger. How about this simpler logic which should work when the user is signed in or signed out.

onAuthStateChanged (add this observer to every page):
  clear any existing timer
  get saved ID token and expiration from cookie if available.
  if user
    getToken()
    if same saved token (should happen on page reload)
      set timer to same expiration time saved in cookie. this will run getToken(true) when triggered
    else
      Update cookie with new token and expiration (1 hour)
      set timer to next hour. this will run getToken(true)
    If this is an intermediate page (cookie was expired), redirect the user to the intended
    page that requires the user to be authenticated.
    This may happen if the user hasn't accessed your site in a while.
  else no user
    clear cookie if not already cleared.
    Redirect to login page unless already there. If already there, render Sign-in widget.

All 9 comments

The Firebase ID token needs to be refreshed every hour. So you have to set your own proactive token refresh logic. You can every 55 minutes, force refresh via getToken(true) and save the token in the cookie.
You will need to include that script on every page.
However, there will always be a case where the token expires (user hasn't visited your website for longer than an hour). You need to add a proxy page when no cookie is available and append in the URL query parameters the intended destination. The page has the following logic:
sets an onAuthStateChanged listener on load
if no user detected, redirect to sign in page.
if user detected (token refreshed in the process undeneath), save new token in cookie and then redirect to intended destination

Thank you for your advice @bojeil-google .
The proactive token refresh would be good solution for my use-case. Probably I will save the another cookie to record the 55 minutes timer, and forceRefresh when calling getToken(). That will resolve current annoying every hour redirection!

https://firebase.google.com/docs/reference/js/firebase.User#getToken

Thanks a lot!

This seems to be working fine.

<script>
initApp = function() {
  firebase.auth().onAuthStateChanged(function(user) {
    // Check user is signed in.
    if (user) {
      // Timer to force refresh firebaseAccessToken every 55 minutes.
      var refresh = false;
      var dt = new Date();
      var elapsed = 0;
      var cookie = "; " + document.cookie;
      var parts = cookie.split("; firebaseAccessTimer=");
      if (parts.length == 2) {
        elapsed = dt.getTime() - parts.pop().split(";").shift();
      }

      if (55*60*1000 < elapsed) {
        document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
        refresh = true;
      }
      user.getToken(refresh).then(function(accessToken) {
        document.cookie = "firebaseAccessToken=" + accessToken + '; path=/';
        if (document.getElementById("firebaseAccessToken")) {
          document.getElementById("firebaseAccessToken").value = accessToken;
        }
      });
    } else {
      // console.log('user signed-out');
      document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
       location.href = '/users/login?signInSuccessUrl=' + encodeURIComponent(window.location.pathname);
    }
  }, function(error) {
    document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
    console.log(error);
  });
};
window.addEventListener('load', function() {
  initApp();
});
</script>

Hey @goodpic, the code provided seems to work for these case:

  1. Update cookie of existing user when token is expired
  2. refresh token and update cookie of existing user when token is about to expire

The code does not proactively refresh the token while the user has the page open.
The code does not care if a new user logged in. In that case, you need to reset your cookies and timers.

Hi @bojeil-google . Yes that's true :P Something like this?

<script>
  window.addEventListener("DOMContentLoaded", function(){
    (function poll(){
      firebase.auth().onAuthStateChanged(function(user) {
        // Check user is signed in.
        // console.log(user);
        if (user) {
          // Timer to force refresh firebaseAccessToken every 50 minutes.
          var refresh = false;
          var dt = new Date();
          var elapsed = 0;
          var cookie = "; " + document.cookie;
          var parts = cookie.split("; firebaseAccessTimer=");
          if (parts.length == 2) {
            elapsed = dt.getTime() - parts.pop().split(";").shift();
          }

          if (50*60*1000 < elapsed) {
            document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
            refresh = true;
          }

          user.getToken(refresh).then(function(accessToken) {
            document.cookie = "firebaseAccessToken=" + accessToken + '; path=/';
            if (document.getElementById("firebaseAccessToken")) {
              document.getElementById("firebaseAccessToken").value = accessToken;
            }
          });
          //Setup the poll recursively
          setTimeout(poll, 300000);
          } else {
            // console.log('user signed-out');
            document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
            location.href = '/users/login?signInSuccessUrl=' + encodeURIComponent(window.location.pathname);
          }
        }, function(error) {
          document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
          console.log(error);
          // console.log(document.cookie);
        });
      })();
  }, false);
</script>

and also reset the timer and cookie on the login page.

<script>
  // FirebaseUI config.
  var uiConfig = {
    signInSuccessUrl: '<%= redirectURL %>',
    signInOptions: [
      firebase.auth.FacebookAuthProvider.PROVIDER_ID,
      firebase.auth.EmailAuthProvider.PROVIDER_ID,
    ],
    tosUrl: '<%= tosURL %>'
  };

  // Initialize the FirebaseUI Widget using Firebase.
  var ui = new firebaseui.auth.AuthUI(firebase.auth());
  ui.start('#firebaseui-auth-container', uiConfig);

  initApp = function() {
    firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        // console.log('user signed-in');
        user.getToken().then(function(accessToken) {
          if (uiConfig.signInSuccessUrl === 'undefined') {
            uiConfig.signInSuccessUrl = '/users/profile';
          }

          var dt = new Date();
          document.cookie = "firebaseAccessTimer=" + dt.getTime() + '; path=/';
          document.cookie = "firebaseAccessToken=" + accessToken + '; path=/';
          location.href = uiConfig.signInSuccessUrl + '?firebaseAccessToken=' + accessToken;
        });
      } else {
        // User is signed out.
        $('#firebaseui-auth-container').show();
      }
    }, function(error) {
      console.log(error);
    });
  };

  window.addEventListener('DOMContentLoaded', function() {
    initApp();
    $('#firebaseui-auth-container').show();
  });
</script>

Hey @goodpic you seem to be adding a firebase.auth().onAuthStateChanged (without unsubscribing it) on each poll which is not efficient. When a token change is detected, all previous listeners will trigger. How about this simpler logic which should work when the user is signed in or signed out.

onAuthStateChanged (add this observer to every page):
  clear any existing timer
  get saved ID token and expiration from cookie if available.
  if user
    getToken()
    if same saved token (should happen on page reload)
      set timer to same expiration time saved in cookie. this will run getToken(true) when triggered
    else
      Update cookie with new token and expiration (1 hour)
      set timer to next hour. this will run getToken(true)
    If this is an intermediate page (cookie was expired), redirect the user to the intended
    page that requires the user to be authenticated.
    This may happen if the user hasn't accessed your site in a while.
  else no user
    clear cookie if not already cleared.
    Redirect to login page unless already there. If already there, render Sign-in widget.

@bojeil-google Thank you! This is exactly what I was looking for. I will try to implement it, and get back with the code.

Thanks a lot @bojeil-google for your suggestion. I updated the code to listen onAuthStateChanged once, and tried to reproduce the logic you described. Do you think that the following implementation work as you intended?

<script>
  var docCookies = {
    getItem: function (sKey) {
      if (!sKey || !this.hasItem(sKey)) { return null; }
      return unescape(document.cookie.replace(new RegExp("(?:^|.*;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*"), "$1"));
    },
    hasItem: function (sKey) {
      return (new RegExp("(?:^|;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
    }
  };

  window.addEventListener("DOMContentLoaded", function(){
    firebase.auth().onAuthStateChanged(function(user) {
      var refresh = false;
      (function poll(){
        var dt = new Date();
        var timeout = 55*60*1000;
        if (docCookies.getItem('firebaseAccessTimer') - dt.getTime() < 0) {
          refresh = true;
        }

        if (user) {
          user.getIdToken(refresh).then(function(accessToken) {
            if (accessToken === docCookies.getItem('firebaseAccessToken')) {
              window.setTimeout(poll, docCookies.getItem('firebaseAccessTimer') - dt.getTime());
            } else {
              document.cookie = "firebaseAccessToken=" + accessToken + '; path=/';
              document.cookie = "firebaseAccessTimer=" + (dt.getTime() + timeout) + '; path=/';
              window.setTimeout(poll, timeout);
            }
            if (document.getElementById("firebaseAccessToken")) {
              document.getElementById("firebaseAccessToken").value = accessToken;
            }
          });

        } else {
          // console.log('user signed-out');
          document.cookie = 'firebaseAccessTimer=0; path=/';
          location.href = '/users/login?signInSuccessUrl=' + encodeURIComponent(window.location.pathname);
        }
      })();
    }, function(error) {
      console.log(error);
      // console.log(document.cookie);
    });
  }, false);
</script>

I've made it with rxjs and store all necessary data in a store with vuex.

const source = interval(6000)
source.subscribe(() => {
  updateAuthInfo()
})

function updateAuthInfo() {
  const currentUser = firebase.auth().currentUser
  if (currentUser) {
    const currentTime = Math.ceil(new Date().getTime() / 1000)
    const expire = store.getters['auth/expire']
    const fiveMinutes = 300
    const refresh = expire && (expire - fiveMinutes) < currentTime
    if (refresh) {
      currentUser.getIdToken(refresh).then((accessToken) => {
        console.log('refresh', refresh)
        store.dispatch('auth/updateAccessToken', accessToken)
      })
    }
  }
}

new Vue({
  ...
  created() {
    firebase.initializeApp(config)
    firebase.auth().onAuthStateChanged((user) => {
      ...
    }
  }
})
Was this page helpful?
0 / 5 - 0 ratings