Skip to content

Commit f3f2193

Browse files
authored
Merge pull request #2 from YuriFontella/fix/exp
add: expiration access token
2 parents 4b3811d + 78e0312 commit f3f2193

File tree

6 files changed

+52
-13
lines changed

6 files changed

+52
-13
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ DATABASE_MAX_INACTIVE_CONNECTION_LIFETIME=300.0
2222

2323
# JWT Settings
2424
JWT_ALGORITHM=HS256
25+
ACCESS_TOKEN_EXPIRE_MINUTES=15
26+
REFRESH_TOKEN_EXPIRE_DAYS=7

src/config/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class AppSettings:
3838
PBKDF2_ALGORITHM: str = "sha256"
3939
MAX_FINGERPRINT_VALUE: int = 100_000_000
4040
BCRYPT_GENSALT: int = 12
41+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15 # 15 minutes
42+
REFRESH_TOKEN_EXPIRE_DAYS: int = 7 # 7 days
4143

4244
def __post_init__(self):
4345
self.SECRET_KEY = self.SECRET_KEY or os.getenv("SECRET_KEY")
@@ -46,6 +48,8 @@ def __post_init__(self):
4648
self.PBKDF2_ALGORITHM = os.getenv("PBKDF2_ALGORITHM", self.PBKDF2_ALGORITHM)
4749
self.MAX_FINGERPRINT_VALUE = int(os.getenv("MAX_FINGERPRINT_VALUE", self.MAX_FINGERPRINT_VALUE))
4850
self.BCRYPT_GENSALT = int(os.getenv("BCRYPT_GENSALT", self.BCRYPT_GENSALT))
51+
self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", self.ACCESS_TOKEN_EXPIRE_MINUTES))
52+
self.REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", self.REFRESH_TOKEN_EXPIRE_DAYS))
4953

5054
if not self.ALLOWED_CORS_ORIGINS:
5155
cors_origins = os.getenv("ALLOWED_CORS_ORIGINS")

src/db/migrations/003_create_sessions.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ CREATE TABLE IF NOT EXISTS sessions (
66
revoked bool DEFAULT false,
77
user_uuid uuid NOT NULL REFERENCES users (uuid) ON DELETE CASCADE,
88
type varchar NOT NULL DEFAULT 'manual',
9-
date timestamp with time zone DEFAULT current_timestamp
9+
create timestamp with time zone DEFAULT current_timestamp,
10+
update timestamp with time zone DEFAULT current_timestamp
1011
);
1112

1213
ALTER TABLE sessions ADD CONSTRAINT uq_sessions_access_token UNIQUE (access_token);

src/domain/users/repositories/session.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,17 @@ async def revoke_session(self, session_uuid: UUID) -> bool:
4141
await self.connection.execute(query, str(session_uuid))
4242
return True
4343

44-
async def update_access_token(self, session_uuid: str, access_token: str) -> None:
45-
"""Atualiza apenas o access_token de uma sessão existente"""
44+
async def update_access_token(
45+
self,
46+
session_uuid: str,
47+
access_token: str,
48+
user_agent: Optional[str],
49+
ip: Optional[str],
50+
) -> None:
51+
"""Atualiza o access_token, user_agent e ip de uma sessão existente"""
4652
query = """
4753
UPDATE sessions
48-
SET access_token = $1, date = NOW()
49-
WHERE uuid = $2 AND revoked = false
54+
SET access_token = $1, user_agent = $2, ip = $3, update = NOW()
55+
WHERE uuid = $4 AND revoked = false
5056
"""
51-
await self.connection.execute(query, access_token, session_uuid)
57+
await self.connection.execute(query, access_token, user_agent, ip, session_uuid)

src/domain/users/services.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import bcrypt
44
import jwt
55

6+
from datetime import datetime, timezone, timedelta
7+
68
from dataclasses import dataclass, field
79
from typing import Optional
810
from asyncpg import Connection
@@ -86,14 +88,26 @@ async def authenticate(
8688
if not session:
8789
raise ValueError("Something went wrong creating the session")
8890

91+
# Calculate expiration times
92+
access_token_exp = datetime.now(timezone.utc) + timedelta(minutes=self.settings.app.ACCESS_TOKEN_EXPIRE_MINUTES)
93+
refresh_token_exp = datetime.now(timezone.utc) + timedelta(days=self.settings.app.REFRESH_TOKEN_EXPIRE_DAYS)
94+
8995
access_token_jwt = jwt.encode(
90-
{"uuid": str(user_uuid), "access_token": random_access_token},
96+
{
97+
"uuid": str(user_uuid),
98+
"access_token": random_access_token,
99+
"exp": access_token_exp
100+
},
91101
key=self.settings.app.SECRET_KEY,
92102
algorithm=self.settings.app.JWT_ALGORITHM,
93103
)
94104

95105
refresh_token_jwt = jwt.encode(
96-
{"uuid": str(user_uuid), "refresh_token": random_refresh_token},
106+
{
107+
"uuid": str(user_uuid),
108+
"refresh_token": random_refresh_token,
109+
"exp": refresh_token_exp
110+
},
97111
key=self.settings.app.SECRET_KEY,
98112
algorithm=self.settings.app.JWT_ALGORITHM,
99113
)
@@ -105,7 +119,6 @@ async def refresh_access_token(
105119
) -> Token:
106120
"""Refreshes the access_token using a valid refresh_token"""
107121
try:
108-
# Decode the refresh token JWT
109122
decoded = jwt.decode(
110123
jwt=refresh_token,
111124
key=self.settings.app.SECRET_KEY,
@@ -146,18 +159,29 @@ async def refresh_access_token(
146159
# Update only the access token in the existing session
147160
await self.session_repository.update_access_token(
148161
session_uuid=session["uuid"],
149-
access_token=access_token_hash.hex()
162+
access_token=access_token_hash.hex(),
163+
user_agent=user_agent,
164+
ip=ip
150165
)
151166

167+
# Calculate expiration time for new access token
168+
access_token_exp = datetime.now(timezone.utc) + timedelta(minutes=self.settings.app.ACCESS_TOKEN_EXPIRE_MINUTES)
169+
152170
# Generate new access token JWT
153171
access_token_jwt = jwt.encode(
154-
{"uuid": user_uuid, "access_token": random_access_token},
172+
{
173+
"uuid": user_uuid,
174+
"access_token": random_access_token,
175+
"exp": access_token_exp
176+
},
155177
key=self.settings.app.SECRET_KEY,
156178
algorithm=self.settings.app.JWT_ALGORITHM,
157179
)
158180

159181
# Return the new access token and keep the same refresh token
160182
return Token(access_token=access_token_jwt, refresh_token=refresh_token)
161183

184+
except jwt.ExpiredSignatureError:
185+
raise ValueError("Refresh token expired")
162186
except jwt.PyJWTError:
163187
raise ValueError("Invalid refresh token format")

src/server/auth.py

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

3-
from jwt import PyJWTError, decode
3+
from jwt import PyJWTError, ExpiredSignatureError, decode
44

55
from litestar.connection import ASGIConnection
66
from litestar.exceptions import NotAuthorizedException
@@ -44,8 +44,10 @@ async def authenticate_request(
4444
if not user:
4545
raise NotAuthorizedException()
4646

47+
except ExpiredSignatureError:
48+
raise NotAuthorizedException(detail="Token expired")
4749
except PyJWTError:
48-
raise NotAuthorizedException()
50+
raise NotAuthorizedException(detail="Invalid token")
4951

5052
else:
5153
return AuthenticationResult(user=user, auth=auth)

0 commit comments

Comments
 (0)