Skip to content

Commit 46dbdb1

Browse files
committed
add: implement logout endpoint and session revocation logic
1 parent 210956d commit 46dbdb1

File tree

4 files changed

+87
-1
lines changed

4 files changed

+87
-1
lines changed

src/domain/users/controllers.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from typing import Dict
22

3-
from litestar import Controller, Request, post, get
3+
from litestar import Controller, Request, Response, post, get
44
from litestar.di import Provide
55
from litestar.channels import ChannelsPlugin
66
from litestar.exceptions import HTTPException
77
from litestar.params import Parameter
8+
from litestar.datastructures import Cookie
89

910
from src.server.auth import AuthenticationMiddleware
1011

@@ -108,3 +109,42 @@ async def users_data(self, current_user: Dict) -> User:
108109
role=current_user["role"],
109110
status=current_user["status"],
110111
)
112+
113+
@post(path="/logout", middleware=[AuthenticationMiddleware])
114+
async def logout(
115+
self, request: Request, current_user: Dict, users_service: UsersService
116+
) -> Response[bool]:
117+
auth = request.auth
118+
access_token = auth.get("access_token") if auth else None
119+
120+
if access_token and current_user.get("uuid"):
121+
await users_service.revoke_current_session(
122+
user_uuid=str(current_user["uuid"]), access_token=access_token
123+
)
124+
125+
response = Response(
126+
content=True,
127+
cookies=[
128+
Cookie(
129+
key="x-access-token",
130+
value=None,
131+
httponly=True,
132+
secure=False,
133+
samesite="strict",
134+
max_age=0,
135+
expires=0,
136+
path="/",
137+
),
138+
Cookie(
139+
key="x-refresh-token",
140+
value=None,
141+
httponly=True,
142+
secure=False,
143+
samesite="strict",
144+
max_age=0,
145+
expires=0,
146+
path="/",
147+
),
148+
],
149+
)
150+
return response

src/domain/users/repositories/session.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,32 @@ async def get_by_refresh_token(self, refresh_token: str) -> Optional[dict]:
3434
"""
3535
return await self.connection.fetchrow(query, refresh_token)
3636

37+
async def get_by_user_and_access_token(
38+
self, user_uuid: str, access_token: str
39+
) -> Optional[dict]:
40+
query = """
41+
SELECT uuid, user_uuid, access_token, refresh_token, revoked
42+
FROM sessions
43+
WHERE user_uuid = $1 AND access_token = $2 AND revoked = false
44+
"""
45+
return await self.connection.fetchrow(query, user_uuid, access_token)
46+
3747
async def revoke_session(self, session_uuid: UUID) -> bool:
3848
query = """
3949
UPDATE sessions SET revoked = true WHERE uuid = $1
4050
"""
4151
await self.connection.execute(query, str(session_uuid))
4252
return True
4353

54+
async def revoke_user_sessions(self, user_uuid: UUID) -> bool:
55+
"""Revoga todas as sessões ativas de um usuário"""
56+
query = """
57+
UPDATE sessions SET revoked = true
58+
WHERE user_uuid = $1 AND revoked = false
59+
"""
60+
await self.connection.execute(query, str(user_uuid))
61+
return True
62+
4463
async def update_access_token(
4564
self,
4665
session_uuid: str,

src/domain/users/services.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ async def authenticate(
8686
self.settings.app.PBKDF2_ITERATIONS,
8787
)
8888

89+
# Revoke all active sessions for this user before creating a new one
90+
await self.session_repository.revoke_user_sessions(user_uuid)
91+
8992
session = await self.session_repository.create(
9093
access_token=access_token_hash.hex(),
9194
refresh_token=refresh_token_hash.hex(),
@@ -202,3 +205,25 @@ async def refresh_access_token(
202205
raise ValueError("Refresh token expired")
203206
except jwt.PyJWTError:
204207
raise ValueError("Invalid refresh token format")
208+
209+
async def revoke_current_session(self, user_uuid: str, access_token: str) -> bool:
210+
"""Revokes the current session by user_uuid and access_token"""
211+
# Hash the access token to match the stored hash
212+
salt = self.settings.app.SESSION_SALT
213+
access_token_hash = hashlib.pbkdf2_hmac(
214+
self.settings.app.PBKDF2_ALGORITHM,
215+
access_token.encode(),
216+
salt.encode(),
217+
self.settings.app.PBKDF2_ITERATIONS,
218+
)
219+
220+
# Get the session by user_uuid and access_token
221+
session = await self.session_repository.get_by_user_and_access_token(
222+
user_uuid=user_uuid, access_token=access_token_hash.hex()
223+
)
224+
225+
if not session:
226+
raise ValueError("Session not found")
227+
228+
# Revoke the session
229+
return await self.session_repository.revoke_session(session["uuid"])

src/server/auth.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ async def authenticate_request(
4141
select u.uuid, u.name, u.email, u.role, u.status from users u
4242
join sessions s on u.uuid = s.user_uuid
4343
where u.uuid = $1 and s.access_token = $2 and s.revoked = false and u.status = true
44+
order by s.created_at desc
45+
limit 1
4446
"""
4547
user = await conn.fetchrow(query, user_uuid, access_token.hex())
4648

0 commit comments

Comments
 (0)