Skip to content

Commit aceecca

Browse files
committed
feat: #2 enhance password handling with SecretStr
- Update password fields across models and schemas to use SecretStr - Introduce PasswordStr and PasswordStrOptional types for consistent validation. - Modify token handling to return SecretStr for access tokens. - Update tests to reflect changes in password handling and validation.
1 parent b977c5b commit aceecca

17 files changed

Lines changed: 125 additions & 28 deletions

File tree

app/api/v1/endpoints/token.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import timedelta
22

33
from fastapi import APIRouter
4+
from pydantic import SecretStr
45

56
from app.api.v1.schemas.token import TokenRequest, TokenResponse
67
from app.core.security import create_access_token
@@ -24,4 +25,4 @@ async def get_access_token(db: DbSession, login_request: TokenRequest) -> TokenR
2425
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
2526
)
2627

27-
return TokenResponse(token=access_token, type="Bearer")
28+
return TokenResponse(token=SecretStr(access_token), type="Bearer")

app/api/v1/schemas/token.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from typing import Literal
22

3-
from pydantic import BaseModel, EmailStr
3+
from pydantic import BaseModel, EmailStr, SecretStr
4+
5+
from app.fields import PasswordStr
46

57

68
class TokenRequest(BaseModel):
79
email: EmailStr
8-
password: str
10+
password: PasswordStr
911

1012

1113
class TokenResponse(BaseModel):
12-
token: str
14+
token: SecretStr
1315
type: Literal["Bearer"]

app/api/v1/schemas/user.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pydantic import BaseModel, ConfigDict, EmailStr
44

5+
from app.fields import PasswordStr
56
from app.operations.user.schemas import UserBase, UserCreateBase
67

78

@@ -26,4 +27,4 @@ class UsersResponse(BaseModel):
2627

2728
class UserLogin(BaseModel):
2829
email: EmailStr
29-
password: str
30+
password: PasswordStr

app/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from functools import lru_cache
22

3-
from pydantic import BaseModel
3+
from pydantic import BaseModel, SecretStr
44
from pydantic_settings import BaseSettings, SettingsConfigDict
55
from sqlalchemy.engine.url import URL
66

@@ -9,13 +9,13 @@ class AppSettings(BaseModel):
99
name: str = "pluto"
1010
debug: bool = False
1111
version: str = "0.1.0"
12-
jwt_secret: str
12+
jwt_secret: SecretStr
1313

1414

1515
class DatabaseSettings(BaseModel):
1616
name: str = "pluto"
1717
user: str
18-
password: str
18+
password: SecretStr
1919
host: str = "localhost"
2020
port: int = 5432
2121

@@ -24,7 +24,7 @@ def url(self) -> URL:
2424
return URL.create(
2525
drivername="postgresql",
2626
username=self.user,
27-
password=self.password,
27+
password=self.password.get_secret_value(),
2828
host=self.host,
2929
port=self.port,
3030
database=self.name,

app/core/security.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@ def create_access_token(
2121
expires = datetime.now(UTC) + expires_delta
2222
to_encode.update({"exp": expires})
2323

24-
return jwt.encode(payload=to_encode, key=get_settings().app.jwt_secret, algorithm=ALGORITHM)
24+
return jwt.encode(
25+
payload=to_encode,
26+
key=get_settings().app.jwt_secret.get_secret_value(),
27+
algorithm=ALGORITHM,
28+
)
2529

2630

2731
def decode_token(token: str) -> dict[str, Any]:
2832
try:
29-
return jwt.decode(token.strip(), key=get_settings().app.jwt_secret, algorithms=ALGORITHM)
33+
return jwt.decode(
34+
token.strip(),
35+
key=get_settings().app.jwt_secret.get_secret_value(),
36+
algorithms=ALGORITHM,
37+
)
3038
except jwt.InvalidTokenError as err:
3139
raise UnauthorizedError(detail="Could not validate credentials") from err
3240

app/database/seeders/create_super_user.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import os
33

4+
from pydantic import SecretStr
45
from sqlalchemy.orm import Session
56

67
from app.operations.role import crud as crud_role
@@ -37,7 +38,7 @@ def create_super_user(db: Session) -> None:
3738

3839
user = UserCreate(
3940
email=email,
40-
password=password,
41+
password=SecretStr(password),
4142
name=name,
4243
last_name=last_name,
4344
role_id=admin_role.id,

app/dependencies/auth.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from fastapi.params import Depends
44
from fastapi.security import OAuth2PasswordBearer
5+
from pydantic import SecretStr
56

67
from app.core.security import decode_token, verify_password
78
from app.database.session import DbSession
@@ -12,12 +13,12 @@
1213
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
1314

1415

15-
def authenticate_user(db: DbSession, email: str, password: str) -> User | None:
16+
def authenticate_user(db: DbSession, email: str, password: SecretStr) -> User | None:
1617
db_user = crud_user.get_user_by_email(db, email)
1718
if db_user is None:
1819
return None
1920

20-
if not verify_password(password, db_user.password):
21+
if not verify_password(password.get_secret_value(), db_user.password):
2122
return None
2223

2324
return db_user

app/fields.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import Annotated
2+
3+
from pydantic import Field, SecretStr
4+
5+
# Password constraints used for user-facing password fields (login, registration, update).
6+
PASSWORD_MIN_LENGTH = 8
7+
PASSWORD_MAX_LENGTH = 64
8+
9+
PasswordStr = Annotated[
10+
SecretStr,
11+
Field(min_length=PASSWORD_MIN_LENGTH, max_length=PASSWORD_MAX_LENGTH),
12+
]

app/operations/user/crud.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def get_user_by_email(db: Session, email: str) -> User | None:
1818

1919

2020
def create_user(db: Session, user: UserCreate) -> User:
21-
hashed_password = hash_password(user.password)
21+
hashed_password = hash_password(user.password.get_secret_value())
2222
db_user = User(**user.model_dump(exclude={"password"}), password=hashed_password)
2323

2424
db.add(db_user)

app/operations/user/schemas.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pydantic import BaseModel, EmailStr
22

3+
from app.fields import PasswordStr
4+
35

46
class UserBase(BaseModel):
57
email: EmailStr
@@ -8,7 +10,7 @@ class UserBase(BaseModel):
810

911

1012
class UserCreateBase(UserBase):
11-
password: str
13+
password: PasswordStr
1214

1315

1416
class UserCreate(UserCreateBase):
@@ -18,7 +20,7 @@ class UserCreate(UserCreateBase):
1820

1921
class UserUpdate(BaseModel):
2022
email: EmailStr | None = None
21-
password: str | None = None
23+
password: PasswordStr | None = None
2224
name: str | None = None
2325
last_name: str | None = None
2426
role_id: int | None = None

0 commit comments

Comments
 (0)