5

I'm making a purely client-side web app that displays some information from the user's Google calendar. They go through OAuth, give permission for calendar access, and then see the derived information. Following the Google Calendar JS Quickstart I have this basically working.

The problem is, the access token is only good for 1hr (3599s). After that, I need to send the user through the whole OAuth flow again to get a new token, which is four clicks across three screens. Is there any way to keep from having to ask for new consent each time?

Jeff Kaufman
  • 575
  • 3
  • 13

3 Answers3

2

If you could fetch a refresh token, then you could regularly use that to create new access tokens, and it should work up to six months. However, refresh tokens, because of that additional power, require client authentication, the client (that is, your app) has to provide a client ID and a client secret, and the authorization server can check more carefully and revoke a client if it's been compromised.

In this case, though, you can't have a client (as in, OAuth client) secret in your client (as in, Web client) code; you shared the web page and the source of web pages is easily visible. (And implicitly there is more concern that an in-browser token can be stolen by an attacker, and so there's an interest in reducing the power that such a token has by itself.) So Google's library doesn't even provide the capability to request refresh tokens from the browser-based Javascript library. That flow uses an "implicit grant" where the client is not authenticated and the user just follows a redirect in their user agent, which is simpler, but has some security limitations, and so Google and others typically intend this just for current session applications where the user is currently using the page.

If you spin up a small server (and use one of Google's server-side libraries), you can fetch and securely store refresh tokens and get newly updated access tokens for as long as you want, each time making an updated request from the server with your id/secret.

There are also some flows that would let a client-side application get a longer-lived refresh token, by having the user on a separate device go to a separate URL and enter a code, which I believe still lets the client (your app) be authenticated (because you're running an additional small service that handles that and can use a client secret). But I don't know that Google's OAuth2 set-up supports that.

(OAuth terminology feels confusing to me every time I read about it again, so apologies to the actual experts if I've misstated anything here.)

npdoty
  • 4,729
  • 26
  • 25
  • for this solution to work, users of the app must accept "this app can access my data even when I'm not present". That warning will, quite rightly, disaude the majority of users from using the app because of the obvious security concerns. – pinoyyid Sep 22 '22 at 03:19
0

Partial solution:

  1. Store whether the user has already consented, in localStorage or wherever

  2. Only use tokenClient.requestAccessToken({prompt: 'consent'}); for unconsented users; otherwise use tokenClient.requestAccessToken({prompt: ''});

Users still need to go through the oauth flow again, but now instead of being prompted for consent all they need to do is choose their Google account. Two clicks instead of four.

Leaving this open, though, in case there's a way to get it down to one or zero clicks.

Jeff Kaufman
  • 575
  • 3
  • 13
0

You don't need to send the user through the OAuth process again.

If you look at https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingclient , you'll see that there is an option "prompt=none".

By using this, Google will provide a new access token invisibly to the user. If you are calling the raw OAuth endpoint directly, simply call it from an iframe every 50 minutes and your user will never notice. Alternatively, the Google JS library should take care of the iframe for you. Personally I don't use the JS library because it's unstable and closed source.

pinoyyid
  • 21,499
  • 14
  • 64
  • 115