Replaces the old "gAPI"/OAuth2 login flow (deprecated) with the Sign In with Google flow (probably not deprecated for at least a couple years hopefully).
See the official announcement indicating deprecation will happen on March 31st of this year.
This PR addresses login for the web instance only, NOT for Android or iOS apps. Those apps will likely fail to authenticate when Google turns off the old API.
The login buttons we used to control ourselves are now controlled by Google JS code
Authorization to use the Drive API is now separate from authenticating the user, so the authorization step now must happen after login, when a user interacts with Drive. Authorization tokens are cached and reused just like with authentication tokens.
The user metadata returned by google that was previously passed alongside the JWT token is now only present inside the token itself. This changes API server behavior as we must extract that metadata to get the user ID and lookup info in our DB (e.g. loot points).
The "old" gapi library is deprecated... for authentication. But it's not deprecated for access to the Drive API, because Google isn't a monolith and teams don't coordinate. I've removed it entirely from the app, but it still has to be loaded in the quest creator.
I didn't touch any of the cordova and app-specific logic. IMO it's better for it to stay safely inactive rather than messing with it and complicating the release.
Prior behavior
In the app:
User clicks the "Sign In" button on the SearchDisclaimer page before seeing search results.
This fires ensureLogin() action within app/src/actions/User.tsx, which calls the underlying loginWeb() from shared/auth/Web.tsx.
That calls gapi.auth2.getAuthInstance().signIn (now deprecated) to open a Google-controlled popup, receive user login credentials, and parse and return user metadata of the form {email, image, name, idToken}.
The app (back in app/src/actions/User.tsx) uses this metadata to configure google analytics and Raven logging, before calling updateState() to finally set the redux user state using the USER_LOGIN action.
In the quest creator:
User clicks the "Log In" button on the quests/src/components/Splash.tsx page, triggering a callback to onLogin in the react container.
This fires loginUser() action within quests/src/actions/User.tsx, which performs step (3) as above to get user authentication and metadata - however, it also passes two extra authorization scopes: drive.file and drive.install, needed for Google Drive file saving and manipulation.
After gapi stuff returns, registerUserAndIdToken() is called (in shared/auth/API.tsx, here) which sends a POST request to the API server.
This is received by the /auth/google' handler in api/src/lib/oauth2.ts, where the JSON body is parsed and req.body.id_token is sneakily used by Passport (and the passport-google-id-token mixin). Passport then
unpacks the JWT token
verifies it
authenticates the user to the API server
fetches saved user metadata from postgresql storage (see the db.users.findOne() invocation)
formats and returns the user metadata including last login, loot points etc. to the code in shared/auth/API.tsx.
Finally, actions are fired including SET_PROFILE_META to set the user profile/state information, and loadQuestFromURL() to load either an existing or new quest depending on user action.
At this point, Google's gapi JS library is loaded with an auth token and authorized scopes. Subsequent calls to gapi.client.request() in quests/src/actions/Quest.tsx pass this info along to Google servers when calling the Drive API.
New behavior
In the app:
The "Sign In" button is replaced by the "Sign In with Google" button, encapsulated in the LoginButton component in shared/auth/LoginButton.tsx.
Button clicks are handled by google, but the retrieved JWT token is directed to the onLogin callback of the component, which in the App is linked to onLogin() within app/src/components/views/SearchDisclaimerContainer.tsx.
This calls sendAuthTokenToAPIServer located in app/src/actions/User.tsx, which calls registerUserAndIdToken from shared/auth/Web.tsx.
This is handled roughly the same as the API handling step (4) in the quest creator, however the parsing is slightly different as the Google metdata provided for the user is now encoded inside the JWT and not passed separately.
The response is munged by registerUserAndIdToken, then returned to sendAuthTokenToAPIServer, which sets GA and Raven state as needed.
The USER_LOGIN action and card navigation action are then fired upon return to the onLogin container callback.\
In the quest creator:
(1) and (2) are handled as in the app, i.e. now Google controls them. Only difference is the onLogin callback within quests/src/components/SplashContainer.tsx is triggered, not the one from the app.
registerUserAndIdToken is called (same as app)
Same API steps as in the app
postLoginUser fires (in quests/src/actions/User.tsx) when registerUserAndIdToken returns. This triggers the SET_PROFILE_META and loadQuestFromURL() as in (6) of the original quest auth flow.
Google has separated authentication from authorization, so we still need to fetch an authorization token granting permission to use Drive despite having already logged in the user. These are short-lived and should be checked before executing API calls.
When the user performs an action that triggers a Drive API call from quests/src/actions/Quest.tsx (e.g. updateDriveFile()), the ensureToken() wrapper is called (from quests/src/actions/User.tsx).
This method checks if a valid token already exists (via. gapi.client.getToken()). If it does, it returns the cached token.
Otherwise, loadGapi() is called (from shared/auth/Web.tsx, modified to call gapi.load, gapi.client.init, and gapi.client.setAPIKey thus providing all non-user config needed to function).
When that returns, getAuthorizationToken is called from shared/auth/Web.tsx which calls Google's google.accounts.oauth2.initTokenClient (i.e. the new, non-gapi approach). This promps the user, then calls back with a JSON object representing the authorization token.
Back in ensureToken(), gapi.auth.setToken is called with the newly retrieved token.
The Drive API call proceeds as required by the user, now with authentication and authorization.
Replaces the old "gAPI"/OAuth2 login flow (deprecated) with the Sign In with Google flow (probably not deprecated for at least a couple years hopefully).
See the official announcement indicating deprecation will happen on March 31st of this year.
This PR addresses login for the web instance only, NOT for Android or iOS apps. Those apps will likely fail to authenticate when Google turns off the old API.
Internal dev log: https://docs.google.com/document/d/1kcUlTsAyu7hfTqFVWQn8zhV_Vi9_HSrNpbMu_ICWYg4/edit#
TL;DR:
gapi
library is deprecated... for authentication. But it's not deprecated for access to the Drive API, because Google isn't a monolith and teams don't coordinate. I've removed it entirely from the app, but it still has to be loaded in the quest creator.Prior behavior
In the app:
ensureLogin()
action withinapp/src/actions/User.tsx
, which calls the underlyingloginWeb()
fromshared/auth/Web.tsx
.gapi.auth2.getAuthInstance().signIn
(now deprecated) to open a Google-controlled popup, receive user login credentials, and parse and return user metadata of the form{email, image, name, idToken}
.app/src/actions/User.tsx
) uses this metadata to configure google analytics and Raven logging, before callingupdateState()
to finally set the redux user state using theUSER_LOGIN
action.In the quest creator:
quests/src/components/Splash.tsx
page, triggering a callback to onLogin in the react container.loginUser()
action withinquests/src/actions/User.tsx
, which performs step (3) as above to get user authentication and metadata - however, it also passes two extra authorization scopes:drive.file
anddrive.install
, needed for Google Drive file saving and manipulation.gapi
stuff returns,registerUserAndIdToken()
is called (inshared/auth/API.tsx
, here) which sends a POST request to the API server./auth/google'
handler inapi/src/lib/oauth2.ts
, where the JSON body is parsed andreq.body.id_token
is sneakily used by Passport (and thepassport-google-id-token
mixin). Passport thendb.users.findOne()
invocation)shared/auth/API.tsx
.SET_PROFILE_META
to set the user profile/state information, andloadQuestFromURL()
to load either an existing or new quest depending on user action.At this point, Google's
gapi
JS library is loaded with an auth token and authorized scopes. Subsequent calls togapi.client.request()
inquests/src/actions/Quest.tsx
pass this info along to Google servers when calling the Drive API.New behavior
In the app:
LoginButton
component inshared/auth/LoginButton.tsx
.onLogin
callback of the component, which in the App is linked toonLogin()
withinapp/src/components/views/SearchDisclaimerContainer.tsx
.sendAuthTokenToAPIServer
located inapp/src/actions/User.tsx
, which callsregisterUserAndIdToken
fromshared/auth/Web.tsx
.registerUserAndIdToken
, then returned tosendAuthTokenToAPIServer
, which sets GA and Raven state as needed.USER_LOGIN
action and card navigation action are then fired upon return to theonLogin
container callback.\In the quest creator:
(1) and (2) are handled as in the app, i.e. now Google controls them. Only difference is the
onLogin
callback withinquests/src/components/SplashContainer.tsx
is triggered, not the one from the app.registerUserAndIdToken
is called (same as app)postLoginUser
fires (inquests/src/actions/User.tsx
) whenregisterUserAndIdToken
returns. This triggers theSET_PROFILE_META
andloadQuestFromURL()
as in (6) of the original quest auth flow.Google has separated authentication from authorization, so we still need to fetch an authorization token granting permission to use Drive despite having already logged in the user. These are short-lived and should be checked before executing API calls.
quests/src/actions/Quest.tsx
(e.g.updateDriveFile()
), theensureToken()
wrapper is called (fromquests/src/actions/User.tsx
).gapi.client.getToken()
). If it does, it returns the cached token.loadGapi()
is called (fromshared/auth/Web.tsx
, modified to callgapi.load
,gapi.client.init
, andgapi.client.setAPIKey
thus providing all non-user config needed to function).getAuthorizationToken
is called fromshared/auth/Web.tsx
which calls Google'sgoogle.accounts.oauth2.initTokenClient
(i.e. the new, non-gapi approach). This promps the user, then calls back with a JSON object representing the authorization token.ensureToken()
,gapi.auth.setToken
is called with the newly retrieved token.