react-google-login icon indicating copy to clipboard operation
react-google-login copied to clipboard

Access Token expires after an hour

Open cotterjd opened this issue 6 years ago • 27 comments

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

cotterjd avatar Jun 26 '18 19:06 cotterjd

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

cotterjd avatar Jun 26 '18 21:06 cotterjd

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 -__-

cotterjd avatar Jun 26 '18 21:06 cotterjd

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

cotterjd avatar Jun 26 '18 22:06 cotterjd

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);
      }
    );
  }

eap avatar Jul 10 '18 17:07 eap

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.

cotterjd avatar Aug 08 '18 15:08 cotterjd

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

cotterjd avatar Aug 11 '18 23:08 cotterjd

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"

angelxmoreno avatar Dec 13 '18 06:12 angelxmoreno

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"
        ...
    />

angelxmoreno avatar Dec 13 '18 07:12 angelxmoreno

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

bionicles avatar Nov 25 '19 17:11 bionicles

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.

chitgoks avatar Nov 25 '19 22:11 chitgoks

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

cycycy-g avatar Jan 15 '20 16:01 cycycy-g

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

chitgoks avatar Feb 14 '20 06:02 chitgoks

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

Assdi avatar Apr 02 '20 14:04 Assdi

@chitgoks include this in your index.html

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

for window.gapi

Utsav2 avatar Apr 05 '20 08:04 Utsav2

@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);
            }

Utsav2 avatar Apr 06 '20 06:04 Utsav2

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.

chitgoks avatar Apr 06 '20 08:04 chitgoks

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);
};

NevenLiang avatar May 31 '20 17:05 NevenLiang

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.✨

abyss-kp avatar Jun 02 '20 16:06 abyss-kp

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?

smarajitdasgupta avatar Jul 02 '20 09:07 smarajitdasgupta

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

maksimf avatar Jul 04 '20 00:07 maksimf

@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);
  }
};

NevenLiang avatar Jul 06 '20 08:07 NevenLiang

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 avatar Jul 06 '20 10:07 maksimf

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

NevenLiang avatar Jul 06 '20 12:07 NevenLiang

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

abyss-kp avatar Jul 21 '20 17:07 abyss-kp

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 as isSignedIn={true} calls the onSuccess 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

abyss-kp avatar Jul 22 '20 02:07 abyss-kp

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

maxmil avatar Sep 27 '20 08:09 maxmil

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

agusterodin avatar May 30 '21 23:05 agusterodin