Skip to content
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

Expire password reset tokens when used (fixes #3270). #3388

Closed

Conversation

AndrolGenhald
Copy link

Added an expires_at column for password reset requests that gets set to a few seconds in the past whenever a new password reset request is created for a user, or when it's used.
I also added a hash index on the hashed token since that's primarily what's going to be queried and a full table scan isn't great once the table gets larger.

@@ -10,11 +10,13 @@ pub struct PasswordResetRequest {
pub token_encrypted: String,
pub published: chrono::NaiveDateTime,
pub local_user_id: LocalUserId,
pub expires_at: chrono::NaiveDateTime,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see any reason to add a new column here. Would be enough to delete the row when the password reset was performed successfully.

Copy link
Collaborator

@sunaurus sunaurus Jun 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that deleting rows would enable malicious actors to bypass the daily limit I added in #3344

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What @sunaurus said, I don't see any real harm in adding the extra column. If expiration duration ever becomes configurable it might be nice to be able to tell when the actual expiration time was, vs having to figure out what the expiration duration was configured to at the time and add it to the published timestamp.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense about the daily limit. But for that a boolean column expired would be enough, I dont see any reason for a second timestamp.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could change it to bool if you like, I just figured storing when it expired might be useful information in some case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I suppose a bool would work fine, I've just always seen it implemented with a timestamp so that's what I went with 🤷 I'll change it later when I have time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add a daily job to delete these based on their published time > than a day, otherwise the password reset request table will grow pointlessly large.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just got some time to work on this again and I realized why I'm not so much a fan of a boolean column. If we go with "expired" as the name then it's confusing because you'll end up with rows that are expired when the expired column is still false. If we go with "used" we'll be marking rows as used when they weren't actually used, they're just no longer valid. Is there a way to name this that's not confusing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the column be marked as used before its been used? It should be set to true after its been used.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be marked used if a new token is generated before 24 hours. Maybe it's not too big of a deal and I'm overthinking it, I'll get it updated this weekend.

Copy link
Member

@dessalines dessalines left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with nutomic that a simple used boolean column would be good.

Another thing: Why are we pointlessly converting the token back and forth to from bytes to hex? What would be wrong about just using an autogenerated uuid column in postgres, emailing them that, and checking it?

@@ -0,0 +1,2 @@
alter table password_reset_request add column expires_at timestamp not null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a not null column without a default means that any existing password_reset_request rows will result in a broken migration. I suggest either deleting them all before running this, or adding a default.

@@ -0,0 +1,2 @@
alter table password_reset_request add column expires_at timestamp not null;
create index idx_password_reset_request_token_encrypted on password_reset_request using hash (token_encrypted);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using hash? Why not just a regular index?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing: why not just generate the UUID in postgres, instead of rust?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using hash? Why not just a regular index?

Btree index seems a waste since ordering by hashed tokens doesn't seem useful in any way, hash index is more performant for direct access.

Another thing: why not just generate the UUID in postgres, instead of rust?

A couple of reasons, one is so that it can be hashed before inserting, and another is that it's not clear if postgres's UUID4 generation is guaranteed to be cryptographically secure.

@@ -10,11 +10,13 @@ pub struct PasswordResetRequest {
pub token_encrypted: String,
pub published: chrono::NaiveDateTime,
pub local_user_id: LocalUserId,
pub expires_at: chrono::NaiveDateTime,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add a daily job to delete these based on their published time > than a day, otherwise the password reset request table will grow pointlessly large.

@@ -33,11 +33,16 @@ impl Perform for PasswordChangeAfterReset {
return Err(LemmyError::from_message("passwords_dont_match"));
}

// Expire reset token
// TODO do this in a transaction along with the user update (blocked by https://github.com/LemmyNet/lemmy/issues/1161)
PasswordResetRequest::expire(context.pool(), reset_request.id).await?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transactions should be doable now that #3420 is merged.

@Nutomic Nutomic closed this Aug 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants