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

Restore password via email #2003

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

juliojesusvizcaino
Copy link

@juliojesusvizcaino juliojesusvizcaino commented May 6, 2024

Pull Request Checklist

  • Description: Briefly describe the changes in this pull request.
  • Changelog: Ensure a changelog entry following the format of Keep a Changelog is added at the bottom of the PR description.
  • Documentation: Have you updated relevant documentation Open WebUI Docs, or other documentation sources?
  • Dependencies: Are there any new dependencies? Have you updated the dependency versions in the documentation?
  • Testing: Have you written and run sufficient tests for the changes?
  • Code Review: Have you self-reviewed your code and addressed any coding standard issues?

Description

Allow users to restore their password via email.

Changelog Entry

Added

  • Allow users to restore their password via email.

Additional Information

  • Show a link to the users "Forgot your password?". Write the email and click on "Send Reset Email". It makes a POST request to /api/v1/auths/request-password-reset
  • The endpoint returns, and a background task runs for email sending.
    • It checks if the user exists.
    • It creates a JWT identifying the user. It includes the start of the hash (sha256) of the current password, so it can't be used twice to change the user password.
    • It renders a basic HTML template with a link to the frontend/auth/reset-password?token=<the jwt>.
    • It sends the email.
  • The user receives the email and clicks on the Reset Password link.
  • They are redirected to frontend/auth/reset-password?token=<the jwt>.
  • They enter the new password and Reset Password. It makes a POST request to /api/v1/auths/reset-password.
  • In the endpoint, the token is checked and the password is changed.
  • The user is redirected to the login page.

For this to work, configure some environment variables:

ENABLE_MAIL="True"
MAIL_USERNAME="user@example.com"
MAIL_PASSWORD=""
MAIL_FROM="user@example.com"
MAIL_PORT="587"
MAIL_SERVER="smtp.gmail.com"
MAIL_FROM_NAME="Desired Name"
MAIL_STARTTLS="True"
MAIL_SSL_TLS="False"

FRONTEND_URL="http://localhost:5173"

@justinh-rahb
Copy link
Collaborator

Thanks for this PR! I think this would be useful on its own, but it may be even better to make the email sending code more generalized so that we can use it for other purposes, such as account invitations or other types of transactional mail.

@juliojesusvizcaino juliojesusvizcaino force-pushed the restore-password branch 2 times, most recently from 28bab75 to e705899 Compare May 6, 2024 16:21
@juliojesusvizcaino
Copy link
Author

@justinh-rahb I just refactored it a bit, so the mail config is accessible from the config.py file. This way, we could send emails just by using it:

    message = MessageSchema(
        subject="Password Reset Request",
        recipients=[user.email],
        body=content,
        subtype=MessageType.html,
    )

    fm = FastMail(MAIL_CONF)
    await fm.send_message(message)

We could also create some wrapper, or maybe define FastMail as a dependency to inject in fastapi:

class MailSender:
    def __init__(self, background_task: BackgroundTasks) -> None:
        if config.MAIL_CONFIG is None:
            raise Exception("No Mail Configured")

        self.fm = FastMail(config.MAIL_CONFIG)
        self.background_task = background_task

    def __call__(self, message: MessageSchema, template_name: str | None = None):
        self.background_task.add_task(self.fm.send_message, message, template_name)

# later in the app
@app.route('...')
async def do_things(mail_sender: Annotated[MailSender, Depends()]):
    message = MessageSchema(
        subject="Password Reset Request",
        recipients=[user.email],
        body=content,
        subtype=MessageType.html,
    )
    mail_sender(message)
    return {'message': 'whatever'}

I like how it is now because of its versatility, what do you think?

@justinh-rahb
Copy link
Collaborator

I like how it is now because of its versatility, what do you think?

Agreed, this is much more flexible now and will provide a useful hook we can use for other similar purposes. Great work!

@juliojesusvizcaino
Copy link
Author

Added some documentation: open-webui/docs#65

@juliojesusvizcaino
Copy link
Author

I'm unsure how to test this with Cypress, should I mock the backend calls?

@juliojesusvizcaino juliojesusvizcaino marked this pull request as ready for review May 8, 2024 09:33
@justinh-rahb
Copy link
Collaborator

I'm unsure how to test this with Cypress, should I mock the backend calls?

Cypress test coverage is far from 100% right now. I'd be comfortable merging this for the time being and waiting for a separate PR for test coverage later. And merging now will mean someone else might take that on even, saving you the work.

@juliojesusvizcaino
Copy link
Author

That sounds nice! This is the first time I create a pull request in a public project, do you know what would be the next step for this to be merged?

@justinh-rahb justinh-rahb requested a review from tjbck May 8, 2024 13:30
@juliojesusvizcaino juliojesusvizcaino force-pushed the restore-password branch 4 times, most recently from 088f8ed to 506f503 Compare May 16, 2024 08:18
@@ -9,6 +9,7 @@

from pathlib import Path
import json
from fastapi_mail import ConnectionConfig
Copy link
Contributor

Choose a reason for hiding this comment

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

Native implementation will be preferred here.

Copy link
Author

Choose a reason for hiding this comment

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

Ok, I can use aiosmtplib instead, the same used by fastapi_mail. I installed the fastapi_mail library because we would end up with a simpler but similar implementation that we would need to maintain.

We can use the smtplib module in the standard library, or install aiosmtplib to make it asynchronous. If we use the second one, we end up with an additional dependency anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use smtplib for now and we can come back to this later.

.env.example Outdated
LITELLM_LOCAL_MODEL_COST_MAP="True"

ENABLE_MAIL="True"
Copy link
Contributor

Choose a reason for hiding this comment

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

ENABLE_EMAIL

Copy link
Contributor

Choose a reason for hiding this comment

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

all occurrence for MAIL should be renamed to EMAIL

Copy link
Contributor

Choose a reason for hiding this comment

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

This should be included as a component from the same auth route.

Copy link
Author

Choose a reason for hiding this comment

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

We need a way to make the reset password component shareable, so the link in the emails opens the "new password" flow.

If we use the same component, we need a way to open that component, via a query param? .../auth?reset-password=true

class ForgetPasswordResponse(BaseModel):
message: str

@router.post("/request-password-reset", response_model=ForgetPasswordResponse)
Copy link
Contributor

Choose a reason for hiding this comment

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

/password-reset/request

class ResetPasswordResponse(BaseModel):
message: str

@router.post("/reset-password", response_model=ResetPasswordResponse)
Copy link
Contributor

@tjbck tjbck May 16, 2024

Choose a reason for hiding this comment

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

/password-reset/verify

@tjbck
Copy link
Contributor

tjbck commented May 16, 2024

The rest LGTM, Thanks for this PR!

EMAIL_SSL_TLS="False"

FRONTEND_URL="http://localhost:5173"
Copy link
Contributor

Choose a reason for hiding this comment

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

WEBUI_URL env var already exists!

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.

None yet

3 participants