-
Notifications
You must be signed in to change notification settings - Fork 86
Add Google OAuth2 Integration #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@stadust there's one issue here, maybe you'll have some idea on how to resolve this; |
stadust
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for working on this!!
I've left a bunch of random comments (didn't really go through everything in depth because I have to go to work now 😭). I don't really have an immediate idea why the access tokens have invalid signatures, I'll need to actually play around with it myself a bit I'm afraid :(
Just to clarify though, you don't mean that access tokens from before linking become invalid after linking an account, right (e.g. logging in again fixes it)?
migrations/20240520201641_remove_password_not_null_constraint.down.sql
Outdated
Show resolved
Hide resolved
|
okay Another Problem has arisen for applications that have an oauth2 login flow (im using Notion as a reference) they usually just trust the token and let it do destructive changes but im asking before we implement anything |
The current system trusts the access token instead of requiring the user to re-enter their password for everything (with few exceptions, such as changing the password), so I think keeping it like that is fine |
| } | ||
|
|
||
| let redirect_uri = "https://accounts.google.com/o/oauth2/v2/auth".to_string() | ||
| + format!("?client_id={}", std::env::var("GOOGLE_CLIENT_ID").unwrap()).as_str() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
config.rs thingy here as well :)
| input.button.red.hover #delete-account type = "button" style = "margin: 15px auto 0px;" value="Delete My Account"; | ||
| input.button.blue.hover #change-password type = "button" style = "margin: 15px auto 0px;" value="Change Password"; | ||
| @if !authenticated_user.is_google_linked() { | ||
| input.button.blue.hover #change-password type = "button" style = "margin: 15px auto 0px;" value="Change Password"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Change Password" seems to be the wrong text here. Maybe "Link with google"?
pointercrate-user-pages/src/login.rs
Outdated
| div.grow {} | ||
| input.button.blue.hover type = "submit" style = "margin: 15px auto 0px;" value = "Register"; | ||
| form.flex.col.grow #google-form novalidate = "" { | ||
| input.button.blue.hover type = "submit" style = "margin: 15px auto 0px;" value = "Log in with Google"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mhh, I was hoping we could use the buttons from the google SDK here instead of our own
pointercrate-user/src/auth/get.rs
Outdated
| // This will never conflict with an existing user | ||
| // According to Google, the account ID is always unique | ||
| // https://developers.google.com/identity/openid-connect/openid-connect#an-id-tokens-payload | ||
| let name = format!("{}#{}", user_info.name, user_info.id); | ||
|
|
||
| let id = sqlx::query!( | ||
| "INSERT INTO | ||
| members (email_address, name, display_name, google_account_id) | ||
| VALUES | ||
| (($1::text)::email, $2, $3, $4) RETURNING member_id | ||
| ", | ||
| user_info.email, | ||
| name, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe do the concatenation in the query?
| // This will never conflict with an existing user | |
| // According to Google, the account ID is always unique | |
| // https://developers.google.com/identity/openid-connect/openid-connect#an-id-tokens-payload | |
| let name = format!("{}#{}", user_info.name, user_info.id); | |
| let id = sqlx::query!( | |
| "INSERT INTO | |
| members (email_address, name, display_name, google_account_id) | |
| VALUES | |
| (($1::text)::email, $2, $3, $4) RETURNING member_id | |
| ", | |
| user_info.email, | |
| name, | |
| // This will never conflict with an existing user | |
| // According to Google, the account ID is always unique | |
| // https://developers.google.com/identity/openid-connect/openid-connect#an-id-tokens-payload | |
| let id = sqlx::query!( | |
| "INSERT INTO | |
| members (email_address, name, display_name, google_account_id) | |
| VALUES | |
| (($1::text)::email, $2 || $3, $4, $5) RETURNING member_id | |
| ", | |
| user_info.email, | |
| user_info.name, | |
| user_info.id, |
pointercrate-user/src/auth/mod.rs
Outdated
| if self.password_hash.is_none() { | ||
| return Vec::new(); | ||
| } | ||
|
|
||
| let raw_parts: Vec<_> = self.password_hash.as_ref().unwrap().split('$').filter(|s| !s.is_empty()).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's use match instead of unwrap here:
| if self.password_hash.is_none() { | |
| return Vec::new(); | |
| } | |
| let raw_parts: Vec<_> = self.password_hash.as_ref().unwrap().split('$').filter(|s| !s.is_empty()).collect(); | |
| let raw_parts = match self.password_hash { | |
| None => return Vec::new(), | |
| Some(ref hash) => hash.split('$').filter(|s| !s.is_empty()).collect(); | |
| }; |
| if legacy.is_some() { | ||
| let legacy_cookie = Cookie::build(("legacy", "true")) | ||
| .http_only(true) | ||
| .same_site(SameSite::Strict) | ||
| .path("/"); | ||
|
|
||
| cookies.add(legacy_cookie); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using a cookie, can we somehow give this information to google, and have google return it to us when it calls our callback? Maybe via query parameter?
pointercrate-user/src/auth/get.rs
Outdated
| google_account_id: Some(user_info.id), | ||
| email_address: Some(user_info.email), | ||
| }), | ||
| Err(UserError::UserNotFoundGoogleAccount { .. }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's factor this entire match arm into some more functions:
- one for updating the google account id of an existing user (can probably go into auth/patch.rs)
- one for creating a new user account from a google account (maybe into auth/post.rs?)
pointercrate-user/src/error.rs
Outdated
| #[display(fmt = "The user is already linked to a Google account")] | ||
| AlreadyLinked, | ||
|
|
||
| #[display(fmt = "This operation is not applicable to this user")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be a bit more descriptive and have this variant be explicitly about changing passwords on oauth accounts :)
pointercrate-user/src/auth/get.rs
Outdated
| } | ||
|
|
||
| let updated_user = sqlx::query!( | ||
| "UPDATE members SET google_account_id = $1, email_address = ($2::text)::email WHERE member_id = $3 RETURNING member_id", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here is the problem why this flow gives you an invalid access token: this query is missing a SET password_hash = NULL. Right now in line 126 onward, you're constructing an AuthenticatedUser with password_hash: None. This means that if you call generate_access_token() on the user returned here, it will only sign the token with the server secret. However, the database will still contain the user's old password hash. This means that when it comes to validating the token, it will retrieve an AuthenticatedUser with password_hash = hash of password from before account was linked, and thus it tries to validate the token using a secret that's a combination of server secret and password hash. But that's the wrong secret, because we signed the token with only the server secret!
| -- Add up migration script here | ||
|
|
||
| ALTER TABLE members | ||
| ADD COLUMN google_account_id VARCHAR(255) NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per discussion on discord: Let's put a UNIQUE constraint here as a failsafe for raceconditions that allow adding the same google account to multiple pointercrate accounts
pointercrate-user/src/error.rs
Outdated
| UserNotFoundName { user_name: String }, | ||
|
|
||
| #[display(fmt = "No user with google account {} found", google_account_id)] | ||
| UserNotFoundGoogleAccount { google_account_id: String }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| UserNotFoundGoogleAccount { google_account_id: String }, | |
| UserNotFoundGoogleAccount { google_account_id: u64 }, |
Originally, this was intended as a way to reset passwords. However, I could never be bothered to actually set up a mail server. Now, we will instead offer the option to link pointercrate accounts to google accounts in #135, so this half-implemented feature is truly dead. Signed-off-by: stadust <43299462+stadust@users.noreply.github.com>
1edd5f6 to
22dd991
Compare
Google OAuth2 Integration
Implements integration from issue #127.
To-do
License Acceptance
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.