mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2025-11-01 01:48:46 +08:00
Implement password validation mechanism (#632)
* Implement password validation mechanism * Add invalid password reason * Always pass user in password validator * Add password validation documentation
This commit is contained in:
@ -4,3 +4,4 @@ __version__ = "6.0.0"
|
||||
|
||||
from fastapi_users import models # noqa: F401
|
||||
from fastapi_users.fastapi_users import FastAPIUsers # noqa: F401
|
||||
from fastapi_users.user import InvalidPasswordException # noqa: F401
|
||||
|
||||
@ -15,6 +15,7 @@ from fastapi_users.router import (
|
||||
from fastapi_users.user import (
|
||||
CreateUserProtocol,
|
||||
GetUserProtocol,
|
||||
ValidatePasswordProtocol,
|
||||
VerifyUserProtocol,
|
||||
get_create_user,
|
||||
get_get_user,
|
||||
@ -39,6 +40,8 @@ class FastAPIUsers:
|
||||
:param user_create_model: Pydantic model for creating a user.
|
||||
:param user_update_model: Pydantic model for updating a user.
|
||||
:param user_db_model: Pydantic model of a DB representation of a user.
|
||||
:param validate_password: Optional function to validate the password
|
||||
at user registration, user update or password reset.
|
||||
|
||||
:attribute create_user: Helper function to create a user programmatically.
|
||||
:attribute current_user: Dependency callable getter to inject authenticated user
|
||||
@ -56,6 +59,7 @@ class FastAPIUsers:
|
||||
create_user: CreateUserProtocol
|
||||
verify_user: VerifyUserProtocol
|
||||
get_user: GetUserProtocol
|
||||
validate_password: Optional[ValidatePasswordProtocol]
|
||||
_user_model: Type[models.BaseUser]
|
||||
_user_create_model: Type[models.BaseUserCreate]
|
||||
_user_update_model: Type[models.BaseUserUpdate]
|
||||
@ -69,6 +73,7 @@ class FastAPIUsers:
|
||||
user_create_model: Type[models.BaseUserCreate],
|
||||
user_update_model: Type[models.BaseUserUpdate],
|
||||
user_db_model: Type[models.BaseUserDB],
|
||||
validate_password: Optional[ValidatePasswordProtocol] = None,
|
||||
):
|
||||
self.db = db
|
||||
self.authenticator = Authenticator(auth_backends, db)
|
||||
@ -83,6 +88,8 @@ class FastAPIUsers:
|
||||
self.verify_user = get_verify_user(db)
|
||||
self.get_user = get_get_user(db)
|
||||
|
||||
self.validate_password = validate_password
|
||||
|
||||
self.current_user = self.authenticator.current_user
|
||||
self.get_current_user = self.authenticator.get_current_user
|
||||
self.get_current_active_user = self.authenticator.get_current_active_user
|
||||
@ -120,6 +127,7 @@ class FastAPIUsers:
|
||||
self._user_model,
|
||||
self._user_create_model,
|
||||
after_register,
|
||||
self.validate_password,
|
||||
)
|
||||
|
||||
def get_verify_router(
|
||||
@ -176,6 +184,7 @@ class FastAPIUsers:
|
||||
reset_password_token_lifetime_seconds,
|
||||
after_forgot_password,
|
||||
after_reset_password,
|
||||
self.validate_password,
|
||||
)
|
||||
|
||||
def get_auth_router(
|
||||
@ -185,6 +194,8 @@ class FastAPIUsers:
|
||||
Return an auth router for a given authentication backend.
|
||||
|
||||
:param backend: The authentication backend instance.
|
||||
:param requires_verification: Whether the authentication
|
||||
require the user to be verified or not.
|
||||
"""
|
||||
return get_auth_router(
|
||||
backend,
|
||||
@ -232,6 +243,8 @@ class FastAPIUsers:
|
||||
|
||||
:param after_update: Optional function called
|
||||
after a successful user update.
|
||||
:param requires_verification: Whether the endpoints
|
||||
require the users to be verified or not.
|
||||
"""
|
||||
return get_users_router(
|
||||
self.db,
|
||||
@ -241,4 +254,5 @@ class FastAPIUsers:
|
||||
self.authenticator,
|
||||
after_update,
|
||||
requires_verification,
|
||||
self.validate_password,
|
||||
)
|
||||
|
||||
@ -3,14 +3,17 @@ from typing import Callable
|
||||
|
||||
|
||||
class ErrorCode:
|
||||
REGISTER_INVALID_PASSWORD = "REGISTER_INVALID_PASSWORD"
|
||||
REGISTER_USER_ALREADY_EXISTS = "REGISTER_USER_ALREADY_EXISTS"
|
||||
LOGIN_BAD_CREDENTIALS = "LOGIN_BAD_CREDENTIALS"
|
||||
LOGIN_USER_NOT_VERIFIED = "LOGIN_USER_NOT_VERIFIED"
|
||||
RESET_PASSWORD_BAD_TOKEN = "RESET_PASSWORD_BAD_TOKEN"
|
||||
RESET_PASSWORD_INVALID_PASSWORD = "RESET_PASSWORD_INVALID_PASSWORD"
|
||||
VERIFY_USER_BAD_TOKEN = "VERIFY_USER_BAD_TOKEN"
|
||||
VERIFY_USER_ALREADY_VERIFIED = "VERIFY_USER_ALREADY_VERIFIED"
|
||||
VERIFY_USER_TOKEN_EXPIRED = "VERIFY_USER_TOKEN_EXPIRED"
|
||||
UPDATE_USER_EMAIL_ALREADY_EXISTS = "UPDATE_USER_EMAIL_ALREADY_EXISTS"
|
||||
UPDATE_USER_INVALID_PASSWORD = "UPDATE_USER_INVALID_PASSWORD"
|
||||
|
||||
|
||||
async def run_handler(handler: Callable, *args, **kwargs):
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
from typing import Callable, Optional, Type
|
||||
from typing import Callable, Optional, Type, cast
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
from fastapi_users.user import CreateUserProtocol, UserAlreadyExists
|
||||
from fastapi_users.user import (
|
||||
CreateUserProtocol,
|
||||
InvalidPasswordException,
|
||||
UserAlreadyExists,
|
||||
ValidatePasswordProtocol,
|
||||
)
|
||||
|
||||
|
||||
def get_register_router(
|
||||
@ -12,6 +17,7 @@ def get_register_router(
|
||||
user_model: Type[models.BaseUser],
|
||||
user_create_model: Type[models.BaseUserCreate],
|
||||
after_register: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
validate_password: Optional[ValidatePasswordProtocol] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the register route."""
|
||||
router = APIRouter()
|
||||
@ -20,6 +26,19 @@ def get_register_router(
|
||||
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def register(request: Request, user: user_create_model): # type: ignore
|
||||
user = cast(models.BaseUserCreate, user) # Prevent mypy complain
|
||||
if validate_password:
|
||||
try:
|
||||
await validate_password(user.password, user)
|
||||
except InvalidPasswordException as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"code": ErrorCode.REGISTER_INVALID_PASSWORD,
|
||||
"reason": e.reason,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
created_user = await create_user(user, safe=True)
|
||||
except UserAlreadyExists:
|
||||
|
||||
@ -8,6 +8,7 @@ from fastapi_users import models
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
from fastapi_users.user import InvalidPasswordException, ValidatePasswordProtocol
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
|
||||
RESET_PASSWORD_TOKEN_AUDIENCE = "fastapi-users:reset"
|
||||
@ -19,6 +20,7 @@ def get_reset_password_router(
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
after_forgot_password: Optional[Callable[[models.UD, str, Request], None]] = None,
|
||||
after_reset_password: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
validate_password: Optional[ValidatePasswordProtocol] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the reset password routes."""
|
||||
router = APIRouter()
|
||||
@ -74,6 +76,18 @@ def get_reset_password_router(
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
if validate_password:
|
||||
try:
|
||||
await validate_password(password, user)
|
||||
except InvalidPasswordException as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD,
|
||||
"reason": e.reason,
|
||||
},
|
||||
)
|
||||
|
||||
user.hashed_password = get_password_hash(password)
|
||||
await user_db.update(user)
|
||||
if after_reset_password:
|
||||
|
||||
@ -8,6 +8,7 @@ from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
from fastapi_users.user import InvalidPasswordException, ValidatePasswordProtocol
|
||||
|
||||
|
||||
def get_users_router(
|
||||
@ -18,6 +19,7 @@ def get_users_router(
|
||||
authenticator: Authenticator,
|
||||
after_update: Optional[Callable[[models.UD, Dict[str, Any], Request], None]] = None,
|
||||
requires_verification: bool = False,
|
||||
validate_password: Optional[ValidatePasswordProtocol] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the authentication routes."""
|
||||
router = APIRouter()
|
||||
@ -54,7 +56,10 @@ def get_users_router(
|
||||
):
|
||||
for field in update_dict:
|
||||
if field == "password":
|
||||
hashed_password = get_password_hash(update_dict[field])
|
||||
password = update_dict[field]
|
||||
if validate_password:
|
||||
await validate_password(password, user)
|
||||
hashed_password = get_password_hash(password)
|
||||
user.hashed_password = hashed_password
|
||||
else:
|
||||
setattr(user, field, update_dict[field])
|
||||
@ -84,9 +89,17 @@ def get_users_router(
|
||||
updated_user,
|
||||
) # Prevent mypy complain
|
||||
updated_user_data = updated_user.create_update_dict()
|
||||
updated_user = await _update_user(user, updated_user_data, request)
|
||||
|
||||
return updated_user
|
||||
try:
|
||||
return await _update_user(user, updated_user_data, request)
|
||||
except InvalidPasswordException as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
||||
"reason": e.reason,
|
||||
},
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/{id:uuid}",
|
||||
@ -110,7 +123,17 @@ def get_users_router(
|
||||
) # Prevent mypy complain
|
||||
user = await _get_or_404(id)
|
||||
updated_user_data = updated_user.create_update_dict_superuser()
|
||||
return await _update_user(user, updated_user_data, request)
|
||||
|
||||
try:
|
||||
return await _update_user(user, updated_user_data, request)
|
||||
except InvalidPasswordException as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"code": ErrorCode.UPDATE_USER_INVALID_PASSWORD,
|
||||
"reason": e.reason,
|
||||
},
|
||||
)
|
||||
|
||||
@router.delete(
|
||||
"/{id:uuid}",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from typing import Awaitable, Type
|
||||
from typing import Any, Awaitable, Union, Type
|
||||
|
||||
try:
|
||||
from typing import Protocol
|
||||
@ -12,18 +12,34 @@ from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import get_password_hash
|
||||
|
||||
|
||||
class UserAlreadyExists(Exception):
|
||||
class FastAPIUsersException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserNotExists(Exception):
|
||||
class UserAlreadyExists(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class UserAlreadyVerified(Exception):
|
||||
class UserNotExists(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class UserAlreadyVerified(FastAPIUsersException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidPasswordException(FastAPIUsersException):
|
||||
def __init__(self, reason: Any) -> None:
|
||||
self.reason = reason
|
||||
|
||||
|
||||
class ValidatePasswordProtocol(Protocol): # pragma: no cover
|
||||
def __call__(
|
||||
self, password: str, user: Union[models.BaseUserCreate, models.BaseUserDB]
|
||||
) -> Awaitable[None]:
|
||||
pass
|
||||
|
||||
|
||||
class CreateUserProtocol(Protocol): # pragma: no cover
|
||||
def __call__(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user