React-google-login: Access Token expires after an hour

Created on 26 Jun 2018  Â·  27Comments  Â·  Source: anthonyjgrove/react-google-login

I assumed libraries like this take care of this sort of thing automagically. Is there a built-in way to handle this?

Most helpful comment

According to README, The response in onSuccess handler is also a GoogleUser Object, which provides the methods as calling gapi.

Here is my code to refreshing token automatically.

const onLoginSuccess = res => {
  if (res) {
    // Sometime `res.accessToken` is undefined
    // saveUserToken(res.getAuthResponse(true).access_token);  <-- save token

    refreshTokenSetup(res);
  }
};

/**
 * The setup for refreshing token automatically
 *
 * @param res GoogleLoginResponse
 */
const refreshTokenSetup = res => {
  // Timing to renew access token
  let refreshTiming = (res.tokenObj.expires_in || 3600  - 5 * 60) * 1000;

  const refreshToken = async () => {
    const newAuthRes = await res.reloadAuthResponse();
    refreshTiming = (newAuthRes.expires_in || 3600  - 5 * 60) * 1000;

    // saveUserToken(newAuthRes.access_token);  <-- save new token

    // Setup the other timer after the first one
    setTimeout(refreshToken, refreshTiming);
  };

  // Setup first refresh timer
  setTimeout(refreshToken, refreshTiming);
};

All 27 comments

I assumed setting the accessType prop to "offline" would take care of this, but it didn't seem to work. According to this comment that's what needs to be done https://stackoverflow.com/questions/10827920/not-receiving-google-oauth-refresh-token#comment56214632_10857806

I guess I have to use the responseType of "code" according to the docs which state that the accessType "Can be either 'online' or 'offline'. Use offline with responseType 'code' to retrieve a refresh token", but I'll have wait an hour to see if it actually works -__-

Having accessType set to "offline" and responseType set to "code" did not work. How do we get the access token refreshed automatically?

I encountered the same issue and honestly I'm not going to say I've got an elegant solution, but I do have a workable solution. I've edited it down to something understandable below. I can guarantee that the following won't work without some edits and typo fixes.

render() {
  ....
  return (
    <div>
      <MyComponentThatNeedsAnAuthToken
             tokenGetter={this.tokenGetter}
             googleUser={this.state.googleUser}
      />
      <GoogleLogin
              ...
              onSuccess={this.loginSuccess}
         />
    </div>)
}

loginSuccess = (googleUser) => {
    this.setState({
      googleUser: googleUser,
    });
    this.accessToken = googleUser.tokenObj.access_token
    this.setRefreshTimeout(googleUser.tokenObj.expires_at);
}

// Keeping the token out of "state" means that the automatic refresh of an
// access token will not cause the entire application's component to re-render.
tokenGetter = () => {
    return this.accessToken;
}

setRefreshTimeout = (expiresAt) => {
    // Either 5 minutes before the deadline, or 5 minutes from now. This
    // should prevent thrashing while keeping the credentials fresh.
    const oneMin = 60 * 1000;
    var refreshDeadline =  Math.max(
      5*oneMin,
      expiresAt - Date.now() - (5*oneMin));
    console.log("Refreshing credentials in "
                + Math.floor(refreshDeadline/oneMin).toString()
                + " minutes");
    setTimeout(this.reloadAuthToken, refreshDeadline);
  }

reloadAuthToken = () => {
    this.state.googleUser.reloadAuthResponse().then(
      // success handler.
      (authResponse) => {
        // The GoogleUser is mutated in-place, this callback updates component state.
        this.accessToken = authResponse.access_token;
        this.setRefreshTimeout(authResponse.expires_at);
      },
      // fail handler.
      (failResponse) => {
         this.accessToken = "";
         console.log("Could not refresh token");
         console.log(failResponse);
      }
    );
  }

Thanks for posting your code. That looks like a good solution. It will be tricky to implement for me though since I have a login app that redirects to my main app.

Since I redirect to another app and don't have that reloadAuthToken available to me anymore, I just have them login again if their token expires. It can be quite painless if you have few options

const storeToken = (response, props) => {
  // store accessToken

  // retry action
  functionInvolveingGoogleAPI(props)
} 

const relogin = (props) => {
  const { gapi, user } = props
  gapi.auth2.init({
    client_id: 'myclientid'
  })
  const auth2 = gapi.auth2.getAuthInstance()
  auth2.signIn({
    promp: 'none'
  , login_hint: user.email
  , scope: "all of the scopes that you need"
  })
  .then(res => storeToken(res, props), err => console.error(err))
}

const functionInvolvingGoogleAPI = async function (props) {
  const response = await callGoogleAPI()
  if (response.error && response.error.status === 'UNAUTHENTICATED') {
    relogin(props)
  } else {
    // do stuff with response
  }
}

I haven't tested this much so it may need some tweeking

Thanks for the posted code..but the issue is that it does not work. I have to send a code to my backend for verification but the code generated is not of offline use even when i set accessType "offline" and responseType "code"

I was mistaken. The code was 100% as expected. I found this under step 6 of https://developers.google.com/identity/sign-in/web/server-side-flow

The code is your one-time code that your server can exchange for its own access token and refresh token. You can only obtain a refresh token after the user has been presented an authorization dialog requesting offline access. _You must store the refresh token that you retrieve for later use because subsequent exchanges will return null for the refresh token._ This flow provides increased security over your standard OAuth 2.0 flow.

In order to get the refresh token for subsequent calls you need to set the prompt prop to consent like so:

<GoogleLogin
        ...
        responseType="code"
        prompt="consent"
        ...
    />

does this mean we need to change the button props after the first signin?

it is a bit confusing because the refresh can only be rerrieved if the value in the form is changed so it looks like we have to connect twice.

So, after tinkering around how to get the reloadAuthResponse() method after redirecting/refreshing to another page. You can still access this method by calling the window object like: window.gapi.auth2.getAuthInstance().j8.currentUser.Ab.reloadAuthResponse()
This should return you a new access_token and use that token to make requests to google API

@galoncyryll where does window.gapi come from? i tried to look into that object but it's undefined.

Thanks @eap your solution works for me and I don't know why they don't mention it here

@chitgoks include this in your index.html

    <script src="https://apis.google.com/js/client.js"></script>

for window.gapi

@chitgoks
I have something to this effect

      window.gapi.load('auth2', () => {
        window.gapi.auth2.getAuthInstance().then((auth2) =>  {
              const user = auth2.currentUser.get();
              if (!user) {
                return;
              }
              user.reloadAuthResponse().then(onGoogleLoginSuccess);
            }

i got it to work. i placed mine in an interceptor and only reload the access token if its say, 55 minutes near expiration.

then i call init first so auth wont be null. rather than loading the auth object everytime.

According to README, The response in onSuccess handler is also a GoogleUser Object, which provides the methods as calling gapi.

Here is my code to refreshing token automatically.

const onLoginSuccess = res => {
  if (res) {
    // Sometime `res.accessToken` is undefined
    // saveUserToken(res.getAuthResponse(true).access_token);  <-- save token

    refreshTokenSetup(res);
  }
};

/**
 * The setup for refreshing token automatically
 *
 * @param res GoogleLoginResponse
 */
const refreshTokenSetup = res => {
  // Timing to renew access token
  let refreshTiming = (res.tokenObj.expires_in || 3600  - 5 * 60) * 1000;

  const refreshToken = async () => {
    const newAuthRes = await res.reloadAuthResponse();
    refreshTiming = (newAuthRes.expires_in || 3600  - 5 * 60) * 1000;

    // saveUserToken(newAuthRes.access_token);  <-- save new token

    // Setup the other timer after the first one
    setTimeout(refreshToken, refreshTiming);
  };

  // Setup first refresh timer
  setTimeout(refreshToken, refreshTiming);
};
let refreshTiming = (res.tokenObj.expires_in || 3600  - 5 * 60) * 1000;

I was banging my head from last 2 days for a way to get refresh token inorder to refresh my access_token once it is expired. This was all that I needed. Thanks a ton!
No need of refresh token to refresh access token with this method.✨

According to README, The response in onSuccess handler is also a GoogleUser Object, which provides the methods as calling gapi.

Here is my code to refreshing token automatically.

const onLoginSuccess = res => {
  if (res) {
    // Sometime `res.accessToken` is undefined
    // saveUserToken(res.getAuthResponse(true).access_token);  <-- save token

    refreshTokenSetup(res);
  }
};

/**
 * The setup for refreshing token automatically
 *
 * @param res GoogleLoginResponse
 */
const refreshTokenSetup = res => {
  // Timing to renew access token
  let refreshTiming = (res.tokenObj.expires_in || 3600  - 5 * 60) * 1000;

  const refreshToken = async () => {
    const newAuthRes = await res.reloadAuthResponse();
    refreshTiming = (newAuthRes.expires_in || 3600  - 5 * 60) * 1000;

    // saveUserToken(newAuthRes.access_token);  <-- save new token

    // Setup the other timer after the first one
    setTimeout(refreshToken, refreshTiming);
  };

  // Setup first refresh timer
  setTimeout(refreshToken, refreshTiming);
};

Does this solution work when the page is refreshed? The refreshTokenSetup seems to be called on loginSuccess only. Is refreshing token based on expires_in enough for handling page refresh or expires_at needs to be considered?

Can confirm that this solution does not work when the page is refreshed. It helps a lot thought in the case user doesn't refresh the page

@smarajitdasgupta @maksimf The only thing I do to make it work when refresh the page is trying to trigger the login logic first.

  1. I have a <Dashboard> component, which is something as following. Any other valid routes will go to /dashboard first.
// Dashboard.js
const Dashboard = (isLoggedIn) => { // `isLoggedIn` with default value `false` is the state in Redux 
  // ...code
  return isLoggedIn ? Layout : <Redirect to="/login" />;
}
  1. Set isSignedIn to true in Google Login, which leads users to login automatically if their access token are still valid. And it also call the callback onLoginSuccess when the auto logic is finished.
// Login.js
const onLoginSuccess = res => {
  if (res) {
    // the action to change `isLoggedIn` state.
    userLogin(); // <--- this line is the only difference

    // Sometime `res.accessToken` is undefined
    // saveUserToken(res.getAuthResponse(true).access_token);  <-- save token

    refreshTokenSetup(res);
  }
};

From what I can see in the code above, refresh token logic won't be triggered unless isLoggedIn is false. Correct me if I'm wrong here. So, we could end up with the situation when user has refreshed the page, we did not refresh the token (isLoggedIn is true) and the token just expires in, say 10 minutes.

Is this a possibility or am I not getting it?

@maksimf The default value of isLoggedIn is false. I have updated the comment above.

So is there any way to refresh token even after page refresh?

According to README, The response in onSuccess handler is also a GoogleUser Object, which provides the methods as calling gapi.

Here is my code to refreshing token automatically.

const onLoginSuccess = res => {
  if (res) {
    // Sometime `res.accessToken` is undefined
    // saveUserToken(res.getAuthResponse(true).access_token);  <-- save token

    refreshTokenSetup(res);
  }
};

/**
 * The setup for refreshing token automatically
 *
 * @param res GoogleLoginResponse
 */
const refreshTokenSetup = res => {
  // Timing to renew access token
  let refreshTiming = (res.tokenObj.expires_in || 3600  - 5 * 60) * 1000;

  const refreshToken = async () => {
    const newAuthRes = await res.reloadAuthResponse();
    refreshTiming = (newAuthRes.expires_in || 3600  - 5 * 60) * 1000;

    // saveUserToken(newAuthRes.access_token);  <-- save new token

    // Setup the other timer after the first one
    setTimeout(refreshToken, refreshTiming);
  };

  // Setup first refresh timer
  setTimeout(refreshToken, refreshTiming);
};

isSignedIn={true} automatically refreshes the token even after page refresh.Using `isSignedIn={true} with above code I was able to auto refresh and save the token asisSignedIn={true}calls theonSuccess` callback on reload.
In my private route I am using this function

function _isLoggedIn() {
    let user = _getUserDetails(); //fetch data from localStorage
    if (user && (user.accessToken || user.access_token)&&((_getUserDetails().tokenObj.expires_at-new Date().getTime())/60000>5)) return true;
    return false;
  }

Since isSignedIn={true} the google login prompt automatically closes and the token is refreshed and then stored in localStorage

On my machine setTimeout doesn't reliably fire after coming back from a suspend

Is this a reliable solution? Does it work after coming back from a suspend?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

eap picture eap  Â·  7Comments

rishipr picture rishipr  Â·  7Comments

pulkit-max picture pulkit-max  Â·  8Comments

MitulGedeeya picture MitulGedeeya  Â·  3Comments

emwee picture emwee  Â·  4Comments