Skip to content

Make password reset tokens single-use - #687

Open
varmar05 wants to merge 1 commit into
developfrom
fix_token_lifetime
Open

varmar05 wants to merge 1 commit into
developfrom
fix_token_lifetime

Conversation

@varmar05

Copy link
Copy Markdown
Collaborator

Bind each token to a random nonce stored on the user. Requesting a reset rotates the nonce, revoking older links; any password change clears it, so a link works once and dies on a logged-in password change.

There is also a new config variable PASSWORD_RESET_TOKEN_EXPIRATION.

Bind each token to a random nonce stored on the user. Requesting a reset
rotates the nonce, revoking older links; any password change clears it,
so a link works once and dies on a logged-in password change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@varmar05
varmar05 requested a review from MarcelGeo September 29, 2026 11:27
@varmar05
varmar05 changed the base branch from master to develop September 29, 2026 11:34
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 36561611579

Warning

No base build found for commit 129040b on develop.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 92.694%

Details

  • Patch coverage: 86 of 86 lines across 6 files are fully covered (100%).

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 11223
Covered Lines: 10403
Line Coverage: 92.69%
Coverage Strength: 0.93 hits per line

💛 - Coveralls

@MarcelGeo MarcelGeo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

As I understand old tokens will be revoked, no?

Comment thread server/mergin/auth/app.py
confirm_url = f"{url}/{token}"


def _send_token_email(app, user, confirm_url, template, header, **kwargs):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we have such function, I am expecting here this token value as argument. Not hidden in some tricky string.

)
abort(404)
# token was already used, superseded by a newer one or revoked by a password change
if user.password_reset_nonce != payload["nonce"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can be payload["nonce"] None? If yes, reject everything too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

that can be only for old tokens, but any new request should have some


form = UserPasswordForm.from_json(request.json)
if form.validate():
# also clears the reset nonce, making the token single-use

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we do not need such comment here.

"BEARER_TOKEN_EXPIRATION", default=3600 * 12, cast=int
) # in seconds
PASSWORD_RESET_TOKEN_EXPIRATION = config(
"PASSWORD_RESET_TOKEN_EXPIRATION", default=600, cast=int

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this is short window.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I can make it 1800 or 3600 if you like -> but if you hit reset button, then I assume you want to do it straightaway

Comment thread server/mergin/auth/app.py
send_email_async.delay(**email_data)


def generate_password_reset_token(app: Flask, user: User) -> str:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's not better to add it to user model? Just wondering ;)

Comment thread deployment/community/.env.template
Comment thread server/mergin/auth/app.py
return serializer.dumps(payload, salt=app.config["SECURITY_PASSWORD_SALT"])


def confirm_password_reset_token(token: str) -> Optional[dict]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's not enough tom just use confirm_token function with some is instance check?

This branch has not been deployed

No deployments
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.

3 participants