wasp
wasp copied to clipboard
Adds Google as an Authentication method
Description
This change adds Google as a new AuthN method.
Demo
Here is an example if you want to play around with it: https://wasp-csrf-demo.netlify.app/ (Note: I'm using the old Cookies project but it is using this branch and JWTs). I added everyone's email to the Google project, which you have to do if unpublished, but let me know if you get some error about access when signing in with Google.
Syntax
The syntax for auth
is updated, to look like this when fully specified:
auth: {
userEntity: User,
methods: {
emailAndPassword: {},
google: {
configFn: import { config } from "@ext/auth/google.js",
onSignInFn: import { signInHandler } from "@ext/auth/google.js"
}
},
onAuthFailedRedirectTo: "/login",
onAuthSucceededRedirectTo: "/profile"
},
When using just the defaults, the methods
can simply be:
methods: {
emailAndPassword: {},
google: {}
},
OAuth Flow
The OAuth flow used here is as follows:
- User navigates to: https://wasp-csrf-demo.netlify.app/login
- When clicking the Google login button, a
GET
request to the API server happens: https://wasp-csrf-demo.herokuapp.com/auth/external/google/login - The API server then constructs a redirect URL, and redirects the browser to: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?response_type=code&redirect_uri=https%3A%2F%2Fwasp-csrf-demo.netlify.app%2Fauth%2Fredirect%2Fgoogle&scope=email%20profile&client_id=213102658416-49aoc8onkqj44e8o3cbnqtbmvel4plml.apps.googleusercontent.com&flowName=GeneralOAuthFlow
- After login, Google will redirect to the frontend with a code, like so: https://wasp-csrf-demo.netlify.app/auth/redirect/google?code=4%2F0AdQt8qjMwXABj01-i3MOwy9o8OkxWg4z6dxwcxaNPf6WfgEsmb1v4XNmU_EskVypkI7jHQ&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none
5a) The React page on the frontend takes this URL, and issues an AJAX
GET
request to the API server with the same query string: https://wasp-csrf-demo.herokuapp.com/auth/external/google/validateCode?code=4%2F0AdQt8qjMwXABj01-i3MOwy9o8OkxWg4z6dxwcxaNPf6WfgEsmb1v4XNmU_EskVypkI7jHQ&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&prompt=none 5b) The API server takes the code, and Passport talks directly to Google in a server-to-server communication. After that succeeds, it returns the JWT for the authenticated user back to the frontend.
Notes
A couple of things to note:
- There was a bug in a downstream dependency (
node-oauth
) of our Passport Google library, but it wasn't being updated. Therefore, I had to introduce: https://www.npmjs.com/package/patch-package - Google auth shares the same User model as Email and Password.
- If someone logs in with Google when they do not have an account, one is created with a random password.
- Once we add reset password functionality, they can then use either.
- If someone signs up first with Email and Password, and then logs in with Google, we just look them up. Therefore, they can use either.
- If someone logs in with Google when they do not have an account, one is created with a random password.
- There are default functions for both configuration and setup, if the user does not supply them: https://github.com/wasp-lang/wasp/blob/shayne-jwt-authn-google/waspc/data/Generator/templates/server/src/routes/auth/passport/google/googleDefaults.js
- So in a minimal configuration, they just need to add
google: {}
to theauth.methods
and add 2 environment variables 🪄
- So in a minimal configuration, they just need to add
- This auth method should not require
emailAndPassword
. - This auth method gives user a button they can easily use to build their login page.
- I added a notion of
googleImports.js
sogoogle.js
andgoogleDefaults.js
could be pure JS. - I tried to use a generic term, External Auth, vs Passport, when doing generic things to not tie it to the implementation. When operating on Passport things directly, I just said Passport.
Closes #667
Type of change
⚠️ WARNING: This is a breaking change, since we now add a new env var (WASP_WEB_CLIENT_URL
) for CORS protection, as well as knowing where to redirect from on the server. ⚠️
Please select the option(s) that is more relevant.
- [ ] Code cleanup
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] This change requires a documentation update
Thanks so much for all the great questions, comments, and suggestions @Martinsos! I appreciate it. I replied to them all, and will start working on them shortly.
Per your two other notes:
- Good point on still allowing the first user who signed up for the email to log in. I think we could either:
- change the password on every Google login, as suggested, or
- we could add a field to the User entity that tracks what login method can be used. it would start as
usernameAndPassword
but the minute they used any other auth method, it would change and they could only use that moving forward. however, that does introduce a bit of complexity and requires us to either change the model for them or ask them to update it themselves. additionally, most of this goes away once we do email validation and password reset in the future.So for now, I think the best way is just to update the password on login by Google.
I just realized now we don't have password reset or email verification steps -> what about that, when should we implement that? Hmmmm. Tricky thing is that smth to send emails is needed, some kind of emailer. I am not sure if any server can just send them? Ok, this is beyond scope of this PR.
Since this is already getting pretty big and complex, I agree we should punt on that. I think this will give users some real value/buzz, and we can later add those incrementally over time.
Thanks again!
soudns good let's just reset the password on google login!
Heads up @Martinsos I had to push one more commit to this PR (https://github.com/wasp-lang/wasp/pull/669/commits/d853bb323748033d2d1051eb0dff98d24478fb3f)
I was doing from-scratch testing and realized uuid
was implicitly imported via pg-boss
(and available in example app from testing), but when enabling auth from a new project it wasn't there of course. So I am now explicitly importing on server.
I also noticed we used an old version on the client, and could not find a use of it, so I removed it. Does that sound right to you as well?
If all this sounds good lemme know and I can merge. Thanks!
Heads up @Martinsos I had to push one more commit to this PR (d853bb3)
I was doing from-scratch testing and realized
uuid
was implicitly imported viapg-boss
(and available in example app from testing), but when enabling auth from a new project it wasn't there of course. So I am now explicitly importing on server.I also noticed we used an old version on the client, and could not find a use of it, so I removed it. Does that sound right to you as well?
If all this sounds good lemme know and I can merge. Thanks!
Oh wow that was a really old version, who would expect uuid to progress so much :D! Yup sounds all good, let's merge!
Converting to draft until I can make changes after UsernameAndPassword
is merged into main
. In the meantime, I am making those updates on a new branch: https://github.com/wasp-lang/wasp/tree/shayne-username-jwt-authn-google Once they are ready, I will merge them into this branch and request a final review. Thanks 👍🏻
Ok @Martinsos and @sodic, I think this is ready for a final review. The main update here is we no longer intertwine Google authn with any email
field (it's now username
, anyway :D) and instead we use an externalAuthAssociationEntity
to link any 3rd party auth with an internal user. It will generate a random username as part of the first login process, and I made one of those cutesy Heroku-like functions to do it (say hello to "first-green-crab-88221" lol).
Please let me know if you have any questions about this update, and thanks for sticking with me through basically three reviews :) (cookies, JWT with EmailAndPassword
, and now JWT with usernameAndPassword
). In the end, I think we have the right approach. 🙏🏻
Thanks so much for the review and comments @Martinsos and @sodic They were helpful. We are super close. I got most of the tiny ones included already (and resolved some as I went). The last major thing is to rename externalAuthAssociationEntity
to externalAuthEntity
. I will do that in a bit, and try to finalize any other lingering suggestions. Should be finalized by tomorrow and then I'll merge. Thanks!
@shayneczyzewski great, all good for me, feel free to resolve all of my comments any way you see fit.