Require that all passwords are between 8 and 64 characters (#1239)
- Require that all passwords are between 8 and 64 characters - Fixes account settings password reset form to only trigger logged-in event after successful password change. - Password validation can be extended within the UserManager's validate_password method to add or modify requirements. - Add tests for password validation
This commit is contained in:
		
							parent
							
								
									b1ead614ee
								
							
						
					
					
						commit
						bbdb7f8ce5
					
				| @ -6,7 +6,7 @@ import os | ||||
| import uuid | ||||
| import asyncio | ||||
| 
 | ||||
| from typing import Optional | ||||
| from typing import Optional, Union | ||||
| 
 | ||||
| from pydantic import UUID4 | ||||
| import passlib.pwd | ||||
| @ -17,7 +17,7 @@ from fastapi.security import OAuth2PasswordBearer | ||||
| from pymongo.errors import DuplicateKeyError | ||||
| 
 | ||||
| from fastapi_users import FastAPIUsers, BaseUserManager | ||||
| from fastapi_users.manager import UserAlreadyExists | ||||
| from fastapi_users.manager import UserAlreadyExists, InvalidPasswordException | ||||
| from fastapi_users.authentication import ( | ||||
|     AuthenticationBackend, | ||||
|     BearerTransport, | ||||
| @ -96,6 +96,23 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): | ||||
|         await self.on_after_register_custom(created_user, user, request) | ||||
|         return created_user | ||||
| 
 | ||||
|     async def validate_password( | ||||
|         self, password: str, user: Union[UserCreate, UserDB] | ||||
|     ) -> None: | ||||
|         """ | ||||
|         Validate a password. | ||||
| 
 | ||||
|         Overloaded to set password requirements. | ||||
| 
 | ||||
|         :param password: The password to validate. | ||||
|         :param user: The user associated to this password. | ||||
|         :raises InvalidPasswordException: The password is invalid. | ||||
|         :return: None if the password is valid. | ||||
|         """ | ||||
|         pw_length = len(password) | ||||
|         if not 8 <= pw_length <= 64: | ||||
|             raise InvalidPasswordException(reason="invalid_password_length") | ||||
| 
 | ||||
|     async def get_user_names_by_ids(self, user_ids): | ||||
|         """return list of user names for given ids""" | ||||
|         user_ids = [UUID4(id_) for id_ in user_ids] | ||||
| @ -147,9 +164,11 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): | ||||
|             ) | ||||
|             print(f"Super user {email} created", flush=True) | ||||
|             print(res, flush=True) | ||||
| 
 | ||||
|         except (DuplicateKeyError, UserAlreadyExists): | ||||
|             print(f"User {email} already exists", flush=True) | ||||
|         # pylint: disable=raise-missing-from | ||||
|         except InvalidPasswordException: | ||||
|             raise HTTPException(status_code=422, detail="invalid_password") | ||||
| 
 | ||||
|     async def create_non_super_user( | ||||
|         self, | ||||
| @ -174,9 +193,17 @@ class UserManager(BaseUserManager[UserCreate, UserDB]): | ||||
|                 newOrg=False, | ||||
|                 is_verified=True, | ||||
|             ) | ||||
|             created_user = await super().create(user_create, safe=False, request=None) | ||||
|             await self.on_after_register_custom(created_user, user_create, request=None) | ||||
|             return created_user | ||||
|             try: | ||||
|                 created_user = await super().create( | ||||
|                     user_create, safe=False, request=None | ||||
|                 ) | ||||
|                 await self.on_after_register_custom( | ||||
|                     created_user, user_create, request=None | ||||
|                 ) | ||||
|                 return created_user | ||||
|             # pylint: disable=raise-missing-from | ||||
|             except InvalidPasswordException: | ||||
|                 raise HTTPException(status_code=422, detail="invalid_password") | ||||
| 
 | ||||
|         except (DuplicateKeyError, UserAlreadyExists): | ||||
|             print(f"User {email} already exists", flush=True) | ||||
|  | ||||
| @ -226,7 +226,7 @@ def test_send_and_accept_org_invite( | ||||
|         json={ | ||||
|             "name": "accepted", | ||||
|             "email": expected_stored_email, | ||||
|             "password": "testpw", | ||||
|             "password": "testingpassword", | ||||
|             "inviteToken": token, | ||||
|             "newOrg": False, | ||||
|         }, | ||||
|  | ||||
| @ -1,6 +1,10 @@ | ||||
| import requests | ||||
| import time | ||||
| 
 | ||||
| from .conftest import API_PREFIX, CRAWLER_USERNAME | ||||
| from .conftest import API_PREFIX, CRAWLER_USERNAME, ADMIN_PW, ADMIN_USERNAME | ||||
| 
 | ||||
| VALID_USER_EMAIL = "validpassword@example.com" | ||||
| VALID_USER_PW = "validpassw0rd!" | ||||
| 
 | ||||
| 
 | ||||
| def test_create_super_user(admin_auth_headers): | ||||
| @ -42,3 +46,139 @@ def test_me_with_orgs(crawler_auth_headers, default_org_id): | ||||
|     assert default_org["name"] | ||||
|     assert default_org["default"] | ||||
|     assert default_org["role"] == 20 | ||||
| 
 | ||||
| 
 | ||||
| def test_add_user_to_org_invalid_password(admin_auth_headers, default_org_id): | ||||
|     r = requests.post( | ||||
|         f"{API_PREFIX}/orgs/{default_org_id}/add-user", | ||||
|         json={ | ||||
|             "email": "invalidpassword@example.com", | ||||
|             "password": "pw", | ||||
|             "name": "invalid pw user", | ||||
|             "description": "test invalid password", | ||||
|             "role": 20, | ||||
|         }, | ||||
|         headers=admin_auth_headers, | ||||
|     ) | ||||
|     assert r.status_code == 422 | ||||
|     assert r.json()["detail"] == "invalid_password" | ||||
| 
 | ||||
| 
 | ||||
| def test_register_user_invalid_password(admin_auth_headers, default_org_id): | ||||
|     email = "invalidpassword@example.com" | ||||
|     # Send invite | ||||
|     r = requests.post( | ||||
|         f"{API_PREFIX}/orgs/{default_org_id}/invite", | ||||
|         headers=admin_auth_headers, | ||||
|         json={"email": email, "role": 20}, | ||||
|     ) | ||||
|     assert r.status_code == 200 | ||||
|     data = r.json() | ||||
|     assert data["invited"] == "new_user" | ||||
| 
 | ||||
|     # Look up token | ||||
|     r = requests.get( | ||||
|         f"{API_PREFIX}/orgs/{default_org_id}/invites", | ||||
|         headers=admin_auth_headers, | ||||
|     ) | ||||
|     assert r.status_code == 200 | ||||
|     data = r.json() | ||||
|     invites_matching_email = [ | ||||
|         invite for invite in data["items"] if invite["email"] == email | ||||
|     ] | ||||
|     token = invites_matching_email[0]["id"] | ||||
| 
 | ||||
|     # Create user with invite | ||||
|     r = requests.post( | ||||
|         f"{API_PREFIX}/auth/register", | ||||
|         headers=admin_auth_headers, | ||||
|         json={ | ||||
|             "name": "invalid", | ||||
|             "email": email, | ||||
|             "password": "passwd", | ||||
|             "inviteToken": token, | ||||
|             "newOrg": False, | ||||
|         }, | ||||
|     ) | ||||
|     assert r.status_code == 400 | ||||
|     detail = r.json()["detail"] | ||||
|     assert detail["code"] == "REGISTER_INVALID_PASSWORD" | ||||
|     assert detail["reason"] == "invalid_password_length" | ||||
| 
 | ||||
| 
 | ||||
| def test_register_user_valid_password(admin_auth_headers, default_org_id): | ||||
|     # Send invite | ||||
|     r = requests.post( | ||||
|         f"{API_PREFIX}/orgs/{default_org_id}/invite", | ||||
|         headers=admin_auth_headers, | ||||
|         json={"email": VALID_USER_EMAIL, "role": 20}, | ||||
|     ) | ||||
|     assert r.status_code == 200 | ||||
|     data = r.json() | ||||
|     assert data["invited"] == "new_user" | ||||
| 
 | ||||
|     # Look up token | ||||
|     r = requests.get( | ||||
|         f"{API_PREFIX}/orgs/{default_org_id}/invites", | ||||
|         headers=admin_auth_headers, | ||||
|     ) | ||||
|     assert r.status_code == 200 | ||||
|     data = r.json() | ||||
|     invites_matching_email = [ | ||||
|         invite for invite in data["items"] if invite["email"] == VALID_USER_EMAIL | ||||
|     ] | ||||
|     token = invites_matching_email[0]["id"] | ||||
| 
 | ||||
|     # Create user with invite | ||||
|     r = requests.post( | ||||
|         f"{API_PREFIX}/auth/register", | ||||
|         headers=admin_auth_headers, | ||||
|         json={ | ||||
|             "name": "valid", | ||||
|             "email": VALID_USER_EMAIL, | ||||
|             "password": VALID_USER_PW, | ||||
|             "inviteToken": token, | ||||
|             "newOrg": False, | ||||
|         }, | ||||
|     ) | ||||
|     assert r.status_code == 201 | ||||
| 
 | ||||
| 
 | ||||
| def test_reset_invalid_password(admin_auth_headers): | ||||
|     r = requests.patch( | ||||
|         f"{API_PREFIX}/users/me", | ||||
|         headers=admin_auth_headers, | ||||
|         json={"email": ADMIN_USERNAME, "password": "12345"}, | ||||
|     ) | ||||
|     assert r.status_code == 400 | ||||
|     detail = r.json()["detail"] | ||||
|     assert detail["code"] == "UPDATE_USER_INVALID_PASSWORD" | ||||
|     assert detail["reason"] == "invalid_password_length" | ||||
| 
 | ||||
| 
 | ||||
| def test_reset_valid_password(admin_auth_headers, default_org_id): | ||||
|     valid_user_headers = {} | ||||
|     while True: | ||||
|         r = requests.post( | ||||
|             f"{API_PREFIX}/auth/jwt/login", | ||||
|             data={ | ||||
|                 "username": VALID_USER_EMAIL, | ||||
|                 "password": VALID_USER_PW, | ||||
|                 "grant_type": "password", | ||||
|             }, | ||||
|         ) | ||||
|         data = r.json() | ||||
|         try: | ||||
|             valid_user_headers = {"Authorization": f"Bearer {data['access_token']}"} | ||||
|             break | ||||
|         except: | ||||
|             print("Waiting for valid user auth headers") | ||||
|             time.sleep(5) | ||||
| 
 | ||||
|     r = requests.patch( | ||||
|         f"{API_PREFIX}/users/me", | ||||
|         headers=valid_user_headers, | ||||
|         json={"email": VALID_USER_EMAIL, "password": "new!password"}, | ||||
|     ) | ||||
|     assert r.status_code == 200 | ||||
|     assert r.json()["email"] == VALID_USER_EMAIL | ||||
|  | ||||
| @ -311,6 +311,8 @@ export class AccountSettings extends LiteElement { | ||||
|             name="newPassword" | ||||
|             type="password" | ||||
|             label="${msg("New password")}" | ||||
|             help-text=${msg("Must be between 8-64 characters")} | ||||
|             minlength="8" | ||||
|             autocomplete="new-password" | ||||
|             password-toggle | ||||
|             required | ||||
| @ -351,8 +353,6 @@ export class AccountSettings extends LiteElement { | ||||
|         email: this.authState.username, | ||||
|         password: formData.get("password") as string, | ||||
|       }); | ||||
| 
 | ||||
|       this.dispatchEvent(AuthService.createLoggedInEvent(nextAuthState)); | ||||
|     } catch (e: any) { | ||||
|       console.debug(e); | ||||
|     } | ||||
| @ -371,6 +371,7 @@ export class AccountSettings extends LiteElement { | ||||
| 
 | ||||
|     const params = { | ||||
|       password: formData.get("newPassword"), | ||||
|       email: this.userInfo?.email, | ||||
|     }; | ||||
| 
 | ||||
|     try { | ||||
| @ -385,13 +386,17 @@ export class AccountSettings extends LiteElement { | ||||
|           successMessage: "Successfully updated password", | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       this.dispatchEvent(AuthService.createLoggedInEvent(nextAuthState)); | ||||
|     } catch (e) { | ||||
|       console.error(e); | ||||
| 
 | ||||
|       this.formStateService.send({ | ||||
|         type: "ERROR", | ||||
|         detail: { | ||||
|           serverError: msg("Something went wrong changing password"), | ||||
|           serverError: msg( | ||||
|             "Something went wrong changing password. Verify that new password meets requirements (8-64 characters)." | ||||
|           ), | ||||
|         }, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
| @ -79,7 +79,9 @@ export class SignUpForm extends LiteElement { | ||||
|             id="password" | ||||
|             name="password" | ||||
|             type="password" | ||||
|             label=${msg("Create a password")} | ||||
|             label="${msg("New password")}" | ||||
|             help-text=${msg("Must be between 8-64 characters")} | ||||
|             minlength="8" | ||||
|             autocomplete="new-password" | ||||
|             passwordToggle | ||||
|             required | ||||
| @ -172,9 +174,12 @@ export class SignUpForm extends LiteElement { | ||||
|         const { detail } = await resp.json(); | ||||
|         if (detail === "REGISTER_USER_ALREADY_EXISTS") { | ||||
|           shouldLogIn = true; | ||||
|         } else if (detail.code && detail.code === "REGISTER_INVALID_PASSWORD") { | ||||
|           this.serverError = msg( | ||||
|             "Invalid password. Must be between 8 and 64 characters" | ||||
|           ); | ||||
|         } else { | ||||
|           // TODO show validation details
 | ||||
|           this.serverError = msg("Invalid email address or password"); | ||||
|           this.serverError = msg("Invalid email or password"); | ||||
|         } | ||||
|         break; | ||||
|       default: | ||||
|  | ||||
| @ -38,6 +38,8 @@ export class ResetPassword extends LiteElement { | ||||
|                 name="password" | ||||
|                 type="password" | ||||
|                 label="${msg("New password")}" | ||||
|                 help-text=${msg("Must be between 8-64 characters")} | ||||
|                 minlength="8" | ||||
|                 autocomplete="new-password" | ||||
|                 passwordToggle | ||||
|                 required | ||||
| @ -95,17 +97,19 @@ export class ResetPassword extends LiteElement { | ||||
|       case 400: | ||||
|       case 422: | ||||
|         const { detail } = await resp.json(); | ||||
| 
 | ||||
|         if (detail === "RESET_PASSWORD_BAD_TOKEN") { | ||||
|           // TODO password validation details
 | ||||
|           this.serverError = msg( | ||||
|             "Password reset email is not valid. Request a new password reset email" | ||||
|           ); | ||||
|         } else { | ||||
|           // TODO password validation details
 | ||||
|           this.serverError = msg("Invalid password"); | ||||
|         } else if ( | ||||
|           detail.code && | ||||
|           detail.code === "RESET_PASSWORD_INVALID_PASSWORD" | ||||
|         ) { | ||||
|           this.serverError = msg( | ||||
|             "Invalid password. Must be between 8 and 64 characters" | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|         break; | ||||
|       default: | ||||
|         this.serverError = msg("Something unexpected went wrong"); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user