Firebase refresh-token expiration
Asked Answered
C

3

10

While testing the security of one of our product, a web application, using the REST API of Firebase we got surprised when we realised that refresh-tokens never expire in the V3 of the Firebase implementation, allowing any refresh-token to create new tokens forever.

While local-storage seem a reasonably safe solution today, we are concerned by the possibility that it could fail tomorrow, even for a short amount of time, and that we cannot stop someone from using any of these refresh-tokens.

Two factor authentication will help mitigate the issue, but the first step would become compromised nonetheless.

Is there a way to blacklist tokens, or similar behaviour, with Firebase, without handling all tokens exchange, such as minting, ourselves? We could not find such feature when going through the doc.

Any advice appreciated.

Cynde answered 19/5, 2017 at 13:4 Comment(3)
I don't know is it about me....But i hate firebase because it has all thise features pre done, and i feel that I'm not doing anything.I'm okay with google app engine, lots of useful tools.Personally when i started with OAuth and tokens i made my own scripts that handled all that stuff.If you want i can help you out with it...Pericles
Thanks @Arslan.H for the help but we can handle this ourselves :) We are curious to hear how other people handle this in Firebase, we could not find a lot of people concerned about this in our research.Cynde
We are also concerned. We have opened tickets with the firebase team; but so far do not have any steps for remediation to share.Gigantes
G
5

Firebase recently implemented revokeRefreshTokens() inside the admin sdk. Although this will not let you kill an invalid JWT, it does allow you to prevent a refresh of the token (from my testing so far at least) and it allows cleaner control flow inside firebase database.

See Admin Manage Sessions For rough examples

Gigantes answered 7/2, 2018 at 6:24 Comment(1)
Thanks for the update, I believe the refresh token expiration rules have also been updated: I don't remember seeing "major account changes" leading to existing refresh token expiring (at least not in the documentation).Cynde
U
6

Authentication sessions don't expire with Firebase login. But the ID token will have to be refreshed hourly, to keep access to the services. If you disable an account, refreshing the token will fail and the account won't be able to access services anymore. There is no way to invalidate individual tokens.

Unvoiced answered 19/5, 2017 at 14:9 Comment(12)
As ID token are refreshed with the refresh-token, this does not solve our problem. Thank you however for confirming this. I wonder if someone found an elegant solution to this, because so far we only came up with a heavy solution that we would like to avoid.Cynde
Firebase Auth runs on the client side and Firebase sessions are long lived. Refresh tokens have to reside on the client side in order to make this possible. On sign out, they are cleared from web storage. There is no way to invalidate them. Unless you are vulnerable to XSS attacks, and as long as you are not passing around refresh tokens, you should be fine. Can you explain the specific concern you have?Prizewinner
[reformulated] We are concerned about the possibility of wild free pass for the first step of a user, where the only way to get rid of it is to ask the user to create a new account. While using local storage for the token reasonably safe today, it might become unsafe tomorrow and we currently have no easy way of stopping someone from using that token.Cynde
@Prizewinner "Unless you are vulnerable to XSS attacks" With the current front end landscape everyone should be worried about being vulnerable to XSS attacks/localstorage leaves a large vector of attack that is being exposed to the entire javascript application dependency graph.Gigantes
Hey @Levi, if malicious code is able to run on your website, you are basically owned. Attackers could do anything. Developers should be vigilant to how they store and present data in their templating system, and scrutinize third party code that runs on their app.Prizewinner
Hello @Prizewinner ! One of the peculiarities of firebase auth is the JWT token is sitting in localstorage and is exposed to all javascript. This is different than other auth, that store the JWT in a http-only cookie. This means an xss attacker can take a user session to another machine, and keep it alive 'forever'. That last part is what is not the same as other systems. A compromised client is game over you are 100% right, but with firebase it's a more catastrophic game over. Firebase provides many features, but also new security challenges for front end developers.Gigantes
Firebase is optimized to run on the client. httpOnly cookies will not work. There is no way to access the tokens via javascript. So yeah it is safer but Firebase will not work with it. If you are hosting your own server, you can add your additional layer where you mint your own token after sign in and set it via http-only.Prizewinner
http only cookies would work...the auth Domain is the same as storage and database. So they could be added with a bearer token header by the fire base server without JavaScript intervention. Here is an example from storm path (They are going out of business, but the http-only example is no less valid.) stormpath.com/blog/…. Sorry for the many edits bojeil! I am on my phone. Let me know what you think. I am more worried about XSS + local storage than CSRF. a uid and some user data is way safer to store in LS than the entire JWT.Gigantes
Unfortunately, it is not that easy Levi. Auth domain is firebaseapp.com, database is firebaseio.com and storage is appspot.com. In addition a lot of apps use their own custom domains. I acknowledge Firebase Auth can do more to guard against XSS vulnerabilities. There are many ways to help do that, some have been already brought up here. However, its current system architecture makes it harder to work with HTTP-only cookies. Its client side API already exposes the ability to get the ID token/refresh token. That said, I will relay these thoughts and suggestions to the Firebase Auth team.Prizewinner
I acknowledge I am not listing easy or trivial changes (and from the outside I can't speak at all to the viability of the suggestions), no doubt there are better solutions than what I have listed. Thanks for the conversation @Prizewinner !Gigantes
An article from HN today, a prime example of what we should be worried about. github.com/ChALkeR/notes/blob/master/…Gigantes
Firebase has implemented revokeRefreshTokens() inside the admin sdk. Although this will not let you kill an invalid JWT, it does allow you to prevent a refresh of the token (from my testing so far at least) and it allows cleaner control flow inside firebase database. firebase.google.com/support/release-notes/admin/node#5.7.0Gigantes
G
5

Firebase recently implemented revokeRefreshTokens() inside the admin sdk. Although this will not let you kill an invalid JWT, it does allow you to prevent a refresh of the token (from my testing so far at least) and it allows cleaner control flow inside firebase database.

See Admin Manage Sessions For rough examples

Gigantes answered 7/2, 2018 at 6:24 Comment(1)
Thanks for the update, I believe the refresh token expiration rules have also been updated: I don't remember seeing "major account changes" leading to existing refresh token expiring (at least not in the documentation).Cynde
F
0

Wanted to add a concrete example to this question. This is how I am handling it with SolidJS using a context provider/ their way of handling global state:

My routes.tsx looks like this:

import { lazy } from 'solid-js';
import { Route, Router } from '@solidjs/router';
import { getCurrentUser } from '~/apis/current-user.api';
import AuthContextProvider from '~/contexts/Auth.context';


import HomeRoutes from '~/pages/auth/routes';

const LandingPage = lazy(() => import('./pages/landing/index'));
const user = await getCurrentUser();

export const Routes = () => {
  return (<AuthContextProvider
    user={user}>
      <Router>
        <Route path='/'>
          <Route path='/' component={LandingPage} />,
          <HomeRoutes/>,
          <Route path='/*' component={LandingPage} />,
        </Route>,
      </Router>
  </AuthContextProvider>)
};

I'm getting the current user and passing it to my AuthContextProvider, because it's at the top level of all of the routes every component will have access to it and will be able to call const { user } = useAuthContext(); to get the user information.

The ~/apis/current-user.api looks something like this:

import { API_URL, COOKIES, getHeadersWithAuth } from "~/constants/api";
import { emptyUser, User } from "~/models/user.model";
import { firebaseAuth } from '~/utils/firebase';

const TOKEN_EXPIRATION_TIME = 55 * 60 * 1000; // 55 minutes in milliseconds

export const refreshUserToken = async () => {
  const userToken = localStorage.getItem("userToken");
  const tokenCreationTime = parseInt(localStorage.getItem("tokenCreationTime"), 10);
  const currentTime = Date.now();

  if (userToken && tokenCreationTime && currentTime - tokenCreationTime < TOKEN_EXPIRATION_TIME) {
    sessionStorage.setItem(COOKIES.ACCESS_TOKEN, userToken);
    return true;
  }

  try {
    // Force a token refresh
    const user = firebaseAuth.currentUser;
    if (!user) {
      return false;
    }
    const userToken = await user.getIdToken(true);
    sessionStorage.setItem(COOKIES.ACCESS_TOKEN, userToken);

    // Update the local storage with new token and creation time
    localStorage.setItem("userToken", userToken);
    localStorage.setItem("tokenCreationTime", Date.now().toString());
    return true;

  } catch (error) {
    return false;
  }
};

export const getCurrentUser = async () => {
  const result = await refreshUserToken();
  if (!result) {
    return emptyUser();
  }

  if (!sessionStorage.getItem(COOKIES.ACCESS_TOKEN)) {
    return emptyUser();
  }

  const endPoint = `${API_URL}/auth/current-user`;
  const res = await fetch(endPoint, {
    method: 'GET',
    headers: getHeadersWithAuth(),
    // credentials: 'include',
  }).catch(err => {
    console.error(`Error: getCurrentUser - ${endPoint}`, err);
    return err;
  });

  if (res.status !== 200) {
    sessionStorage.removeItem(COOKIES.ACCESS_TOKEN);
    // Remove tokens from local storage
    localStorage.removeItem("userToken");
    localStorage.removeItem("refreshToken");
    localStorage.removeItem("tokenCreationTime");
    return emptyUser();
  }

  type data = {
    user: User;
  }

  const { user: _user }: data = await res.json();
  const user = _user ? new User(_user) : emptyUser();
  return user;
}

This way whenever the client refreshes or goes to a new page, the getCurrentUser function is called and will check to see if the token has expired or not. If it hasn't it will use the found in the session token. If not it will create a new one.

I'm using local storage so that the user stays logged in even when they close the window and come back to it. I did that using setPersistence with the browserLocalPersistence option.

When the user signs out, I am deleting the data from local storage and calling revokeRefreshTokens which causes the refreshToken to no longer be valid.

You can for sure go a step further to make things even more secure if you want, but this should be a good jump off point.

Felicitous answered 29/7 at 2:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.