Skip to content

Commit 634caf5

Browse files
committed
Add Auth App
1 parent 0f39e9d commit 634caf5

42 files changed

Lines changed: 2244 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/.env.example

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Server
2+
POST=8000
3+
HOST=localhost
4+
5+
# Environment
6+
ENVIRONMENT=development
7+
8+
# Database
9+
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
10+
11+
# Security
12+
SECRET_KEY=my_secret_key
13+
14+
# JWT
15+
JWT_ALGORITHM=HS256
16+
ACCESS_TOKEN_EXPIRE_MINUTES=900
17+
REFRESH_TOKEN_EXPIRE_DAYS=43200
18+
AUDIENCE="Your-service"
19+
20+
# Redis
21+
REDIS_URL=redis://localhost:6379/0
22+
23+
# SQL
24+
SQL_ECHO=False
25+
26+
# Cookie & CORS
27+
HTTP_ONLY=True
28+
HTTP_SECURE=False
29+
SAME_SITE=lax
30+
ALLOWED_ORIGINS_LIST=["https://localhost:8000"]
31+
32+
# JWT Issuer
33+
ISSUER=myapp

src/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
FastAPI Authentication Service

src/auth/controller.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
from datetime import datetime
2+
3+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
4+
5+
from core.security.jwt.controller import JWTController
6+
from core.security.utils.hashing import hash_password
7+
from infrastructure.redis.client import RedisManager
8+
9+
from .expections import PasswordError, TokenInvalidError, TokenRevokedError
10+
from .repository import AuthRepository
11+
from .schamas import (
12+
LoginSchema,
13+
RegisterAccessSchema,
14+
RegisterRefreshSchema,
15+
logoutSchamas,
16+
)
17+
18+
19+
class AuthController:
20+
def __init__(
21+
self,
22+
repository: AuthRepository,
23+
session_factory: async_sessionmaker[AsyncSession],
24+
jwt_controller: JWTController,
25+
redis_manger: RedisManager,
26+
) -> None:
27+
28+
self.repository = repository
29+
self.session_factory = session_factory
30+
self.jwt_controller = jwt_controller
31+
self.redis_manager = redis_manger
32+
33+
async def login(self, data: LoginSchema, **context):
34+
35+
async with (
36+
self.session_factory() as session
37+
): # Just connection Here for prevent idle_transaction and less session active
38+
async with session.begin():
39+
user = await self.repository.get_user_by_username(
40+
session=session, username=data.username
41+
)
42+
43+
if not user:
44+
raise PasswordError("User not found")
45+
46+
if hash_password(data.password) != user.password_hash:
47+
raise PasswordError("Invalid password")
48+
49+
assert context.get("context").get("request"), "must context for request"
50+
request = context["context"]["request"]
51+
52+
user_agent = request.user_agent
53+
ip_address = request.ip_address.host
54+
55+
token_pair = await self.jwt_controller.create_token_pair(
56+
user_id=user.id,
57+
user_agent=user_agent,
58+
ip_address=ip_address,
59+
)
60+
61+
return token_pair
62+
63+
async def register_access(
64+
self,
65+
data: RegisterAccessSchema,
66+
**context,
67+
):
68+
access_token = data.access_token
69+
70+
payload = await self.jwt_controller.get_access_token(access_token)
71+
72+
is_revoked = await self.redis_manager.get(f"revoked_access_token:{payload.jti}")
73+
74+
if is_revoked:
75+
raise TokenInvalidError("access token is invalid")
76+
77+
return payload
78+
79+
async def register_refresh(
80+
self,
81+
data: RegisterRefreshSchema,
82+
**context,
83+
):
84+
refresh_token = data.refresh_token
85+
86+
payload, family_id = await self.jwt_controller.get_refresh_token(refresh_token)
87+
88+
is_revoked = await self.redis_manager.get(
89+
f"revoked_refresh_token:{payload.jti}"
90+
)
91+
92+
if is_revoked:
93+
raise TokenInvalidError("refresh token is invalid")
94+
95+
return payload, family_id
96+
97+
async def refresh(
98+
self,
99+
data: RegisterRefreshSchema,
100+
):
101+
102+
refresh_token = data.refresh_token
103+
104+
refresh_token_payload, family_id = await self.jwt_controller.get_refresh_token(
105+
refresh_token
106+
)
107+
108+
is_revoked = await self.redis_manager.get(
109+
f"revoked_refresh_token:{refresh_token_payload.jti}"
110+
)
111+
112+
if is_revoked:
113+
raise TokenInvalidError("refresh token is invalid")
114+
115+
async with (
116+
self.session_factory() as session
117+
): # Just connection Here for prevent idle_transaction and less session active
118+
async with session.begin():
119+
refresh_token_model = await self.jwt_controller.repository.get_by_jti(
120+
session,
121+
refresh_token_payload.jti,
122+
)
123+
if not refresh_token_model:
124+
raise TokenInvalidError("tokan was invalidate in model class")
125+
await self.jwt_controller.repository.mark_used(
126+
session,
127+
refresh_token,
128+
datetime.now(),
129+
)
130+
131+
if refresh_token_model.revoked_at:
132+
raise TokenRevokedError("Refresh token was revoked")
133+
134+
(
135+
create_access_token,
136+
access_token_pyload,
137+
) = await self.jwt_controller.create_access_token(
138+
user_id=refresh_token_payload.sub,
139+
)
140+
141+
return create_access_token, family_id
142+
143+
async def logout(
144+
self,
145+
data: logoutSchamas,
146+
**context,
147+
):
148+
# first check if the token is valid
149+
access_token = data.access_token
150+
try:
151+
access_token_payload = await self.jwt_controller.get_access_token(
152+
access_token
153+
)
154+
except Exception as e:
155+
raise TokenInvalidError(f"access token is invalid {e}")
156+
157+
refresh_token = data.refresh_token
158+
try:
159+
(
160+
refresh_token_payload,
161+
family_id,
162+
) = await self.jwt_controller.get_refresh_token(refresh_token)
163+
except Exception as e:
164+
raise TokenInvalidError(f"refresh token is invalid {e}")
165+
166+
# check if the refresh token is valid or not revoked
167+
async with (
168+
self.session_factory() as session
169+
): # Just connection Here for prevent idle_transaction and less session active
170+
async with session.begin():
171+
# get refresh token
172+
refresh_token_model = await self.jwt_controller.repository.get_by_jti(
173+
session,
174+
refresh_token_payload.jti,
175+
)
176+
# mark as last
177+
if refresh_token_model is None:
178+
raise TokenInvalidError("refresh token is invalid")
179+
180+
await self.jwt_controller.repository.mark_used(
181+
session,
182+
refresh_token_model.jti,
183+
datetime.now(),
184+
)
185+
186+
if refresh_token_model.revoked_at:
187+
raise TokenInvalidError("refresh token is invalid it revoked")
188+
189+
await self.jwt_controller.repository.revoke(
190+
session,
191+
refresh_token_model.jti,
192+
datetime.now(),
193+
)
194+
195+
await self.redis_manager.set(
196+
f"revoked_refresh_token:{refresh_token_payload.jti}",
197+
"1", # TODO need data
198+
ex=30,
199+
)
200+
201+
"""
202+
Hint that accces token is state less
203+
but in secure system we can store in redis
204+
to prevent replay attacks
205+
206+
and have ttl just 5 min ot that can be configured
207+
"""
208+
209+
await self.redis_manager.set(
210+
f"revoked_access_token:{access_token_payload.jti}",
211+
"1", # TODO need data
212+
ex=5,
213+
)
214+
215+
return True

src/auth/dependencies.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from core.security.jwt.dependencies import get_jwt_controller
2+
from infrastructure.redis.client import RedisManager
3+
from infrastructure.sqlalchemy.AsyncSession import AsyncSessionLocal
4+
5+
from .controller import AuthController
6+
from .repository import AuthRepository
7+
8+
9+
def get_auth_repository():
10+
return AuthRepository()
11+
12+
13+
def get_auth_controller():
14+
return AuthController(
15+
repository=get_auth_repository(),
16+
jwt_controller=get_jwt_controller(),
17+
session_factory=AsyncSessionLocal,
18+
redis_manger=RedisManager(),
19+
)

src/auth/expections.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from http import HTTPStatus
2+
3+
from core.base.expections import CustomException
4+
5+
6+
class PasswordError(CustomException):
7+
code = HTTPStatus.UNAUTHORIZED
8+
error_code = HTTPStatus.UNAUTHORIZED
9+
message = HTTPStatus.UNAUTHORIZED.description
10+
11+
12+
class TokenRevokedError(CustomException):
13+
code = HTTPStatus.UNAUTHORIZED
14+
error_code = HTTPStatus.UNAUTHORIZED
15+
message = HTTPStatus.UNAUTHORIZED.description
16+
17+
18+
class TokenInvalidError(CustomException):
19+
code = HTTPStatus.UNAUTHORIZED
20+
error_code = HTTPStatus.UNAUTHORIZED
21+
message = HTTPStatus.UNAUTHORIZED.description

src/auth/repository.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from users.repository import UserRepository
2+
3+
4+
class AuthRepository(UserRepository):
5+
pass

0 commit comments

Comments
 (0)