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
Add routes for user activation #403
Add routes for user activation #403
Conversation
Generate a token after creating the user in register route, passing to `activation_callback`, if `activation_callback` supplied Create new `/activate` route that will verify the token and activate the user Add new error codes to `fastapi_users/router/common.py` Update documentation Add tests Co-authored-by: Mark Todd <markpeter.todd@hotmail.co.uk>
Hi @eddsalkield! Wow, that's quite some work, thank you :) I'll review that carefully! |
Codecov Report
@@ Coverage Diff @@
## master #403 +/- ##
===========================================
- Coverage 100.00% 99.15% -0.85%
===========================================
Files 22 23 +1
Lines 801 945 +144
===========================================
+ Hits 801 937 +136
- Misses 0 8 +8
Continue to review full report at Codecov.
|
First of all, thank you for your work. It will be a very good starting point. I've some concerns on some points though, but that doesn't alter the quality of your code 🙂 Use of
|
No input here, y'all are the experts! Stoked about this though, props on the hard work |
Thanks for the swift response. We made a fair number of design decisions here, and so I'm more than happy to debate them to get it right. Account verificationI now understand your intended semantics of activation - the example of deactivating a user account made this clear. Having had a think, I agree that we should separate activation from verification in two different routers. Doing so lets us keep the current implementation of Ultimately, we need to let the end-developer answer the question: should a user have to be verified to log in? I suggest that we add a boolean argument to To align with the semantics of verification, I think the default user model should not be verified by default. Therefore, the To add verification, then end-developer then needs to:
Your thoughts would be appreciated. The verification routerThe verification router will expose two routes: one for token generation, and one for verification using the token. Separating the token generation from the Other thoughts
This was required since token generation was tied to the I think these changes shouldn't take too long to implement, so we'll have a crack at implementing them with your approval. Rewriting the docs and tests to match the new behaviour is likely going to take the longest! |
Thanks! I feel we'll make great work there 😄
Sounds great!
Or not, if he wants to allow to login even if not verified 😉
Sounds perfect to me. Let's make a quick to-do list then:
Are you okay with this? 🙂 |
Good idea with the to-do list; it'll help us track what's left in the project. However, on reflection, I think there are only two use cases we can support here:
I think that, from a security perspective, we cannot support a halfway house between these two. I’ll illustrate the problem through a short story. Suppose that the end-developer configures verification, and allows users to log in and access a minimal set of site features without verifying. The site cannot make guarantees that the currently logged in user is the legitimate owner of the email address. In this case, a squatter could theoretically come along first and register an account. Now the legitimate owner needs to be able to come along later and oust the squatter. 😉 Let’s also suppose that the legitimate email address owner is unaware of the squatter’s existence, because the verification email got lost. Since the legitimate owner doesn't know the account password, or even know that the squatter exists, they would try to create a new account under their email address. There are two possibilities for what could occur:
In essence, having accounts that are first-come first-served until somebody gets a verification attempt in, is mad. I believe that either accounts should be given in the current free-for-all style, or restricted by means of verification, but without an intermediate possibility. If an end-developer wants to support a reduced feature set for certain users, they should implement it as an additional permissions layer atop FastAPI-Users, having decided whether to support verification or not beforehand. However, Otherwise, I'm happy with the rest of the plan, and would like to hear your thoughts about how to proceed on this. |
The squatted could do a forgot-password, since they're the email owner, and get into the squatters account, then delete. Kinda a pain, but just a thought |
They'll also know immediately they got squatted by getting an email-verification from a service they've never heard of, and can investigate what's up |
I agree with @lefnire here. Given that the squatted user keeps the control on their mailbox, they can always ask for a reset password email. As I see it, the potential benefits for the attacker seem quite limited. I've tested to create an account on GitHub and Gitlab :
So, I would say that it's perfectly okay to allow login and some actions for non-verified users (typically, you at least need to allow it for verification routes). If the end-developer doesn't want to allow any action without verification, they would still be able to do so. What do you think? |
Hey folks, Just a quick update - yep, all of that is fine Frankie - Edd and I have implemented all of the changes on the checklist and are just putting together the tests before making another pull request. We should have it ready at some point in the next few days. :) Kind regards, Mark |
Just thought I'd give you all another quick update - the tests and changes are now complete, we're just doing a review before we send it in, so the next pull request should be ready in the next couple of days :) |
That's great @mark-todd! Thank you for the update! |
* Separate verification logic and token generation into `/fastapi_users/router/verify.py`, with per-route callbacks for custom behaviour * Return register router to original state * Added `is_verified` property to user models * Added `requires_verification` argument to `get_users_router`and `get_auth_router` * Additional dependencies added for verification in `fastapi_users/authentication/__init__.py` * Update tests for new behaviour * Update `README.md` to describe a workaround for possible problems during testing, by exceeding ulimit file descriptor limit Co-authored-by: Mark Todd <markpeter.todd@hotmail.co.uk>
We've implemented the changes and corresponding tests in our latest commit. I'm glad we've been able to iterate a bit to converge on this design, which I think is good overall. Feedback appreciated! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there!
First of all, I wanted to thank you for your work! This is quite a big thing!
I've made some comments and questions. When we have solved that, I'll merge that PR. Then, I'll probably revisit some things (I admit I've not read all those unit tests) before release.
Cheers guys 🎄
fastapi_users/router/verify.py
Outdated
): | ||
router = APIRouter() | ||
|
||
@router.post("/request_verify_token", status_code=status.HTTP_202_ACCEPTED) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer kebab-case for API paths (with "-")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we can make it consistent with this
fastapi_users/user.py
Outdated
) -> models.BaseUserDB: | ||
existing_user = await user_db.get_by_email(user.email) | ||
|
||
if existing_user is not None: | ||
if existing_user is not None and existing_user.is_active: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still not convinced by this. I would prefer to remove it ; and maybe talk about it in a separate issue/PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I suppose without it it just means inactive users get a nicer error message upon re-registration - I think we just added this so that if you're banned from a site you don't get the nice "UserAlreadyExists" flag, as this could be potentially useful information to malicious users. That said, I'm not sure it's that big a deal, so I'm happy to revert it :)
fastapi_users/user.py
Outdated
raise UserAlreadyExists() | ||
|
||
hashed_password = get_password_hash(user.password) | ||
user_dict = ( | ||
user.create_update_dict() if safe else user.create_update_dict_superuser() | ||
) | ||
if is_active is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary as it should be handled properly by create_update_dict
and create_update_dict_superuser
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent! Yeah I think I just accidentally implemented this twice haha - the changes are already made to the create_update_dicts
fastapi_users/user.py
Outdated
raise UserAlreadyExists() | ||
|
||
hashed_password = get_password_hash(user.password) | ||
user_dict = ( | ||
user.create_update_dict() if safe else user.create_update_dict_superuser() | ||
) | ||
if is_active is not None: | ||
user_dict["is_active"] = is_active | ||
if is_verified is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessary as it should be handled properly by create_update_dict
and create_update_dict_superuser
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent! Yeah I think I just accidentally implemented this twice haha - the changes are already made to the create_update_dicts
fastapi_users/user.py
Outdated
|
||
|
||
class VerifyUserProtocol(Protocol): | ||
def __call__(self, user_uuid: str) -> Awaitable[models.BaseUserDB]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type annotation for user_uuid
should be UUID4
(from pydantic
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok thanks!
@pytest.fixture | ||
def verified_user() -> UserDB: | ||
return UserDB( | ||
email="lake.lady@camelot.bt", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, ha, you got the spirit 😄 Nice 👍
tests/test_fastapi_users.py
Outdated
app.include_router( | ||
fastapi_users.get_verify_router( | ||
after_verification_request=verification_callback, | ||
verification_token_secret="teststring", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use "SECRET"
to be consistent with the others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep sure!
docs/configuration/model.md
Outdated
@@ -4,7 +4,7 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should revert those changes. (except grammar fixes of courses)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agreed
@@ -1,9 +1,26 @@ | |||
# Register routes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should revert those changes. (except grammar fixes of courses)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agreed
docs/usage/routes.md
Outdated
@@ -44,7 +44,7 @@ Logout the authenticated user against the method named `name`. Check the corresp | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should revert those changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agreed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Frankie
Yep this all seems fine, I think the only potential question mark now is the get_get_user function - if you're happy with my explanation though I can go through and make the required changes. :)
Cheers,
Mark
user_db: BaseUserDatabase[models.BaseUserDB], | ||
) -> GetUserProtocol: | ||
async def get_user(user_email: EmailStr) -> models.BaseUserDB: | ||
if not (user_email == EmailStr(user_email)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_user is the equivalent of create_user, only it simply fetches and returns the user without adding them to the database. It was required to abstract away from accessing the database directly in verify.py in an analogous way to your abstraction in register.py for create_user, although it would also be a useful function for future sections of fastapi-users.
fastapi_users/router/verify.py
Outdated
): | ||
router = APIRouter() | ||
|
||
@router.post("/request_verify_token", status_code=status.HTTP_202_ACCEPTED) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, we can make it consistent with this
fastapi_users/user.py
Outdated
|
||
|
||
class VerifyUserProtocol(Protocol): | ||
def __call__(self, user_uuid: str) -> Awaitable[models.BaseUserDB]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok thanks!
fastapi_users/user.py
Outdated
raise UserAlreadyExists() | ||
|
||
hashed_password = get_password_hash(user.password) | ||
user_dict = ( | ||
user.create_update_dict() if safe else user.create_update_dict_superuser() | ||
) | ||
if is_active is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent! Yeah I think I just accidentally implemented this twice haha - the changes are already made to the create_update_dicts
docs/configuration/model.md
Outdated
@@ -4,7 +4,7 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agreed
@@ -1,9 +1,26 @@ | |||
# Register routes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agreed
docs/usage/routes.md
Outdated
@@ -44,7 +44,7 @@ Logout the authenticated user against the method named `name`. Check the corresp | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, agreed
fastapi_users/user.py
Outdated
) -> models.BaseUserDB: | ||
existing_user = await user_db.get_by_email(user.email) | ||
|
||
if existing_user is not None: | ||
if existing_user is not None and existing_user.is_active: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I suppose without it it just means inactive users get a nicer error message upon re-registration - I think we just added this so that if you're banned from a site you don't get the nice "UserAlreadyExists" flag, as this could be potentially useful information to malicious users. That said, I'm not sure it's that big a deal, so I'm happy to revert it :)
tests/test_fastapi_users.py
Outdated
app.include_router( | ||
fastapi_users.get_verify_router( | ||
after_verification_request=verification_callback, | ||
verification_token_secret="teststring", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep sure!
Kebab-case on request-verify-token SECRET now used as test string Other minor changes
Add requested modifications for user verification.
Many thanks for your great work @mark-todd and @eddsalkield 👍 I've merged it in a sub-branch ; I'll take the liberty to revisit it before putting it in master! |
Nice job y'all! Epic epic |
* Add routes for user activation (#403) * Add routes for user activation Generate a token after creating the user in register route, passing to `activation_callback`, if `activation_callback` supplied Create new `/activate` route that will verify the token and activate the user Add new error codes to `fastapi_users/router/common.py` Update documentation Add tests Co-authored-by: Mark Todd <markpeter.todd@hotmail.co.uk> * Rework routes for user activation * Separate verification logic and token generation into `/fastapi_users/router/verify.py`, with per-route callbacks for custom behaviour * Return register router to original state * Added `is_verified` property to user models * Added `requires_verification` argument to `get_users_router`and `get_auth_router` * Additional dependencies added for verification in `fastapi_users/authentication/__init__.py` * Update tests for new behaviour * Update `README.md` to describe a workaround for possible problems during testing, by exceeding ulimit file descriptor limit Co-authored-by: Mark Todd <markpeter.todd@hotmail.co.uk> * Restored docs to original state. * All other modifications reqested added Kebab-case on request-verify-token SECRET now used as test string Other minor changes Co-authored-by: Mark Todd <markpeter.todd@hotmail.co.uk> * Embed token in body in verify route * Reorganize checks in verify route and add unit test * Ignore coverage on Protocol classes * Tweak verify_user function to take full user in parameter * Improve unit tests structure regarding parametrized test client * Make after_verification_request optional to be more consistent with other routers * Tweak status codes on verify routes * Write documentation for verification feature * Add not released warning on verify docs Co-authored-by: Edd Salkield <edd@salkield.uk> Co-authored-by: Mark Todd <markpeter.todd@hotmail.co.uk>
@all-contributors add @eddsalkield for code, doc |
I could not determine your intention. Basic usage: @all-contributors please add @Someone for code, doc and infra For other usages see the documentation |
@all-contributors add @mark-todd for code, doc |
I've put up a pull request to add @mark-todd! 🎉 |
@all-contributors add @eddsalkield for code, doc |
I've put up a pull request to add @eddsalkield! 🎉 |
@all-contributors add @mark-todd for code, doc |
I've put up a pull request to add @mark-todd! 🎉 |
Allow user accounts to be activated via an
activation_callback
, which is called in the/register
route to handle user verification, resolving #106.User accounts created through the
/register
route haveis_active == True
if and only ifactivation_callback
is supplied toget_register_router
.The
activation_callback
expects a token, which if supplied to the/activate
route will activate the user upon token verification.The semantics of
after_register
have been changed slightly: it's called at the point when an activated user has been created. If noactivation_callback
is supplied, it's called after the/register
route. Otherwise, it's called after the/activate
route; then any desired behaviour to be run after/register
should be put in theactivation_callback
.This PR additionally:
fastapi_users/router/common.py
Co-authored-by: Mark Todd