Skip to content

Commit 0d150eb

Browse files
committed
refactor: split refresh tokens cleanup function, harmonize units and handle pending/ready flows
1 parent 48ecec8 commit 0d150eb

8 files changed

Lines changed: 109 additions & 50 deletions

File tree

diracx-core/src/diracx/core/settings.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
SecretStr,
3333
TypeAdapter,
3434
UrlConstraints,
35+
model_validator,
3536
)
3637
from pydantic_settings import BaseSettings, SettingsConfigDict
3738

@@ -151,6 +152,21 @@ def create(cls) -> Self:
151152
class AuthSettings(ServiceSettingsBase):
152153
"""Settings for the authentication service."""
153154

155+
@model_validator(mode="after")
156+
def check_retention_greater_than_expiration(self) -> Self:
157+
"""Ensure retention times are bigger than expiration times to avoid deleting valid flows."""
158+
if self.completed_flow_retention_minutes <= (
159+
self.device_flow_expiration_seconds / 60
160+
) or self.completed_flow_retention_minutes <= (
161+
self.authorization_flow_expiration_seconds / 60
162+
):
163+
raise ValueError(
164+
f"completed_flow_retention_minutes ({self.completed_flow_retention_minutes} minutes) must be bigger"
165+
f" than device_flow_expiration_seconds ({self.device_flow_expiration_seconds / 60} minutes) and"
166+
f" authorization_flow_expiration_seconds: ({self.authorization_flow_expiration_seconds / 60} minutes)"
167+
)
168+
return self
169+
154170
model_config = SettingsConfigDict(
155171
env_prefix="DIRACX_SERVICE_AUTH_", use_attribute_docstrings=True
156172
)
@@ -233,11 +249,11 @@ class AuthSettings(ServiceSettingsBase):
233249
through a new authentication flow. Default: 60 minutes.
234250
"""
235251

236-
revoked_refresh_token_retention_days: int = 30
237-
"""Retention time in days for revoked refresh tokens.
252+
revoked_refresh_token_retention_minutes: int = 43200
253+
"""Retention time in minutes for revoked refresh tokens.
238254
239255
The maximum retention time of refresh tokens after being
240-
revoked and before they are deleted. Default: 30 days.
256+
revoked and before they are deleted. Default: 43200 minutes (30 days).
241257
"""
242258

243259
available_properties: set[SecurityProperty] = Field(

diracx-db/src/diracx/db/sql/auth/db.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,11 @@ async def revoke_user_refresh_tokens(self, subject):
340340
.values(status=RefreshTokenStatus.REVOKED)
341341
)
342342

343-
async def clean_expired_refresh_token(
344-
self, max_validity: int, max_retention: int
345-
) -> tuple[int, int]:
346-
"""Delete expired and old revoked refresh tokens."""
343+
async def clean_expired_refresh_tokens(self, max_validity: int) -> int:
344+
"""Delete expired refresh tokens.
345+
346+
max_validity: Maximum validity time in minutes for refresh tokens.
347+
"""
347348
expired_date = str(
348349
uuid7_from_datetime(substract_date(minutes=max_validity), randomize=False)
349350
)
@@ -353,31 +354,60 @@ async def clean_expired_refresh_token(
353354
)
354355
res_expired = await self.conn.execute(stmt_expired)
355356

357+
return res_expired.rowcount
358+
359+
async def clean_revoked_refresh_tokens(self, max_retention: int) -> int:
360+
"""Delete old revoked refresh tokens.
361+
362+
max_retention: Maximum retention time in minutes for revoked refresh tokens.
363+
"""
356364
revoked_date = str(
357-
uuid7_from_datetime(substract_date(days=max_retention), randomize=False)
365+
uuid7_from_datetime(substract_date(minutes=max_retention), randomize=False)
358366
)
359367
stmt_revoked = delete(RefreshTokens).where(
360368
RefreshTokens.status == RefreshTokenStatus.REVOKED,
361369
RefreshTokens.jti < revoked_date,
362370
)
363371
res_revoked = await self.conn.execute(stmt_revoked)
364372

365-
return res_expired.rowcount, res_revoked.rowcount
373+
return res_revoked.rowcount
366374

367375
async def clean_expired_authorization_flows(self, max_retention: int) -> int:
368-
"""Delete old DONE/ERROR authorization flows."""
376+
"""Delete old authorization flows.
377+
378+
max_retention: Maximum retention time in minutes for expired authorization flows.
379+
Must be bigger than authorization_flow_expiration_seconds.
380+
"""
369381
stmt_auth = delete(AuthorizationFlows).where(
370-
AuthorizationFlows.status.in_([FlowStatus.DONE, FlowStatus.ERROR]),
382+
AuthorizationFlows.status.in_(
383+
[
384+
FlowStatus.PENDING,
385+
FlowStatus.READY,
386+
FlowStatus.DONE,
387+
FlowStatus.ERROR,
388+
]
389+
),
371390
AuthorizationFlows.creation_time < substract_date(minutes=max_retention),
372391
)
373392
res_auth = await self.conn.execute(stmt_auth)
374393

375394
return res_auth.rowcount
376395

377396
async def clean_expired_device_flows(self, max_retention: int) -> int:
378-
"""Delete old DONE/ERROR device flows."""
397+
"""Delete old device flows.
398+
399+
max_retention: Maximum retention time in minutes for expired device flows.
400+
Must be bigger than device_flow_expiration_seconds.
401+
"""
379402
stmt_device = delete(DeviceFlows).where(
380-
DeviceFlows.status.in_([FlowStatus.DONE, FlowStatus.ERROR]),
403+
DeviceFlows.status.in_(
404+
[
405+
FlowStatus.PENDING,
406+
FlowStatus.READY,
407+
FlowStatus.DONE,
408+
FlowStatus.ERROR,
409+
]
410+
),
381411
DeviceFlows.creation_time < substract_date(minutes=max_retention),
382412
)
383413
res_device = await self.conn.execute(stmt_device)

diracx-db/tests/auth/test_authorization_flow.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,39 @@ async def test_insert(auth_db: AuthDB):
8080
async def test_clean_authorization_flows(auth_db: AuthDB):
8181
# Insert two authorization flows
8282
async with auth_db as auth_db:
83-
uuid1 = await auth_db.insert_authorization_flow(
83+
_ = await auth_db.insert_authorization_flow(
8484
"client_id", "scope", "code_challenge", "S256", "redirect_uri"
8585
)
8686
uuid2 = await auth_db.insert_authorization_flow(
8787
"client_id2", "scope2", "code_challenge2", "S256", "redirect_uri2"
8888
)
89+
uuid3 = await auth_db.insert_authorization_flow(
90+
"client_id3", "scope3", "code_challenge3", "S256", "redirect_uri3"
91+
)
92+
uuid4 = await auth_db.insert_authorization_flow(
93+
"client_id4", "scope4", "code_challenge4", "S256", "redirect_uri4"
94+
)
8995

9096
id_token = {"sub": "myIdToken"}
9197

9298
async with auth_db as auth_db:
93-
code1, _ = await auth_db.authorization_flow_insert_id_token(uuid1, id_token, 1)
94-
code2, _ = await auth_db.authorization_flow_insert_id_token(uuid2, id_token, 1)
99+
_, _ = await auth_db.authorization_flow_insert_id_token(uuid2, id_token, 1)
100+
code3, _ = await auth_db.authorization_flow_insert_id_token(uuid3, id_token, 1)
101+
code4, _ = await auth_db.authorization_flow_insert_id_token(uuid4, id_token, 1)
95102

96103
async with auth_db as auth_db:
97-
await auth_db.update_authorization_flow_status(code1, FlowStatus.DONE)
98-
await auth_db.update_authorization_flow_status(code2, FlowStatus.ERROR)
104+
await auth_db.update_authorization_flow_status(code3, FlowStatus.DONE)
105+
await auth_db.update_authorization_flow_status(code4, FlowStatus.ERROR)
99106

100107
# Check the number of deleted authorization flow (should be 0)
101108
async with auth_db as auth_db:
102109
deleted_auth = await auth_db.clean_expired_authorization_flows(max_retention=30)
103110
assert deleted_auth == 0
104111

105-
# Check the number of deleted authorization flow (should be 2)
112+
# Check the number of deleted authorization flow (should be 4: 1 PENDING, 1 READY, 1 DONE, 1 ERROR)
106113
async with auth_db as auth_db:
107114
deleted_auth = await auth_db.clean_expired_authorization_flows(max_retention=0)
108-
assert deleted_auth == 2
115+
assert deleted_auth == 4
109116

110117
# Check the number of deleted authorization flow (should be 0 because there is nothing left to delete)
111118
async with auth_db as auth_db:

diracx-db/tests/auth/test_device_flow.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,26 +144,28 @@ async def test_device_flow_insert_id_token(auth_db: AuthDB):
144144
async def test_clean_device_flows(auth_db: AuthDB):
145145
# Insert two device flows
146146
async with auth_db as auth_db:
147-
user_code1, device_code1 = await auth_db.insert_device_flow(
148-
"client_id", "scope"
149-
)
150-
user_code2, device_code2 = await auth_db.insert_device_flow(
151-
"client_id", "scope"
152-
)
147+
_, _ = await auth_db.insert_device_flow("client_id1", "scope1")
148+
user_code2, _ = await auth_db.insert_device_flow("client_id2", "scope2")
149+
_, device_code3 = await auth_db.insert_device_flow("client_id3", "scope3")
150+
_, device_code4 = await auth_db.insert_device_flow("client_id4", "scope4")
153151

154152
async with auth_db as auth_db:
155-
await auth_db.update_device_flow_status(device_code1, FlowStatus.DONE)
156-
await auth_db.update_device_flow_status(device_code2, FlowStatus.ERROR)
153+
await auth_db.device_flow_validate_user_code(user_code2, MAX_VALIDITY)
154+
await auth_db.device_flow_insert_id_token(
155+
user_code2, {"sub": "myIdToken"}, MAX_VALIDITY
156+
)
157+
await auth_db.update_device_flow_status(device_code3, FlowStatus.DONE)
158+
await auth_db.update_device_flow_status(device_code4, FlowStatus.ERROR)
157159

158160
# Check the number of deleted device flows (should be 0)
159161
async with auth_db as auth_db:
160162
deleted_device = await auth_db.clean_expired_device_flows(max_retention=30)
161163
assert deleted_device == 0
162164

163-
# Check the number of deleted device flows (should be 2)
165+
# Check the number of deleted device flows (should be 4: 1 PENDING, 1 READY, 1 DONE, 1 ERROR)
164166
async with auth_db as auth_db:
165167
deleted_device = await auth_db.clean_expired_device_flows(max_retention=0)
166-
assert deleted_device == 2
168+
assert deleted_device == 4
167169

168170
# Check the number of deleted device flow (should be 0 because there is nothing left to delete)
169171
async with auth_db as auth_db:

diracx-db/tests/auth/test_refresh_token.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,18 +278,20 @@ async def test_clean_refresh_tokens(auth_db: AuthDB):
278278

279279
# Check the number of deleted refresh tokens (should be 0)
280280
async with auth_db as auth_db:
281-
deleted_expired, deleted_revoked = await auth_db.clean_expired_refresh_token(
282-
max_validity=10, max_retention=30
283-
)
281+
deleted_expired = await auth_db.clean_expired_refresh_tokens(max_validity=10)
284282
assert deleted_expired == 0
283+
284+
async with auth_db as auth_db:
285+
deleted_revoked = await auth_db.clean_revoked_refresh_tokens(max_retention=30)
285286
assert deleted_revoked == 0
286287

287288
# Check the number of deleted refresh tokens (should be 1 of each)
288289
async with auth_db as auth_db:
289-
deleted_expired, deleted_revoked = await auth_db.clean_expired_refresh_token(
290-
max_validity=0, max_retention=0
291-
)
290+
deleted_expired = await auth_db.clean_expired_refresh_tokens(max_validity=0)
292291
assert deleted_expired == 1
292+
293+
async with auth_db as auth_db:
294+
deleted_revoked = await auth_db.clean_revoked_refresh_tokens(max_retention=0)
293295
assert deleted_revoked == 1
294296

295297
# Get all refresh tokens (Admin)

diracx-logic/src/diracx/logic/__main__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,15 @@ async def delete_jwk(args):
9393
async def cleanup_authdb(args):
9494
"""Delete expired tokens and flows from the AuthDB."""
9595
logger.info("Deleting expired tokens and flows")
96+
import os
97+
9698
from diracx.core.settings import AuthSettings
9799
from diracx.db.sql import AuthDB
98100
from diracx.logic.auth.management import cleanup_expired_data
99101

100102
settings = AuthSettings()
101-
db = AuthDB(args.db_url)
103+
db_url = os.environ["DIRACX_DB_URL_AUTHDB"]
104+
db = AuthDB(db_url)
102105
async with db.engine_context():
103106
async with db:
104107
await cleanup_expired_data(db, settings)
@@ -137,9 +140,6 @@ def parse_args():
137140
cleanup_authdb_parser = subparsers.add_parser(
138141
"cleanup-authdb", help="Delete expired tokens and flows from the AuthDB"
139142
)
140-
cleanup_authdb_parser.add_argument(
141-
"--db-url", required=True, help="URL to the AuthDB"
142-
)
143143
cleanup_authdb_parser.set_defaults(func=cleanup_authdb)
144144

145145
args = parser.parse_args()

diracx-logic/src/diracx/logic/auth/management.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,22 @@ async def revoke_refresh_token_by_refresh_token(
6565

6666
async def cleanup_expired_data(auth_db: AuthDB, settings: AuthSettings) -> None:
6767
"""Remove expired data from the auth database."""
68-
expired_tokens, revoked_tokens = await auth_db.clean_expired_refresh_token(
68+
expired_tokens = await auth_db.clean_expired_refresh_tokens(
6969
max_validity=settings.refresh_token_expire_minutes,
70-
max_retention=settings.revoked_refresh_token_retention_days,
7170
)
72-
logger.info(
73-
f"Deleted {expired_tokens} expired and {revoked_tokens} revoked refresh tokens"
71+
logger.info("Deleted %d expired refresh tokens", expired_tokens)
72+
73+
revoked_tokens = await auth_db.clean_revoked_refresh_tokens(
74+
max_retention=settings.revoked_refresh_token_retention_minutes,
7475
)
76+
logger.info("Deleted %d revoked refresh tokens", revoked_tokens)
7577

7678
auth = await auth_db.clean_expired_authorization_flows(
7779
max_retention=settings.completed_flow_retention_minutes,
7880
)
79-
logger.info(f"Deleted {auth} expired authorization flows")
81+
logger.info("Deleted %d expired authorization flows", auth)
8082

8183
device = await auth_db.clean_expired_device_flows(
8284
max_retention=settings.completed_flow_retention_minutes,
8385
)
84-
logger.info(f"Deleted {device} expired device flows")
86+
logger.info("Deleted %d expired device flows", device)

docs/admin/reference/env-variables.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,14 @@ Expiration time in minutes for refresh tokens.
105105
The maximum lifetime of refresh tokens before they must be re-issued
106106
through a new authentication flow. Default: 60 minutes.
107107

108-
### `DIRACX_SERVICE_AUTH_REVOKED_REFRESH_TOKEN_RETENTION_DAYS`
108+
### `DIRACX_SERVICE_AUTH_REVOKED_REFRESH_TOKEN_RETENTION_MINUTES`
109109

110-
*Optional*, default value: `30`
110+
*Optional*, default value: `43200`
111111

112-
Retention time in days for revoked refresh tokens.
112+
Retention time in minutes for revoked refresh tokens.
113113

114114
The maximum retention time of refresh tokens after being
115-
revoked and before they are deleted. Default: 30 days.
115+
revoked and before they are deleted. Default: 43200 minutes (30 days).
116116

117117
### `DIRACX_SERVICE_AUTH_AVAILABLE_PROPERTIES`
118118

0 commit comments

Comments
 (0)