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

Email not send . SendGrid , JWT Autentication, API Platfrom #288

Closed
iamabhijeet2003 opened this issue Mar 25, 2024 · 4 comments
Closed

Email not send . SendGrid , JWT Autentication, API Platfrom #288

iamabhijeet2003 opened this issue Mar 25, 2024 · 4 comments
Labels
expected behavior The issue describe is expected Status: Reviewed Has been reviewed by a maintainer

Comments

@iamabhijeet2003
Copy link

I am using sendgrid+smtp, my app is made in symfony with API-Platform and i have JWT authentication.
When I fill the form, it is registered correctly but the email is not sent.

@jrushlow
Copy link
Collaborator

Howdy @iamabhijeet2003 - can you provide a little bit of information so we can help you figure this out?

  1. What do you mean by "it is registered correctly"? Does this mean that when you fill out the reset password form and click submit - a ResetPasswordRequest object is created and persisted? Or you're just not seeing any sort of error message in the UI when you submit the reset password form?

  2. After checking the log's - are there any exception's thrown before or after submitting a reset password request?

  3. Does your app successfully send emails from other components? e.g. VerifyEmailBundle, notifications, etc.

  4. Have you verified that the sendgrid+smtp configuration is setup correctly? See https://github.com/symfony/symfony/blob/7.0/src/Symfony/Component/Mailer/Bridge/Sendgrid/README.md

  5. Is the email sent in the dev environment? in a test? in production?

  6. what version of Symfony are you using?

  7. did you use bin/console make:reset-password to create the reset password functionality?

Without more information or the exact steps to reproduce the problem, it's hard to narrow down what the actual problem is. In the meantime, a couple things to check:

  • config/packages/reset_password.yaml should be similar to:
symfonycasts_reset_password:
    request_password_repository: App\Repository\ResetPasswordRequestRepository

If you do find a solution to the problem, please let us know! It could help someone else in the future. Otherwise, we look forward to hearing back from you.

@jrushlow jrushlow added Bug Bug Fix unconfirmed Status: Waiting Feedback Needs feedback from the author labels Mar 26, 2024
@iamabhijeet2003
Copy link
Author

What do you mean by "it is registered correctly"? Does this mean that when you fill out the reset password form and click submit - a ResetPasswordRequest object is created and persisted? Or you're just not seeing any sort of error message in the UI when you submit the reset password form?

  • I mean to say when I fill the email form to reset the password, the token is created correctly in the database:
    image

  • The mailer is working correctly.

  • The email is sent in the dev environment

  • I am using symfony 6

  • I used this command to create the reset password bin/console make:reset-password

But when I access to the route and fill the email:
image

and submit the request it shows an authentication error:
image

This is my security.yaml file:

security:
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        api:
            pattern: ^/api/
            stateless: true
            provider: app_user_provider
            jwt: ~
        main:
            json_login:
                check_path: /auth 
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            # lazy: true
            # provider: app_user_provider

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#the-firewall

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/reset-password$, roles: PUBLIC_ACCESS }
        - { path: ^/api$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI
        - { path: ^/auth, roles: PUBLIC_ACCESS }
        - { path: ^/api/users$, roles: PUBLIC_ACCESS }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }
        
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }

when@test:
    security:
        password_hashers:
            # By default, password hashers are resource intensive and take time. This is
            # important to generate secure password hashes. In tests however, secure hashes
            # are not important, waste resources and increase test times. The following
            # reduces the work factor to the lowest possible values.
            Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
                algorithm: auto
                cost: 4 # Lowest possible value for bcrypt
                time_cost: 3 # Lowest possible value for argon
                memory_cost: 10 # Lowest possible value for argon

I wait for your response and I am available here to provide any further information.
Thanks!!

@jrushlow
Copy link
Collaborator

Ahh ok I think I see whats going on here. It look's like your access_control's are preventing you from reaching the /reset-password/check-email route. In short, make sure the routes in the table below are publicly accessible:

Route Name Path Methods ResetPasswordController Method
app_forgot_password_request /reset-password GET/POST Controller::request()
app_check_email /reset-password/check-email GET Controller::checkEmail()
app_reset_password /reset-password/reset/{token} GET/POST Controller::reset()

In your security.yaml:

    ...
    access_control:
-        - { path: ^/reset-password$, roles: PUBLIC_ACCESS }
+        - { path: ^/reset-password.*$, roles: PUBLIC_ACCESS }
    ...

Regex Test Results: https://regex101.com/r/xtQ5L5/1

The long version:
When a user needs to reset their password, it is safe to assume that they are not authenticated. Let's walk through the process:

  1. User goes to GET -/reset-password, types in there email address in the form, then clicks the submit button

  2. The form is submitted to POST - /reset-password. Internally, the controller attempts to get the users email from persistence.

  • 2a) If a user is NOT found, the controller returns a redirect to /reset-password/check-email.
  • 2b) If a user IS found, the controller creates a ResetPasswordToken (stored in persistence as a ResetPasswordRequest object), creates and then sends an email containing a link to reset their password, stores a copy of the generated token in the session (this is needed in step 3), and then redirects the user to /reset-password/check-email
  1. Redirect from step 2a or 2b: GET /reset-password/check-email - displays a message to the user that IF we've found your email address, go check your email, you have 1 hour (or whatever timeout is configured) to click the link in your email.

Under the hood, step 3 checks the session for the token that was stored in step 2b. We need this token to show (in reset-password/check-email.html.twig) how long the user has to "click the link" in their email. To protect from "phishing" attacks, if a token does NOT exist in the session, we generate a fake token and display the check email page. This basically keeps someone from going to /reset-password and entering random email addresses to determine if a account exists or not. Think rainbow table attacks, etc...

  1. The user clicks the link in the email -> GET /reset-password/reset/a-long-token-1234. The controller grabs the token from the URL, stores it in the session, and returns a redirect to GET /reset-password/reset. (This prevents leaking the token to 3rd party javascript.)

  2. Redirected to GET /reset-password/reset. we grab the token from the session, validate the token (compare it with the ResetPasswordRequest object in persistence, then return reset-password/reset.html.twig.

  3. The user fills out the form and clicks submit, the form is submitted to POST /reset-password/reset, if the form is submission is "valid" we:

  • remove the ResetPasswordRequest object from persistence
  • hash the new password, set it on the User object then persist/flush
  • Clear the session of any tokens
  • redirect the user to app_main or whatever route you have defined in the controller.

As you can see this entire process was done without the user being authenticated. But, we attempt to prevent malicious behavior by using fake tokens and the like where necessary. Anywho, I hope this helps! If you're still having problems, please let us know...

Side note: I'm working on a PR for MakerBundle right now that will generate a functional test for entire process. In the meantime, I created a gist that has a rough draft of the actual test logic here -> https://gist.github.com/jrushlow/193798c452a6e135c883273277e05621

@jrushlow jrushlow added expected behavior The issue describe is expected Status: Reviewed Has been reviewed by a maintainer and removed Bug Bug Fix unconfirmed Status: Waiting Feedback Needs feedback from the author labels Mar 26, 2024
@iamabhijeet2003
Copy link
Author

Changing this , worked for me 👍 :

    ...
    access_control:
-        - { path: ^/reset-password$, roles: PUBLIC_ACCESS }
+        - { path: ^/reset-password.*$, roles: PUBLIC_ACCESS }
    ...

Thanks so much🙏 !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
expected behavior The issue describe is expected Status: Reviewed Has been reviewed by a maintainer
Projects
None yet
Development

No branches or pull requests

2 participants