fastapi-users icon indicating copy to clipboard operation
fastapi-users copied to clipboard

Wrong Method for email verification

Open Bongomannn opened this issue 1 year ago • 5 comments

Describe the bug

I am going to send a mail to my registered users to verify the mail adress. But when they click on it they got "Method not allowed".

It would be nice to allow a get method for verification instead of post (or both)? Please correct me if iam wrong.

Thats the code insite library:

    @router.post(
        "/verify",
        response_model=user_schema,
        name="verify:verify",
        responses={
            status.HTTP_400_BAD_REQUEST: {
                "model": ErrorModel,
                "content": {
                    "application/json": {
                        "examples": {
                            ErrorCode.VERIFY_USER_BAD_TOKEN: {
                                "summary": "Bad token, not existing user or"
                                "not the e-mail currently set for the user.",
                                "value": {"detail": ErrorCode.VERIFY_USER_BAD_TOKEN},
                            },
                            ErrorCode.VERIFY_USER_ALREADY_VERIFIED: {
                                "summary": "The user is already verified.",
                                "value": {
                                    "detail": ErrorCode.VERIFY_USER_ALREADY_VERIFIED
                                },
                            },
                        }
                    }
                },
            }
        },
    )
    async def verify(
        request: Request,
        token: str = Body(..., embed=True),
        user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager),
    ):
        try:
            user = await user_manager.verify(token, request)
            return schemas.model_validate(user_schema, user)
        except (exceptions.InvalidVerifyToken, exceptions.UserNotExists):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=ErrorCode.VERIFY_USER_BAD_TOKEN,
            )
        except exceptions.UserAlreadyVerified:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=ErrorCode.VERIFY_USER_ALREADY_VERIFIED,
            )

    return router

To Reproduce

async def on_after_register(self, user: UserTable, request: Optional[Request] = None) -> None:
      logging.info(f"UserTable {user.id} has registered.")
      token_type: Any = "activation"
      token: str = create_token(data={"user_id": str(user.id)}, user_id=str(user.id), token_type=token_type).token  # TokenTable generieren basierend auf Benutzer-ID
      verify_link = f"http://localhost:8000/auth/request-verify-token?email={user.email}"
      await send_mail(subject="Account Registrierung",
                      body=f"Welcome",
                      `receiver=[user.email],`
                      )
      try:
          await self.request_verify(user, request)

      except UserAlreadyVerified:
          pass

  async def on_after_request_verify(self, user: UserTable, token: str, request: Optional[Request] = None):
      print(f"Verification requested for user . Verification token: {token}")
      # Send verification email
      # link = "{}/auth/verify?token={}".format(settings.FRONTEND_HOST, token)
      verify_link = f"http://localhost:8000/auth/verify?token={token}"
      # if you click on that link the data goes into a wrong method with Post and not Get
      await send_mail(subject="Account Registrierung",
                      body=f"klicken Sie auf den Link für die Bestätigung ihres Accounts:\n{verify_link}",
                      receiver=[user.email],
                      )

Bongomannn avatar Jun 27 '24 06:06 Bongomannn

I think the reason for using POST is to let the hyperlink in your email direct users to your front-end page. After clicking, the front-end will retrieve the token from the URL parameters and then call the POST verify API. During the verification process, the front-end can display a screen for the user, such as "Verifying...".

Or, if the token is found to be expired after verification, the front-end can show some message, animation, or other operations.

If you want to use GET to do it, it is also possible. Create a GET verify endpoint where the POST verify body is replaced with query parameters. Then, according to the original code, it should work.

I've seen many issues related to verify, and in the end, they are not problems with fastapi-users.

I do not think this is a bug, it should be a discussion.

hgalytoby avatar Jun 27 '24 07:06 hgalytoby

What is the usual workflow for getting the email verification working? @hgalytoby do you have any pointers to look for? I am having the same issue as @Bongomannn .

tejokrishna avatar Jun 27 '24 17:06 tejokrishna

There are many ways to verify an email address. I will provide two examples; please feel free to develop your own methods according to your needs.

version 1

image

Version 2 requires setting up a "verify API" with the GET method. Based on the original code, you likely just need to change the parameters from the request body to query parameters.

version 2

image

English is not my native language.

I may not fully understand what you need. Please correct me if I'm wrong.

I will do my best to resolve this issue.

hgalytoby avatar Jun 28 '24 05:06 hgalytoby

@hgalytoby You are right is not really a bug you can switch it to something else than a bug. I dont know how.

I want to add a subtext to this post. For a newbie webdevoloper it was not so easy to get it. If the usermanagement flow is all standard in a website you can add this flows in detailed manner to your documentation.

Documentation or an example should show:

  1. What triggers what which methods are there to overwrite
  2. What is the proper usual standard way for doing email verification. + the other flows

My flow: 0. You have to register

  1. you get a link with a token send at the end of on_after_register within await self.request_verify(user, request) which is not automaticly triggered by someone else inside the jwt gets created
  2. you can click on link in mail and the verification site opens
  3. this site sends a post to /auth/verify message with token to the backend and shows the user you get verified
  4. in the backend verify method sets is_verifyed to True in db additional i had to overwrite: def parse_id(self, value: Any) -> models.ID: return uuid.UUID(value)

Here is my developed HTML with post method.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Verification Page</title>
</head>
<body>
    <h1>Verification Page</h1>
    <p id="status"></p>

    <script>
        // Function to extract token from URL query parameters
        function getTokenFromUrl() {
            const urlParams = new URLSearchParams(window.location.search);
            return urlParams.get('token');
        }

        // Function to send token to backend for verification
        async function verifyToken(token) {
            try {
                const response = await fetch('http://localhost:8000/auth/verify', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ token: token })
                });

                if (!response.ok) {
                    throw new Error('Failed to verify token');
                }

                const data = await response.json();
                document.getElementById('status').innerText = 'Token verified successfully';
            } catch (error) {
                console.error('Error:', error);
                document.getElementById('status').innerText = 'Failed to verify token';
            }
        }

        // Call function to start the verification process
        async function startVerification() {
            const token = getTokenFromUrl();
            if (token) {
                await verifyToken(token);
            } else {
                document.getElementById('status').innerText = 'Token not found in URL';
            }
        }

        // Call function to start the verification process when the page loads
        startVerification();
    </script>
</body>
</html>

this must be also inside your app :


    @app.get('/verification')
    def verification():
        """Verification page"""
        return FileResponse('frontend/static_files/verification.html')

Bongomannn avatar Jun 28 '24 05:06 Bongomannn

  1. in the backend verify method sets is_verifyed to True in db additional i had to overwrite: def parse_id(self, value: Any) -> models.ID: return uuid.UUID(value)

https://fastapi-users.github.io/fastapi-users/latest/configuration/user-manager/#the-id-parser-mixin

uuid https://github.com/fastapi-users/fastapi-users/blob/master/fastapi_users/manager.py#L685

integer https://github.com/fastapi-users/fastapi-users/blob/master/fastapi_users/manager.py#L695

hgalytoby avatar Jun 28 '24 06:06 hgalytoby