mirror of
https://github.com/lnbits/lnbits.git
synced 2025-12-18 16:42:26 +01:00
feat: add password reset for usermanager (#2688)
* feat: add password reset for usermanager - add a reset_key to account table - add ?reset_key= GET arguments to index.html and show reset form if provided - superuser can generate and copy reset url with key to share future ideas: - could add send forgot password email if user fill out email address * feat: simplify reset key * test: use reset key * test: add more tests * test: reset passwords do not match * test: `reset_password_auth_threshold_expired` --------- Co-authored-by: Vlad Stan <stan.v.vlad@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import shortuuid
|
||||
from httpx import AsyncClient
|
||||
|
||||
from lnbits.core.models import AccessTokenPayload, User
|
||||
from lnbits.core.views.user_api import api_users_reset_password
|
||||
from lnbits.settings import AuthMethods, settings
|
||||
from lnbits.utils.nostr import hex_to_npub, sign_event
|
||||
|
||||
@@ -469,8 +470,8 @@ async def test_alan_change_password_auth_threshold_expired(
|
||||
assert response.status_code == 403, "Treshold expired."
|
||||
assert (
|
||||
response.json().get("detail") == "You can only update your credentials"
|
||||
" in the first 1 seconds after login."
|
||||
" Please login again!"
|
||||
" in the first 1 seconds."
|
||||
" Please login again or ask a new reset key!"
|
||||
)
|
||||
|
||||
|
||||
@@ -856,3 +857,167 @@ async def test_alan_change_pubkey_auth_threshold_expired(
|
||||
" in the first 1 seconds after login."
|
||||
" Please login again!"
|
||||
)
|
||||
|
||||
|
||||
################################ RESET PASSWORD ################################
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_reset_key_ok(http_client: AsyncClient):
|
||||
tiny_id = shortuuid.uuid()[:8]
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"username": f"u21.{tiny_id}",
|
||||
"password": "secret1234",
|
||||
"password_repeat": "secret1234",
|
||||
"email": f"u21.{tiny_id}@lnbits.com",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, "User created."
|
||||
access_token = response.json().get("access_token")
|
||||
assert access_token is not None
|
||||
|
||||
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
|
||||
access_token_payload = AccessTokenPayload(**payload)
|
||||
assert access_token_payload.usr, "User id set."
|
||||
|
||||
reset_key = await api_users_reset_password(access_token_payload.usr)
|
||||
assert reset_key, "Reset key created."
|
||||
assert reset_key[:10] == "reset_key_", "This is not a reset key."
|
||||
|
||||
response = await http_client.put(
|
||||
"/api/v1/auth/reset",
|
||||
json={
|
||||
"reset_key": reset_key,
|
||||
"password": "secret0000",
|
||||
"password_repeat": "secret0000",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, "Password reset."
|
||||
access_token = response.json().get("access_token")
|
||||
assert access_token is not None
|
||||
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth", json={"username": f"u21.{tiny_id}", "password": "secret1234"}
|
||||
)
|
||||
assert response.status_code == 401, "Old passord not valid."
|
||||
assert response.json().get("detail") == "Invalid credentials."
|
||||
|
||||
response = await http_client.post(
|
||||
"/api/v1/auth", json={"username": f"u21.{tiny_id}", "password": "secret0000"}
|
||||
)
|
||||
assert response.status_code == 200, "Login new password OK."
|
||||
access_token = response.json().get("access_token")
|
||||
assert access_token is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_reset_key_user_not_found(http_client: AsyncClient):
|
||||
user_id = "926abb2ab59a48ebb2485bcceb58d05e"
|
||||
reset_key = await api_users_reset_password(user_id)
|
||||
assert reset_key, "Reset key created."
|
||||
assert reset_key[:10] == "reset_key_", "This is not a reset key."
|
||||
|
||||
response = await http_client.put(
|
||||
"/api/v1/auth/reset",
|
||||
json={
|
||||
"reset_key": reset_key,
|
||||
"password": "secret0000",
|
||||
"password_repeat": "secret0000",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 403, "User does not exist."
|
||||
assert response.json().get("detail") == "User not found."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_username_password_not_allowed(http_client: AsyncClient):
|
||||
# exclude 'username_password'
|
||||
settings.auth_allowed_methods = [AuthMethods.user_id_only.value]
|
||||
|
||||
user_id = "926abb2ab59a48ebb2485bcceb58d05e"
|
||||
reset_key = await api_users_reset_password(user_id)
|
||||
assert reset_key, "Reset key created."
|
||||
|
||||
response = await http_client.put(
|
||||
"/api/v1/auth/reset",
|
||||
json={
|
||||
"reset_key": reset_key,
|
||||
"password": "secret0000",
|
||||
"password_repeat": "secret0000",
|
||||
},
|
||||
)
|
||||
settings.auth_allowed_methods = AuthMethods.all()
|
||||
|
||||
assert response.status_code == 401, "Login method not allowed."
|
||||
assert (
|
||||
response.json().get("detail") == "Auth by 'Username and Password' not allowed."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_username_passwords_do_not_matcj(
|
||||
http_client: AsyncClient, user_alan: User
|
||||
):
|
||||
|
||||
reset_key = await api_users_reset_password(user_alan.id)
|
||||
assert reset_key, "Reset key created."
|
||||
|
||||
response = await http_client.put(
|
||||
"/api/v1/auth/reset",
|
||||
json={
|
||||
"reset_key": reset_key,
|
||||
"password": "secret0000",
|
||||
"password_repeat": "secret-does-not-mathc",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 403, "Passwords do not match."
|
||||
assert response.json().get("detail") == "Passwords do not match."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_username_password_bad_key(http_client: AsyncClient):
|
||||
|
||||
response = await http_client.put(
|
||||
"/api/v1/auth/reset",
|
||||
json={
|
||||
"reset_key": "reset_key_xxxxxxxxxxx",
|
||||
"password": "secret0000",
|
||||
"password_repeat": "secret0000",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 500, "Bad reset key."
|
||||
assert response.json().get("detail") == "Cannot reset user password."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_password_auth_threshold_expired(
|
||||
user_alan: User, http_client: AsyncClient
|
||||
):
|
||||
|
||||
reset_key = await api_users_reset_password(user_alan.id)
|
||||
assert reset_key, "Reset key created."
|
||||
|
||||
initial_update_threshold = settings.auth_credetials_update_threshold
|
||||
settings.auth_credetials_update_threshold = 1
|
||||
time.sleep(1.1)
|
||||
response = await http_client.put(
|
||||
"/api/v1/auth/reset",
|
||||
json={
|
||||
"reset_key": reset_key,
|
||||
"password": "secret0000",
|
||||
"password_repeat": "secret0000",
|
||||
},
|
||||
)
|
||||
|
||||
settings.auth_credetials_update_threshold = initial_update_threshold
|
||||
|
||||
assert response.status_code == 403, "Treshold expired."
|
||||
assert (
|
||||
response.json().get("detail") == "You can only update your credentials"
|
||||
" in the first 1 seconds."
|
||||
" Please login again or ask a new reset key!"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user