nicholaschiang / hammock

Move your newsletters outside of your inbox. Focus on and learn from the content you love in a distraction-free reading space.
https://readhammock.com
GNU Affero General Public License v3.0
11 stars 1 forks source link

Fix 60min authentication timeout bug #8

Closed nicholaschiang closed 3 years ago

nicholaschiang commented 3 years ago

Currently, @kunalmodi had been using the credential.accessToken returned by the Firebase Authentication SDK like so:

  const provider = new firebase.auth.GoogleAuthProvider();
  provider.addScope('https://www.googleapis.com/auth/gmail.modify');
  provider.addScope('https://www.googleapis.com/auth/gmail.settings.basic');
  provider.addScope('https://www.googleapis.com/auth/gmail.labels');
  const cred = await firebase.auth().signInWithPopup(provider);
  const token = (cred.credential as any)?.accessToken; // This expires after 60mins.

However, that accessToken expires after 60mins and the Firebase Authentication SDK does not give you access to the Google OAuth2 refresh tokens so there's no way to refresh access to Google's API after 60mins:

Note that these OAuth access tokens are not used to authenticate to Firebase and, for those providers with expiring access tokens, Firebase does not refresh them on your behalf.

Instead, I should work directly with Google's OAuth2 libraries (or REST API endpoints) and then use the returned googleIdToken to login to the Firebase SDK client-side (which I'll keep using for server-side authentication):

function onGoogleSignIn(googleUser) {
  var googleIdToken = googleUser.getAuthResponse().id_token;
  firebase.auth().signInWithCredential(
    firebase.auth.GoogleAuthProvider.credential(googleIdToken));
}

Some links to documentation (it seems the low-level OAuth2 documentation is spread everywhere haha):

Note that the gapi libraries cannot be bundled with the rest of the app which is really annoying IMO. I want to have complete control over tree-shaking, code-splitting, and just how the browser loads my code in general. Thus, I'll probably opt to use a Node.js server-side implementation or just call Google's OAuth2 REST API endpoints directly.

nicholaschiang commented 3 years ago

Reopening this because the id_token (returned by Google's OAuth2 API) that is stored as a cookie client-side and used to authenticate users server-side seems to expire after a couple of hours (I'm guessing that it also expires after 60 mins):

Your JWT is invalid: Token used too late, 1620237357.595 > 1620234917: 
{
  "iss":"https://accounts.google.com",
  "azp":"1852201844-hqm8c38munh63mo9gb90r02qh8953hms.apps.googleusercontent.com",
  "aud":"1852201844-hqm8c38munh63mo9gb90r02qh8953hms.apps.googleusercontent.com",
  "sub":"110270419839003533489",
  "hd":"tutorbook.org",
  "email":"nicholas@tutorbook.org",
  "email_verified":true,
  "at_hash":"j0s-vmhsNAGaQC80gRVwOA",
  "name":"Nicholas Chiang",
  "picture":"https://lh3.googleusercontent.com/a/AATXAJw5F58517n9XKGW6t3YicMuZDNN6eKE7NCDQU5h=s96-c",
  "given_name":"Nicholas",
  "family_name":"Chiang",
  "locale":"en",
  "iat":1620231017,
  "exp":1620234617
}.

That ☝️ also looks like I don't have to call the userinfo API to get the user's name, picture, and locale. Changing that might be a useful performance optimization (e.g. only fetch userinfo when it doesn't exist on the decoded id_token).

nicholaschiang commented 3 years ago

Also, we shouldn't require the user to grant permissions every time if they've already done so and thus we already have their refresh_token stored in our database. Instead, they should be able to just select their account and immediately login.

jurajpal commented 3 years ago

@nicholaschiang Can we also keep them "logged in"? So they don't have to choose their Google account every time they open the reader?

nicholaschiang commented 3 years ago

My current fix stores the OAuth2 refresh token client-side in an authorization cookie. This cookie is marked as both httpOnly (inaccessible to JavaScript) and secure (can only be sent over encrypted HTTPS connection). This setup could be a good advertising point: we don't have to store the OAuth2 token in our database and thus we can only access your email when you're using the app. To revoke permissions, simply get rid of the cookie (by clearing site data) and we can't access anything!

nicholaschiang commented 3 years ago

Essentially we have three tokens: