mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-11-04 14:45:50 +08:00
Ensure reset password token is single use
This commit is contained in:
@ -367,6 +367,7 @@ class BaseUserManager(Generic[models.UP, models.ID]):
|
|||||||
|
|
||||||
token_data = {
|
token_data = {
|
||||||
"user_id": str(user.id),
|
"user_id": str(user.id),
|
||||||
|
"password_fgpt": self.password_helper.hash(user.hashed_password),
|
||||||
"aud": self.reset_password_token_audience,
|
"aud": self.reset_password_token_audience,
|
||||||
}
|
}
|
||||||
token = generate_jwt(
|
token = generate_jwt(
|
||||||
@ -404,6 +405,7 @@ class BaseUserManager(Generic[models.UP, models.ID]):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user_id = data["user_id"]
|
user_id = data["user_id"]
|
||||||
|
password_fingerprint = data["password_fgpt"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise exceptions.InvalidResetPasswordToken()
|
raise exceptions.InvalidResetPasswordToken()
|
||||||
|
|
||||||
@ -414,6 +416,12 @@ class BaseUserManager(Generic[models.UP, models.ID]):
|
|||||||
|
|
||||||
user = await self.get(parsed_id)
|
user = await self.get(parsed_id)
|
||||||
|
|
||||||
|
valid_password_fingerprint, _ = self.password_helper.verify_and_update(
|
||||||
|
user.hashed_password, password_fingerprint
|
||||||
|
)
|
||||||
|
if not valid_password_fingerprint:
|
||||||
|
raise exceptions.InvalidResetPasswordToken()
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise exceptions.UserInactive()
|
raise exceptions.UserInactive()
|
||||||
|
|
||||||
|
|||||||
@ -46,11 +46,17 @@ def verify_token(user_manager: UserManagerMock[UserModel]):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def forgot_password_token(user_manager: UserManagerMock[UserModel]):
|
def forgot_password_token(user_manager: UserManagerMock[UserModel]):
|
||||||
def _forgot_password_token(
|
def _forgot_password_token(
|
||||||
user_id=None, lifetime=user_manager.reset_password_token_lifetime_seconds
|
user_id=None,
|
||||||
|
current_password_hash=None,
|
||||||
|
lifetime=user_manager.reset_password_token_lifetime_seconds,
|
||||||
):
|
):
|
||||||
data = {"aud": user_manager.reset_password_token_audience}
|
data = {"aud": user_manager.reset_password_token_audience}
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
data["user_id"] = str(user_id)
|
data["user_id"] = str(user_id)
|
||||||
|
if current_password_hash is not None:
|
||||||
|
data["password_fgpt"] = user_manager.password_helper.hash(
|
||||||
|
current_password_hash
|
||||||
|
)
|
||||||
return generate_jwt(data, user_manager.reset_password_token_secret, lifetime)
|
return generate_jwt(data, user_manager.reset_password_token_secret, lifetime)
|
||||||
|
|
||||||
return _forgot_password_token
|
return _forgot_password_token
|
||||||
@ -409,6 +415,11 @@ class TestForgotPassword:
|
|||||||
)
|
)
|
||||||
assert decoded_token["user_id"] == str(user.id)
|
assert decoded_token["user_id"] == str(user.id)
|
||||||
|
|
||||||
|
valid_fingerprint, _ = user_manager.password_helper.verify_and_update(
|
||||||
|
user.hashed_password, decoded_token["password_fgpt"]
|
||||||
|
)
|
||||||
|
assert valid_fingerprint is True
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.manager
|
@pytest.mark.manager
|
||||||
@ -427,7 +438,10 @@ class TestResetPassword:
|
|||||||
):
|
):
|
||||||
with pytest.raises(InvalidResetPasswordToken):
|
with pytest.raises(InvalidResetPasswordToken):
|
||||||
await user_manager.reset_password(
|
await user_manager.reset_password(
|
||||||
forgot_password_token(user.id, lifetime=-1), "guinevere"
|
forgot_password_token(
|
||||||
|
user.id, current_password_hash=user.hashed_password, lifetime=-1
|
||||||
|
),
|
||||||
|
"guinevere",
|
||||||
)
|
)
|
||||||
assert user_manager._update.called is False
|
assert user_manager._update.called is False
|
||||||
assert user_manager.on_after_reset_password.called is False
|
assert user_manager.on_after_reset_password.called is False
|
||||||
@ -441,7 +455,8 @@ class TestResetPassword:
|
|||||||
):
|
):
|
||||||
with pytest.raises(InvalidResetPasswordToken):
|
with pytest.raises(InvalidResetPasswordToken):
|
||||||
await user_manager.reset_password(
|
await user_manager.reset_password(
|
||||||
forgot_password_token(user_id), "guinevere"
|
forgot_password_token(user_id, current_password_hash="old_password"),
|
||||||
|
"guinevere",
|
||||||
)
|
)
|
||||||
assert user_manager._update.called is False
|
assert user_manager._update.called is False
|
||||||
assert user_manager.on_after_reset_password.called is False
|
assert user_manager.on_after_reset_password.called is False
|
||||||
@ -451,7 +466,24 @@ class TestResetPassword:
|
|||||||
):
|
):
|
||||||
with pytest.raises(UserNotExists):
|
with pytest.raises(UserNotExists):
|
||||||
await user_manager.reset_password(
|
await user_manager.reset_password(
|
||||||
forgot_password_token("d35d213e-f3d8-4f08-954a-7e0d1bea286f"),
|
forgot_password_token(
|
||||||
|
"d35d213e-f3d8-4f08-954a-7e0d1bea286f",
|
||||||
|
current_password_hash="old_password",
|
||||||
|
),
|
||||||
|
"guinevere",
|
||||||
|
)
|
||||||
|
assert user_manager._update.called is False
|
||||||
|
assert user_manager.on_after_reset_password.called is False
|
||||||
|
|
||||||
|
async def test_already_used_token(
|
||||||
|
self,
|
||||||
|
user: UserModel,
|
||||||
|
user_manager: UserManagerMock[UserModel],
|
||||||
|
forgot_password_token,
|
||||||
|
):
|
||||||
|
with pytest.raises(InvalidResetPasswordToken):
|
||||||
|
await user_manager.reset_password(
|
||||||
|
forgot_password_token(user.id, current_password_hash="old_password"),
|
||||||
"guinevere",
|
"guinevere",
|
||||||
)
|
)
|
||||||
assert user_manager._update.called is False
|
assert user_manager._update.called is False
|
||||||
@ -465,7 +497,10 @@ class TestResetPassword:
|
|||||||
):
|
):
|
||||||
with pytest.raises(UserInactive):
|
with pytest.raises(UserInactive):
|
||||||
await user_manager.reset_password(
|
await user_manager.reset_password(
|
||||||
forgot_password_token(inactive_user.id),
|
forgot_password_token(
|
||||||
|
inactive_user.id,
|
||||||
|
current_password_hash=inactive_user.hashed_password,
|
||||||
|
),
|
||||||
"guinevere",
|
"guinevere",
|
||||||
)
|
)
|
||||||
assert user_manager._update.called is False
|
assert user_manager._update.called is False
|
||||||
@ -479,7 +514,9 @@ class TestResetPassword:
|
|||||||
):
|
):
|
||||||
with pytest.raises(InvalidPasswordException):
|
with pytest.raises(InvalidPasswordException):
|
||||||
await user_manager.reset_password(
|
await user_manager.reset_password(
|
||||||
forgot_password_token(user.id),
|
forgot_password_token(
|
||||||
|
user.id, current_password_hash=user.hashed_password
|
||||||
|
),
|
||||||
"h",
|
"h",
|
||||||
)
|
)
|
||||||
assert user_manager.on_after_reset_password.called is False
|
assert user_manager.on_after_reset_password.called is False
|
||||||
@ -490,7 +527,10 @@ class TestResetPassword:
|
|||||||
user_manager: UserManagerMock[UserModel],
|
user_manager: UserManagerMock[UserModel],
|
||||||
forgot_password_token,
|
forgot_password_token,
|
||||||
):
|
):
|
||||||
await user_manager.reset_password(forgot_password_token(user.id), "holygrail")
|
await user_manager.reset_password(
|
||||||
|
forgot_password_token(user.id, current_password_hash=user.hashed_password),
|
||||||
|
"holygrail",
|
||||||
|
)
|
||||||
|
|
||||||
assert user_manager._update.called is True
|
assert user_manager._update.called is True
|
||||||
update_dict = user_manager._update.call_args[0][1]
|
update_dict = user_manager._update.call_args[0][1]
|
||||||
|
|||||||
Reference in New Issue
Block a user