Skip to content

Commit 8f5570b

Browse files
authored
Merge branch 'main' into 3-problems-data
2 parents 0419d72 + b028643 commit 8f5570b

19 files changed

+305
-91
lines changed

.env.auth

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,4 @@
66
JWT_SECRET="Den Pidr:)"
77
ALGORITHM="RS256"
88
# for testing
9-
# ACCESS_TOKEN_EXPIRE_MINUTES=1
10-
# REFRESH_TOKEN_EXPIRE_MINUTES=2
9+
# ACCESS_TOKEN_EXPIRE_MINUTES=1

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,4 +164,4 @@ logfile
164164
venv*
165165
.env*
166166
traefik.toml
167-
*venv/
167+
*venv/

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ FastAPI server for organizing leetcode progress
77
# start app
88
docker compose up --build
99
```
10-
If error occurs when binding port 80 for Traefik, it means that Apache is already launched on it:
10+
# Testing
1111
```bash
12-
sudo service apache2 stop
12+
docker compose exec auth_server pytest tests/ -v -s
1313
```

auth_server/api_v1/users/dependencies.py

+22-34
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
"Dependencies for endpoints.py"
1+
"""
2+
Dependencies for endpoints.py
3+
"""
4+
25
from typing import Annotated
36

4-
from fastapi import Cookie, Depends, HTTPException, Response, status
7+
from fastapi import Cookie, Depends, HTTPException, status
58
from fastapi.security import OAuth2PasswordRequestForm
6-
from sqlalchemy import select
79
from sqlalchemy.ext.asyncio import AsyncSession
8-
from jwt import ExpiredSignatureError, InvalidTokenError
10+
from jwt import InvalidTokenError
911

1012

1113
from core.models import pg_db_helper, User
12-
from auth.utils import decode_jwt, encode_jwt, validate_password
14+
from auth.utils import decode_jwt, validate_password
1315
from api_v1.users.crud import get_user_by_username, get_user_by_id
1416

1517

1618
async def validate_auth_user_password(
1719
form_data: OAuth2PasswordRequestForm = Depends(),
18-
session: AsyncSession = Depends(pg_db_helper.get_scoped_session),
20+
session: AsyncSession = Depends(pg_db_helper.scoped_session_dependency),
1921
):
2022
"""
2123
Helper Function for login route to check a user by password
@@ -46,67 +48,53 @@ async def validate_auth_user_password(
4648
return user_from_db
4749

4850

49-
async def validate_tokens(
50-
response: Response,
51+
async def validate_access_token(
5152
access_token: Annotated[str | None, Cookie()] = None,
52-
refresh_token: Annotated[str | None, Cookie()] = None,
5353
) -> dict:
54-
if not refresh_token:
54+
if not access_token:
5555
raise HTTPException(
5656
status_code=status.HTTP_401_UNAUTHORIZED,
5757
detail="user is not logged in",
5858
)
59-
refresh_payload = {}
59+
token_payload = {}
6060
try:
61-
refresh_payload = decode_jwt(
62-
token=refresh_token,
61+
token_payload = decode_jwt(
62+
token=access_token,
6363
)
6464
except InvalidTokenError as e:
6565
raise HTTPException(
6666
status_code=status.HTTP_401_UNAUTHORIZED,
6767
# NOTE: REMOVE EXCEPTION IN PROD
68-
detail=f"invalid refresh token error: {e}",
69-
)
70-
# if we dont need to create new access_token, use old one as new :)
71-
new_access_token = access_token
72-
# if access_token is expired, generate new one
73-
try:
74-
decode_jwt(
75-
token=access_token,
68+
detail=f"invalid access token error: {e}",
7669
)
77-
except ExpiredSignatureError as e:
78-
new_access_token = encode_jwt(payload={"sub": refresh_payload["sub"]})
79-
80-
# return new access token to set to cookie
81-
response.set_cookie("access_token", new_access_token)
82-
return refresh_payload
70+
return token_payload
8371

8472

85-
async def get_current_refresh_token_payload(
86-
refresh_token: Annotated[str | None, Cookie()] = None,
73+
async def get_current_access_token_payload(
74+
access_token: Annotated[str | None, Cookie()] = None,
8775
) -> dict:
88-
if not refresh_token:
76+
if not access_token:
8977
raise HTTPException(
9078
status_code=status.HTTP_401_UNAUTHORIZED,
9179
detail="user is not logged in",
9280
)
9381
payload = {}
9482
try:
9583
payload = decode_jwt(
96-
token=refresh_token,
84+
token=access_token,
9785
)
9886
except InvalidTokenError as e:
9987
raise HTTPException(
10088
status_code=status.HTTP_401_UNAUTHORIZED,
10189
# NOTE: REMOVE EXCEPTION IN PROD
102-
detail=f"invalid refresh token error: {e}",
90+
detail=f"invalid access token error: {e}",
10391
)
10492
return payload
10593

10694

10795
async def get_current_auth_user(
108-
payload: dict = Depends(get_current_refresh_token_payload),
109-
session: AsyncSession = Depends(pg_db_helper.get_scoped_session),
96+
payload: dict = Depends(get_current_access_token_payload),
97+
session: AsyncSession = Depends(pg_db_helper.scoped_session_dependency),
11098
) -> User:
11199
id: int | None = payload.get("sub")
112100

auth_server/api_v1/users/endpoints.py

+6-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
"""Endpoints in users router"""
1+
"""
2+
Endpoints in users router
3+
"""
24

35
from typing import Annotated
46

@@ -19,12 +21,11 @@
1921
from auth import (
2022
hash_password,
2123
encode_jwt,
22-
encode_refresh_jwt,
2324
)
2425
from api_v1.users.crud import check_user_by_username, create_user
2526
from api_v1.users.dependencies import (
2627
validate_auth_user_password,
27-
validate_tokens,
28+
validate_access_token,
2829
get_current_auth_user,
2930
)
3031

@@ -49,15 +50,11 @@ async def register_user(
4950
user: UserIn,
5051
session: AsyncSession = Depends(pg_db_helper.scoped_session_dependency),
5152
access_token: Annotated[str | None, Cookie()] = None,
52-
refresh_token: Annotated[str | None, Cookie()] = None,
5353
) -> UserOut:
5454
# removing access_token cookie
5555
headers = {}
56-
if refresh_token:
57-
response.delete_cookie("refresh_token")
5856
if access_token:
5957
response.delete_cookie("access_token")
60-
if refresh_token or access_token:
6158
headers = {"set-cookie": response.headers["set-cookie"]}
6259
# check if user already in db
6360
if await check_user_by_username(session=session, username=user.username):
@@ -86,18 +83,8 @@ async def login_user(
8683
httponly=True,
8784
samesite="lax",
8885
)
89-
refresh_token_value = encode_refresh_jwt(
90-
payload={"sub": user.id},
91-
)
92-
response.set_cookie(
93-
key="refresh_token",
94-
value=f"{refresh_token_value}",
95-
httponly=True,
96-
samesite="lax",
97-
)
9886
return TokenInfo(
9987
access_token=access_token_value,
100-
refresh_token=refresh_token_value,
10188
token_type="Bearer",
10289
)
10390

@@ -106,25 +93,22 @@ async def login_user(
10693
async def logout_user(
10794
response: Response,
10895
access_token: Annotated[str | None, Cookie()] = None,
109-
refresh_token: Annotated[str | None, Cookie()] = None,
11096
):
11197
if not access_token:
11298
raise HTTPException(
11399
status_code=status.HTTP_403_FORBIDDEN, detail="User isnt logged in"
114100
)
115101
response.delete_cookie("access_token")
116-
if refresh_token:
117-
response.delete_cookie("refresh_token")
118102
return {"detail": "Logged out"}
119103

120104

121105
@router.get("/validate/")
122106
async def validate_token(
123-
refresh_payload=Depends(validate_tokens),
107+
token_payload=Depends(validate_access_token),
124108
user: User = Depends(get_current_auth_user),
125109
):
126110
return {
127111
"username": user.username,
128112
"email": user.email,
129-
"exp": refresh_payload["exp"],
113+
"exp": token_payload["exp"],
130114
}

auth_server/api_v1/users/schemas.py

-1
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,4 @@ class UserOut(UserBase): ...
2222

2323
class TokenInfo(BaseModel):
2424
access_token: str
25-
refresh_token: str
2625
token_type: str

auth_server/app.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Main project module
33
This app is responsible for all authorization processes
4-
and is based on JWT on cookie with access and refresh tokens
4+
and is based on JWT on cookie with access token
55
"""
66

77
from contextlib import asynccontextmanager

auth_server/auth/__init__.py

-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
"validate_password",
44
"encode_jwt",
55
"decode_jwt",
6-
"encode_refresh_jwt",
76
)
87

98
from .utils import (
109
hash_password,
1110
validate_password,
1211
encode_jwt,
1312
decode_jwt,
14-
encode_refresh_jwt,
1513
)

auth_server/auth/utils.py

-9
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,3 @@ def decode_jwt(
3939
) -> dict:
4040
decoded = jwt.decode(token, public_key, algorithms=[algorithm])
4141
return decoded
42-
43-
44-
def encode_refresh_jwt(
45-
payload: dict,
46-
private_key: str = settings.private_key_path.read_text(),
47-
algorithm: str = settings.algorithm,
48-
expire_minutes: int = settings.refresh_token_expire_minutes,
49-
):
50-
return encode_jwt(payload, private_key, algorithm, expire_minutes)

auth_server/core/config.py

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class Settings(BaseSettings):
2121
public_key_path: Path = BASE_DIR / "auth" / "certs" / "jwt-public.pem"
2222
algorithm: str = "RS256"
2323
access_token_expire_minutes: int = 10
24-
refresh_token_expire_minutes: int = 60 * 24 * 7
2524
# pg: PostgresSettings = PostgresSettings()
2625
# jwt: JWTSettings = JWTSettings()
2726

auth_server/core/models/helper.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""Module with database helper class"""
22

33
from asyncio import current_task
4+
from typing import AsyncGenerator
45

56
from sqlalchemy.ext.asyncio import (
6-
AsyncSession,
77
create_async_engine,
88
async_sessionmaker,
99
async_scoped_session,
@@ -31,12 +31,12 @@ def get_scoped_session(self):
3131
)
3232
return session
3333

34-
async def session_dependency(self) -> AsyncSession:
34+
async def session_dependency(self) -> AsyncGenerator:
3535
async with self.session_factory() as session:
3636
yield session
3737
await session.close()
3838

39-
async def scoped_session_dependency(self) -> AsyncSession:
39+
async def scoped_session_dependency(self) -> AsyncGenerator:
4040
session = self.get_scoped_session()
4141
yield session
4242
await session.close()

auth_server/core/models/user.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Module with User model for db"""
22

3-
from typing import Optional
4-
53
from sqlalchemy import String, LargeBinary, sql
64
from sqlalchemy.orm import Mapped, mapped_column
75

@@ -12,6 +10,6 @@ class User(Base):
1210
"""User model for db"""
1311

1412
username: Mapped[str] = mapped_column(String(30), unique=True)
15-
email: Mapped[Optional[str]]
13+
email: Mapped[str | None]
1614
is_active: Mapped[bool] = mapped_column(server_default=sql.true())
1715
hashed_password: Mapped[bytes] = mapped_column(LargeBinary())

auth_server/pyproject.toml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[tool.pytest.ini_options]
2+
pythonpath = [
3+
".", ".",
4+
]
5+
asyncio_mode="auto"

auth_server/tests/conftest.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Configurations for pytest"""
2+
3+
from typing import AsyncGenerator
4+
from fastapi.testclient import TestClient
5+
from httpx import AsyncClient
6+
7+
import pytest
8+
from sqlalchemy import NullPool
9+
from sqlalchemy.orm import sessionmaker
10+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
11+
12+
from core.models.helper import pg_db_helper
13+
from core.models import User
14+
from core.config import settings
15+
from app import app
16+
17+
18+
TEST_DB_URL = (
19+
f"postgresql+asyncpg://auth:{settings.postgres_password}@postgres_test/auth"
20+
)
21+
22+
engine_test = create_async_engine(TEST_DB_URL, poolclass=NullPool)
23+
async_session_maker = sessionmaker(
24+
engine_test, class_=AsyncSession, expire_on_commit=False
25+
)
26+
27+
28+
async def override_get_async_session() -> AsyncGenerator[AsyncSession, None]:
29+
async with async_session_maker() as session:
30+
yield session
31+
32+
33+
app.dependency_overrides[pg_db_helper.scoped_session_dependency] = (
34+
override_get_async_session
35+
)
36+
User.metadata.bind = engine_test
37+
38+
39+
@pytest.fixture(autouse=True, scope="session")
40+
async def prepare_database():
41+
async with engine_test.begin() as conn:
42+
await conn.run_sync(User.metadata.create_all)
43+
yield
44+
async with engine_test.begin() as conn:
45+
await conn.run_sync(User.metadata.drop_all)
46+
47+
48+
client = TestClient(app)
49+
50+
51+
@pytest.fixture(scope="session")
52+
async def ac() -> AsyncGenerator[AsyncClient, None]:
53+
async with AsyncClient(app=app, base_url="http://test") as ac:
54+
yield ac

0 commit comments

Comments
 (0)