From 527f8ddcddd68a6696131754cd836dd08b500530 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 10:06:09 +0200 Subject: [PATCH 01/12] chore: updated enrich_token function --- .../src/diracx/routers/access_policies.py | 14 +++++--------- diracx-routers/src/diracx/routers/auth/token.py | 4 +--- diracx-testing/src/diracx/testing/utils.py | 9 +++------ 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/diracx-routers/src/diracx/routers/access_policies.py b/diracx-routers/src/diracx/routers/access_policies.py index c2e498d74..ed82aa489 100644 --- a/diracx-routers/src/diracx/routers/access_policies.py +++ b/diracx-routers/src/diracx/routers/access_policies.py @@ -33,7 +33,6 @@ from diracx.core.extensions import DiracEntryPoint, select_from_extension from diracx.core.models import ( AccessTokenPayload, - RefreshTokenPayload, ) from diracx.core.settings import DevelopmentSettings from diracx.routers.dependencies import auto_inject @@ -90,18 +89,15 @@ async def policy(policy_name: str, user_info: AuthorizedUserInfo, /): return @staticmethod - def enrich_tokens( - access_payload: AccessTokenPayload, refresh_payload: RefreshTokenPayload | None - ) -> tuple[dict, dict]: - """Add content to access or refresh payload when issuing a token. + def enrich_tokens(access_payload: AccessTokenPayload) -> dict: + """Add content to access payload when issuing a token. - Content can be whatever is desired inside the access or refresh payload. + Content can be whatever is desired inside the access payload. :param access_payload: access token payload - :param refresh_payload: refresh token payload - :returns: extra content for both payload + :returns: extra content for the access payload """ - return {}, {} + return {} @auto_inject diff --git a/diracx-routers/src/diracx/routers/auth/token.py b/diracx-routers/src/diracx/routers/auth/token.py index 3b3f7942d..5c23ad620 100644 --- a/diracx-routers/src/diracx/routers/auth/token.py +++ b/diracx-routers/src/diracx/routers/auth/token.py @@ -55,9 +55,7 @@ async def mint_token( dirac_access_policies = {} dirac_refresh_policies = {} for policy_name, policy in all_access_policies.items(): - access_extra, refresh_extra = policy.enrich_tokens( - access_payload, refresh_payload - ) + access_extra, refresh_extra = policy.enrich_tokens(access_payload) if access_extra: dirac_access_policies[policy_name] = access_extra if refresh_extra: diff --git a/diracx-testing/src/diracx/testing/utils.py b/diracx-testing/src/diracx/testing/utils.py index 9816457f7..ab2088926 100644 --- a/diracx-testing/src/diracx/testing/utils.py +++ b/diracx-testing/src/diracx/testing/utils.py @@ -46,8 +46,8 @@ from uuid_utils import uuid7 from diracx.core.extensions import DiracEntryPoint -from diracx.core.models import AccessTokenPayload, RefreshTokenPayload from diracx.core.settings import FactorySettings +from diracx.core.models import AccessTokenPayload if TYPE_CHECKING: from diracx.core.settings import ( @@ -199,11 +199,8 @@ async def policy( pass @staticmethod - def enrich_tokens( - access_payload: AccessTokenPayload, - refresh_payload: RefreshTokenPayload | None, - ): - return {"PolicySpecific": "OpenAccessForTest"}, {} + def enrich_tokens(access_payload: AccessTokenPayload): + return {"PolicySpecific": "OpenAccessForTest"} enabled_systems = { e.name for e in select_from_extension(group=DiracEntryPoint.SERVICES) From 9330f321d1e27f3b045f05ec4fbe9768f1b39eb8 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 10:11:17 +0200 Subject: [PATCH 02/12] chore: moved dirac_policies to each Token --- diracx-core/src/diracx/core/models/auth.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/diracx-core/src/diracx/core/models/auth.py b/diracx-core/src/diracx/core/models/auth.py index 7dcc220b4..3a04f4ccc 100644 --- a/diracx-core/src/diracx/core/models/auth.py +++ b/diracx-core/src/diracx/core/models/auth.py @@ -59,7 +59,6 @@ class OpenIDConfiguration(TypedDict): class TokenPayload(BaseModel): jti: str exp: UTCDatetime - dirac_policies: dict class TokenResponse(BaseModel): @@ -77,10 +76,13 @@ class AccessTokenPayload(TokenPayload): dirac_properties: list[str] preferred_username: str dirac_group: str + dirac_policies: dict class RefreshTokenPayload(TokenPayload): legacy_exchange: bool + # TODO: this should be removed later (https://github.com/DIRACGrid/diracx/issues/524#issuecomment-4395561176) + dirac_policies: dict | None class SupportInfo(TypedDict): From a5502a97868638e845286e79f174b7968c6039c9 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 10:11:44 +0200 Subject: [PATCH 03/12] chore: removed uses of enrich_token for RefreshTokens --- diracx-routers/src/diracx/routers/auth/token.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/diracx-routers/src/diracx/routers/auth/token.py b/diracx-routers/src/diracx/routers/auth/token.py index 5c23ad620..b220ffb21 100644 --- a/diracx-routers/src/diracx/routers/auth/token.py +++ b/diracx-routers/src/diracx/routers/auth/token.py @@ -53,13 +53,10 @@ async def mint_token( # Enrich the token with policy specific content dirac_access_policies = {} - dirac_refresh_policies = {} for policy_name, policy in all_access_policies.items(): - access_extra, refresh_extra = policy.enrich_tokens(access_payload) + access_extra = policy.enrich_tokens(access_payload) if access_extra: dirac_access_policies[policy_name] = access_extra - if refresh_extra: - dirac_refresh_policies[policy_name] = refresh_extra # Create the access token access_payload.dirac_policies = dirac_access_policies @@ -67,7 +64,6 @@ async def mint_token( # Create the refresh token if refresh_payload: - refresh_payload.dirac_policies = dirac_refresh_policies refresh_token = create_token(refresh_payload, settings) elif existing_refresh_token: refresh_token = existing_refresh_token From 5461454f9f9a8cc38c62e9a1b44d3e220e29948a Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 10:13:49 +0200 Subject: [PATCH 04/12] chore: removed dirac_policies uses from RefreshTokenPayload --- diracx-client/tests/test_auth.py | 1 - diracx-core/src/diracx/core/models/auth.py | 2 +- diracx-logic/src/diracx/logic/auth/token.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/diracx-client/tests/test_auth.py b/diracx-client/tests/test_auth.py index 367295425..2b705450c 100644 --- a/diracx-client/tests/test_auth.py +++ b/diracx-client/tests/test_auth.py @@ -17,7 +17,6 @@ "jti": "f0706e0a-af1e-4538-9f1f-7b9620783cba", "exp": int((datetime.now(tz=timezone.utc) + timedelta(days=1)).timestamp()), "legacy_exchange": False, - "dirac_policies": {}, } TOKEN_RESPONSE_DICT = { diff --git a/diracx-core/src/diracx/core/models/auth.py b/diracx-core/src/diracx/core/models/auth.py index 3a04f4ccc..b33ca1338 100644 --- a/diracx-core/src/diracx/core/models/auth.py +++ b/diracx-core/src/diracx/core/models/auth.py @@ -82,7 +82,7 @@ class AccessTokenPayload(TokenPayload): class RefreshTokenPayload(TokenPayload): legacy_exchange: bool # TODO: this should be removed later (https://github.com/DIRACGrid/diracx/issues/524#issuecomment-4395561176) - dirac_policies: dict | None + dirac_policies: dict | None = None class SupportInfo(TypedDict): diff --git a/diracx-logic/src/diracx/logic/auth/token.py b/diracx-logic/src/diracx/logic/auth/token.py index c9b0a7e9c..c5f70e9a1 100644 --- a/diracx-logic/src/diracx/logic/auth/token.py +++ b/diracx-logic/src/diracx/logic/auth/token.py @@ -338,7 +338,6 @@ async def exchange_token( # legacy_exchange is used to indicate that the original refresh token # was obtained from the legacy_exchange endpoint legacy_exchange=legacy_exchange, - dirac_policies={}, ) # Generate access token payload From 0264f330a97aca7afd63f736e2579229f75dbf35 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 10:55:32 +0200 Subject: [PATCH 05/12] chore: added Policies to DB and updated insert_token --- diracx-db/src/diracx/db/sql/auth/db.py | 10 +++------ diracx-db/src/diracx/db/sql/auth/schema.py | 3 +++ diracx-db/tests/auth/test_refresh_token.py | 25 +++++++++------------ diracx-logic/src/diracx/logic/auth/token.py | 11 ++++----- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/diracx-db/src/diracx/db/sql/auth/db.py b/diracx-db/src/diracx/db/sql/auth/db.py index a35a0ad19..d87283754 100644 --- a/diracx-db/src/diracx/db/sql/auth/db.py +++ b/diracx-db/src/diracx/db/sql/auth/db.py @@ -5,6 +5,7 @@ import secrets from datetime import UTC, datetime from itertools import pairwise +from typing import Any from dateutil.relativedelta import relativedelta from dateutil.rrule import MONTHLY, rrule @@ -324,10 +325,7 @@ async def update_authorization_flow_status( ) async def insert_refresh_token( - self, - jti: UUID, - subject: str, - scope: str, + self, jti: UUID, subject: str, scope: str, policies: dict[str, Any] ) -> None: """Insert a refresh token in the DB. @@ -335,9 +333,7 @@ async def insert_refresh_token( """ # Insert values into the DB stmt = insert(RefreshTokens).values( - jti=str(jti), - sub=subject, - scope=scope, + jti=str(jti), sub=subject, scope=scope, policies=policies ) await self.conn.execute(stmt) diff --git a/diracx-db/src/diracx/db/sql/auth/schema.py b/diracx-db/src/diracx/db/sql/auth/schema.py index 0c7554318..54a917d9d 100644 --- a/diracx-db/src/diracx/db/sql/auth/schema.py +++ b/diracx-db/src/diracx/db/sql/auth/schema.py @@ -119,5 +119,8 @@ class RefreshTokens(Base): # User attributes bound to the refresh token sub: Mapped[str] = mapped_column("Sub", String(256), index=True) + policies: Mapped[dict[str, Any] | None] = mapped_column( + "Policies", nullable=True, default=None + ) __table_args__ = (Index("index_status_sub", status, sub),) diff --git a/diracx-db/tests/auth/test_refresh_token.py b/diracx-db/tests/auth/test_refresh_token.py index 28d6dfe9d..498db60b4 100644 --- a/diracx-db/tests/auth/test_refresh_token.py +++ b/diracx-db/tests/auth/test_refresh_token.py @@ -25,6 +25,7 @@ async def test_insert(auth_db: AuthDB): jti1, "subject", "vo:lhcb property:NormalUser", + {"PolicySpecific": "OpenRefreshForTest"}, ) # Insert a second refresh token @@ -34,6 +35,7 @@ async def test_insert(auth_db: AuthDB): jti2, "subject", "vo:lhcb property:NormalUser", + {"PolicySpecific": "OpenRefreshForTest"}, ) # Make sure they don't have the same JWT ID @@ -46,6 +48,7 @@ async def test_get(auth_db: AuthDB): refresh_token_details = { "sub": "12345", "scope": "vo:lhcb property:NormalUser", + "policies": {"PolicySpecific": "OpenRefreshForTest"}, } # Insert refresh token details @@ -55,6 +58,7 @@ async def test_get(auth_db: AuthDB): jti, refresh_token_details["sub"], refresh_token_details["scope"], + refresh_token_details["policies"], ) # Enrich the dict with the generated refresh token attributes @@ -63,6 +67,7 @@ async def test_get(auth_db: AuthDB): "Scope": refresh_token_details["scope"], "JTI": jti, "Status": RefreshTokenStatus.CREATED, + "Policies": refresh_token_details["policies"], } # Get refresh token details @@ -90,9 +95,7 @@ async def test_get_user_refresh_tokens(auth_db: AuthDB): async with auth_db as auth_db: for sub in subjects: await auth_db.insert_refresh_token( - uuid7(), - sub, - "scope", + uuid7(), sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} ) # Get the refresh tokens of each user @@ -117,9 +120,7 @@ async def test_revoke(auth_db: AuthDB): async with auth_db as auth_db: jti = uuid7() await auth_db.insert_refresh_token( - jti, - "subject", - "scope", + jti, "subject", "scope", {"PolicySpecific": "OpenRefreshForTest"} ) # Revoke the token @@ -146,9 +147,7 @@ async def test_revoke_user_refresh_tokens(auth_db: AuthDB): async with auth_db as auth_db: for sub in subjects: await auth_db.insert_refresh_token( - uuid7(), - sub, - "scope", + uuid7(), sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} ) # Revoke the tokens of sub1 @@ -191,9 +190,7 @@ async def test_revoke_and_get_user_refresh_tokens(auth_db: AuthDB): for _ in range(nb_tokens): jti = uuid7() await auth_db.insert_refresh_token( - jti, - sub, - "scope", + jti, sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} ) jtis.append(jti) @@ -239,9 +236,7 @@ async def test_get_refresh_tokens(auth_db: AuthDB): async with auth_db as auth_db: for sub in subjects: await auth_db.insert_refresh_token( - uuid7(), - sub, - "scope", + uuid7(), sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} ) # Get all refresh tokens (Admin) diff --git a/diracx-logic/src/diracx/logic/auth/token.py b/diracx-logic/src/diracx/logic/auth/token.py index c5f70e9a1..da7fc456e 100644 --- a/diracx-logic/src/diracx/logic/auth/token.py +++ b/diracx-logic/src/diracx/logic/auth/token.py @@ -6,7 +6,7 @@ import hashlib import re from datetime import datetime, timedelta, timezone -from typing import cast +from typing import Any, cast from joserfc import jwt from joserfc.jwt import Claims @@ -324,6 +324,7 @@ async def exchange_token( auth_db=auth_db, subject=sub, scope=scope, + policies={}, # TODO ) # Generate refresh token payload @@ -390,9 +391,7 @@ def _sign_token_payload(claims: dict, settings: AuthSettings) -> str: async def insert_refresh_token( - auth_db: AuthDB, - subject: str, - scope: str, + auth_db: AuthDB, subject: str, scope: str, policies: dict[str, Any] ) -> UUID: """Insert a refresh token into the database and return the JWT ID.""" # Generate a JWT ID @@ -400,9 +399,7 @@ async def insert_refresh_token( # Insert the refresh token into the DB await auth_db.insert_refresh_token( - jti=jti, - subject=subject, - scope=scope, + jti=jti, subject=subject, scope=scope, policies=policies ) return jti From f052a79c7b3c9b5fb0ca5c2732d292c2027ac23f Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 11:03:02 +0200 Subject: [PATCH 06/12] chore: added Policies to AccessToken from RefreshToken DB values --- diracx-logic/src/diracx/logic/auth/token.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/diracx-logic/src/diracx/logic/auth/token.py b/diracx-logic/src/diracx/logic/auth/token.py index da7fc456e..7fd27079e 100644 --- a/diracx-logic/src/diracx/logic/auth/token.py +++ b/diracx-logic/src/diracx/logic/auth/token.py @@ -55,6 +55,7 @@ async def get_oidc_token( legacy_exchange = False include_refresh_token = True refresh_token_expire_minutes = None + policies = None if grant_type == GrantType.device_code: assert device_code is not None @@ -77,6 +78,7 @@ async def get_oidc_token( legacy_exchange, refresh_token_expire_minutes, include_refresh_token, + policies, ) = await get_oidc_token_info_from_refresh_flow( refresh_token, auth_db, settings ) @@ -94,6 +96,7 @@ async def get_oidc_token( legacy_exchange=legacy_exchange, refresh_token_expire_minutes=refresh_token_expire_minutes, include_refresh_token=include_refresh_token, + policies=policies, ) @@ -163,7 +166,7 @@ async def get_oidc_token_info_from_authorization_flow( async def get_oidc_token_info_from_refresh_flow( refresh_token: str, auth_db: AuthDB, settings: AuthSettings -) -> tuple[dict, str, bool, float, bool]: +) -> tuple[dict, str, bool, float, bool, dict]: """Get OIDC token information from the refresh token DB and check few parameters before returning it.""" # Decode the refresh token to get the JWT ID jti, exp, legacy_exchange = await verify_dirac_refresh_token( @@ -216,12 +219,14 @@ async def get_oidc_token_info_from_refresh_flow( "sub": sub.split(":", 1)[1], } scope = refresh_token_attributes["Scope"] + policies = refresh_token_attributes["Policies"] return ( oidc_token_info, scope, legacy_exchange, remaining_minutes, include_refresh_token, + policies, ) @@ -267,6 +272,7 @@ async def perform_legacy_exchange( available_properties, refresh_token_expire_minutes=expires_minutes, legacy_exchange=True, + policies=None, ) @@ -281,6 +287,7 @@ async def exchange_token( refresh_token_expire_minutes: float | None = None, legacy_exchange: bool = False, include_refresh_token: bool = True, + policies: dict[str, Any] | None = None, ) -> tuple[AccessTokenPayload, RefreshTokenPayload | None]: """Exchange the OIDC token for a DIRAC generated access token.""" # Extract dirac attributes from the OIDC scope @@ -324,7 +331,7 @@ async def exchange_token( auth_db=auth_db, subject=sub, scope=scope, - policies={}, # TODO + policies=policies or {}, ) # Generate refresh token payload @@ -357,7 +364,7 @@ async def exchange_token( preferred_username=preferred_username, dirac_group=dirac_group, exp=access_exp, - dirac_policies={}, + dirac_policies=policies or {}, ) return access_payload, refresh_payload From 5dacd5063a604c2982bbc7fe3b58a89d1bf91321 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 11:32:01 +0200 Subject: [PATCH 07/12] chore: added enrich_token to exchange_token --- diracx-logic/src/diracx/logic/auth/token.py | 16 +++++++++++++- .../src/diracx/routers/auth/token.py | 22 +++++-------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/diracx-logic/src/diracx/logic/auth/token.py b/diracx-logic/src/diracx/logic/auth/token.py index 7fd27079e..bb4eb51d7 100644 --- a/diracx-logic/src/diracx/logic/auth/token.py +++ b/diracx-logic/src/diracx/logic/auth/token.py @@ -30,6 +30,7 @@ from diracx.db.sql.auth.schema import FlowStatus, RefreshTokenStatus from diracx.db.sql.utils import uuid7_to_datetime from diracx.db.sql.utils.functions import substract_date +from diracx.routers.access_policies import BaseAccessPolicy from .utils import ( get_allowed_user_properties, @@ -45,6 +46,7 @@ async def get_oidc_token( config: Config, settings: AuthSettings, available_properties: set[SecurityProperty], + all_access_policies: dict[str, BaseAccessPolicy], device_code: str | None = None, code: str | None = None, redirect_uri: str | None = None, @@ -93,6 +95,7 @@ async def get_oidc_token( config, settings, available_properties, + all_access_policies, legacy_exchange=legacy_exchange, refresh_token_expire_minutes=refresh_token_expire_minutes, include_refresh_token=include_refresh_token, @@ -245,6 +248,7 @@ async def perform_legacy_exchange( available_properties: set[SecurityProperty], settings: AuthSettings, config: Config, + all_access_policies: dict[str, BaseAccessPolicy], expires_minutes: float | None = None, ) -> tuple[AccessTokenPayload, RefreshTokenPayload | None]: """Endpoint used by legacy DIRAC to mint tokens for proxy -> token exchange.""" @@ -270,6 +274,7 @@ async def perform_legacy_exchange( config, settings, available_properties, + all_access_policies, refresh_token_expire_minutes=expires_minutes, legacy_exchange=True, policies=None, @@ -283,6 +288,7 @@ async def exchange_token( config: Config, settings: AuthSettings, available_properties: set[SecurityProperty], + all_access_policies: dict[str, BaseAccessPolicy], *, refresh_token_expire_minutes: float | None = None, legacy_exchange: bool = False, @@ -364,9 +370,17 @@ async def exchange_token( preferred_username=preferred_username, dirac_group=dirac_group, exp=access_exp, - dirac_policies=policies or {}, + dirac_policies={}, ) + # Enrich the token with policy specific content + if policies is not None: + access_payload.dirac_policies = policies + else: + for policy_name, policy in all_access_policies.items(): + if access_extra := policy.enrich_tokens(access_payload): + access_payload.dirac_policies[policy_name] = access_extra + return access_payload, refresh_payload diff --git a/diracx-routers/src/diracx/routers/auth/token.py b/diracx-routers/src/diracx/routers/auth/token.py index b220ffb21..1ff1d2fac 100644 --- a/diracx-routers/src/diracx/routers/auth/token.py +++ b/diracx-routers/src/diracx/routers/auth/token.py @@ -41,25 +41,15 @@ async def mint_token( access_payload: AccessTokenPayload, refresh_payload: RefreshTokenPayload | None, existing_refresh_token: str | None, - all_access_policies: dict[str, BaseAccessPolicy], settings: AuthSettings, ) -> TokenResponse: - """Enrich the token with policy specific content and mint it.""" + """Mint the token.""" if not refresh_payload and not existing_refresh_token: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Refresh token is not set and no refresh token was provided", ) - # Enrich the token with policy specific content - dirac_access_policies = {} - for policy_name, policy in all_access_policies.items(): - access_extra = policy.enrich_tokens(access_payload) - if access_extra: - dirac_access_policies[policy_name] = access_extra - - # Create the access token - access_payload.dirac_policies = dirac_access_policies access_token = create_token(access_payload, settings) # Create the refresh token @@ -126,6 +116,7 @@ async def get_oidc_token( config, settings, available_properties, + all_access_policies, device_code=device_code, code=code, redirect_uri=redirect_uri, @@ -156,9 +147,7 @@ async def get_oidc_token( status_code=HTTPStatus.FORBIDDEN, detail=str(e), ) from e - return await mint_token( - access_payload, refresh_payload, refresh_token, all_access_policies, settings - ) + return await mint_token(access_payload, refresh_payload, refresh_token, settings) BASE_64_URL_SAFE_PATTERN = ( @@ -203,6 +192,7 @@ async def perform_legacy_exchange( available_properties=available_properties, settings=settings, config=config, + all_access_policies=all_access_policies, expires_minutes=expires_minutes, ) except ValueError as e: @@ -221,6 +211,4 @@ async def perform_legacy_exchange( status_code=HTTPStatus.FORBIDDEN, detail=str(e), ) from e - return await mint_token( - access_payload, refresh_payload, None, all_access_policies, settings - ) + return await mint_token(access_payload, refresh_payload, None, settings) From 5b15e115640d7277ac8b69546ff841cd9ea3ae5e Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 11:35:52 +0200 Subject: [PATCH 08/12] fix: added policies from AccessPayload in the DB --- diracx-logic/src/diracx/logic/auth/token.py | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/diracx-logic/src/diracx/logic/auth/token.py b/diracx-logic/src/diracx/logic/auth/token.py index bb4eb51d7..bf00f707a 100644 --- a/diracx-logic/src/diracx/logic/auth/token.py +++ b/diracx-logic/src/diracx/logic/auth/token.py @@ -329,31 +329,6 @@ async def exchange_token( # Merge the VO with the subject to get a unique DIRAC sub sub = f"{vo}:{sub}" - refresh_payload: RefreshTokenPayload | None = None - if include_refresh_token: - # Insert the refresh token with user details into the RefreshTokens table - # User details are needed to regenerate access tokens later - refresh_jti = await insert_refresh_token( - auth_db=auth_db, - subject=sub, - scope=scope, - policies=policies or {}, - ) - - # Generate refresh token payload - if refresh_token_expire_minutes is None: - refresh_token_expire_minutes = settings.refresh_token_expire_minutes - refresh_exp = uuid7_to_datetime(refresh_jti) + timedelta( - minutes=refresh_token_expire_minutes - ) - refresh_payload = RefreshTokenPayload( - jti=str(refresh_jti), - exp=refresh_exp, - # legacy_exchange is used to indicate that the original refresh token - # was obtained from the legacy_exchange endpoint - legacy_exchange=legacy_exchange, - ) - # Generate access token payload # For now, the access token is only used to access DIRAC services, # therefore, the audience is not set and checked @@ -381,6 +356,31 @@ async def exchange_token( if access_extra := policy.enrich_tokens(access_payload): access_payload.dirac_policies[policy_name] = access_extra + refresh_payload: RefreshTokenPayload | None = None + if include_refresh_token: + # Insert the refresh token with user details into the RefreshTokens table + # User details are needed to regenerate access tokens later + refresh_jti = await insert_refresh_token( + auth_db=auth_db, + subject=sub, + scope=scope, + policies=access_payload.dirac_policies, + ) + + # Generate refresh token payload + if refresh_token_expire_minutes is None: + refresh_token_expire_minutes = settings.refresh_token_expire_minutes + refresh_exp = uuid7_to_datetime(refresh_jti) + timedelta( + minutes=refresh_token_expire_minutes + ) + refresh_payload = RefreshTokenPayload( + jti=str(refresh_jti), + exp=refresh_exp, + # legacy_exchange is used to indicate that the original refresh token + # was obtained from the legacy_exchange endpoint + legacy_exchange=legacy_exchange, + ) + return access_payload, refresh_payload From 0bb96dae24f0a4391c57856e165f121b93751f10 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 12:50:07 +0200 Subject: [PATCH 09/12] fix: removed routers imports in logic --- diracx-logic/src/diracx/logic/auth/token.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/diracx-logic/src/diracx/logic/auth/token.py b/diracx-logic/src/diracx/logic/auth/token.py index bf00f707a..4ac48c1e6 100644 --- a/diracx-logic/src/diracx/logic/auth/token.py +++ b/diracx-logic/src/diracx/logic/auth/token.py @@ -30,7 +30,6 @@ from diracx.db.sql.auth.schema import FlowStatus, RefreshTokenStatus from diracx.db.sql.utils import uuid7_to_datetime from diracx.db.sql.utils.functions import substract_date -from diracx.routers.access_policies import BaseAccessPolicy from .utils import ( get_allowed_user_properties, @@ -46,7 +45,7 @@ async def get_oidc_token( config: Config, settings: AuthSettings, available_properties: set[SecurityProperty], - all_access_policies: dict[str, BaseAccessPolicy], + all_access_policies: dict[str, Any], device_code: str | None = None, code: str | None = None, redirect_uri: str | None = None, @@ -248,7 +247,7 @@ async def perform_legacy_exchange( available_properties: set[SecurityProperty], settings: AuthSettings, config: Config, - all_access_policies: dict[str, BaseAccessPolicy], + all_access_policies: dict[str, Any], expires_minutes: float | None = None, ) -> tuple[AccessTokenPayload, RefreshTokenPayload | None]: """Endpoint used by legacy DIRAC to mint tokens for proxy -> token exchange.""" @@ -288,7 +287,7 @@ async def exchange_token( config: Config, settings: AuthSettings, available_properties: set[SecurityProperty], - all_access_policies: dict[str, BaseAccessPolicy], + all_access_policies: dict[str, Any], *, refresh_token_expire_minutes: float | None = None, legacy_exchange: bool = False, From fafc10b350fd7432fb2d395179d2a5438cc55026 Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 13:40:20 +0200 Subject: [PATCH 10/12] test: added tests --- .../tests/auth/test_legacy_exchange.py | 2 + diracx-routers/tests/auth/test_standard.py | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/diracx-routers/tests/auth/test_legacy_exchange.py b/diracx-routers/tests/auth/test_legacy_exchange.py index 6f28067e7..e01084b65 100644 --- a/diracx-routers/tests/auth/test_legacy_exchange.py +++ b/diracx-routers/tests/auth/test_legacy_exchange.py @@ -101,12 +101,14 @@ async def test_valid(test_client, legacy_credentials, expires_seconds): ) assert r.status_code == 200 user_info = r.json() + assert user_info["sub"] == "lhcb:b824d4dc-1f9d-4ee8-8df5-c0ae55d46041" assert user_info["vo"] == "lhcb" assert user_info["dirac_group"] == "lhcb_user" assert sorted(user_info["properties"]) == sorted( ["PrivateLimitedDelegation", "NormalUser"] ) + assert user_info["policies"] async def test_refresh_token(test_client, legacy_credentials): diff --git a/diracx-routers/tests/auth/test_standard.py b/diracx-routers/tests/auth/test_standard.py index b70f856a0..342de96ff 100644 --- a/diracx-routers/tests/auth/test_standard.py +++ b/diracx-routers/tests/auth/test_standard.py @@ -1490,3 +1490,54 @@ def fake_iam_server_error(*args, **kwargs): r = test_client.get("/api/auth/device", params={"user_code": user_code}) assert r.status_code == 502 assert r.json()["detail"] == "IAM error" + + +async def test_access_token_contains_policies(test_client, auth_httpx_mock: HTTPXMock): + """Test that dirac_policies exists in the access token after login.""" + # Get access token + access_token = _get_tokens(test_client)["access_token"] + + # Get user infos + r = test_client.get( + "/api/auth/userinfo", headers={"Authorization": f"Bearer {access_token}"} + ) + assert r.status_code == 200 + assert r.json()["policies"] + + +async def test_access_token_contains_policies_after_refresh( + test_client, auth_httpx_mock: HTTPXMock +): + """Test that dirac_policies are persisted in the access token after refreshing the token.""" + # Get tokens + tokens = _get_tokens(test_client) + initial_access_token = tokens["access_token"] + initial_refresh_token = tokens["refresh_token"] + + # Retrieve policies + r = test_client.get( + "/api/auth/userinfo", + headers={"Authorization": f"Bearer {initial_access_token}"}, + ) + assert r.status_code == 200 + initial_policies = r.json()["policies"] + assert initial_policies + + # Refresh the token + r = test_client.post( + "/api/auth/token", + data={ + "grant_type": "refresh_token", + "refresh_token": initial_refresh_token, + "client_id": DIRAC_CLIENT_ID, + }, + ) + assert r.status_code == 200 + new_access_token = r.json()["access_token"] + + # Check if policies are the same + r = test_client.get( + "/api/auth/userinfo", headers={"Authorization": f"Bearer {new_access_token}"} + ) + assert r.status_code == 200 + assert r.json()["policies"] == initial_policies From 36de8e295e40e185379f58f6b073b583cfb7b33c Mon Sep 17 00:00:00 2001 From: Stella-Maria Renucci Date: Thu, 2 Jul 2026 13:51:40 +0200 Subject: [PATCH 11/12] fix: renamed test policies --- diracx-db/tests/auth/test_refresh_token.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/diracx-db/tests/auth/test_refresh_token.py b/diracx-db/tests/auth/test_refresh_token.py index 498db60b4..48eacb85f 100644 --- a/diracx-db/tests/auth/test_refresh_token.py +++ b/diracx-db/tests/auth/test_refresh_token.py @@ -25,7 +25,7 @@ async def test_insert(auth_db: AuthDB): jti1, "subject", "vo:lhcb property:NormalUser", - {"PolicySpecific": "OpenRefreshForTest"}, + {"PolicySpecific": "OpenAccessForTest"}, ) # Insert a second refresh token @@ -35,7 +35,7 @@ async def test_insert(auth_db: AuthDB): jti2, "subject", "vo:lhcb property:NormalUser", - {"PolicySpecific": "OpenRefreshForTest"}, + {"PolicySpecific": "OpenAccessForTest"}, ) # Make sure they don't have the same JWT ID @@ -48,7 +48,7 @@ async def test_get(auth_db: AuthDB): refresh_token_details = { "sub": "12345", "scope": "vo:lhcb property:NormalUser", - "policies": {"PolicySpecific": "OpenRefreshForTest"}, + "policies": {"PolicySpecific": "OpenAccessForTest"}, } # Insert refresh token details @@ -95,7 +95,7 @@ async def test_get_user_refresh_tokens(auth_db: AuthDB): async with auth_db as auth_db: for sub in subjects: await auth_db.insert_refresh_token( - uuid7(), sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} + uuid7(), sub, "scope", {"PolicySpecific": "OpenAccessForTest"} ) # Get the refresh tokens of each user @@ -120,7 +120,7 @@ async def test_revoke(auth_db: AuthDB): async with auth_db as auth_db: jti = uuid7() await auth_db.insert_refresh_token( - jti, "subject", "scope", {"PolicySpecific": "OpenRefreshForTest"} + jti, "subject", "scope", {"PolicySpecific": "OpenAccessForTest"} ) # Revoke the token @@ -147,7 +147,7 @@ async def test_revoke_user_refresh_tokens(auth_db: AuthDB): async with auth_db as auth_db: for sub in subjects: await auth_db.insert_refresh_token( - uuid7(), sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} + uuid7(), sub, "scope", {"PolicySpecific": "OpenAccessForTest"} ) # Revoke the tokens of sub1 @@ -190,7 +190,7 @@ async def test_revoke_and_get_user_refresh_tokens(auth_db: AuthDB): for _ in range(nb_tokens): jti = uuid7() await auth_db.insert_refresh_token( - jti, sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} + jti, sub, "scope", {"PolicySpecific": "OpenAccessForTest"} ) jtis.append(jti) @@ -236,7 +236,7 @@ async def test_get_refresh_tokens(auth_db: AuthDB): async with auth_db as auth_db: for sub in subjects: await auth_db.insert_refresh_token( - uuid7(), sub, "scope", {"PolicySpecific": "OpenRefreshForTest"} + uuid7(), sub, "scope", {"PolicySpecific": "OpenAccessForTest"} ) # Get all refresh tokens (Admin) From ca5ed0fbf5ea37aba9a1ec3c35911b95e78c1519 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:06:28 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- diracx-testing/src/diracx/testing/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diracx-testing/src/diracx/testing/utils.py b/diracx-testing/src/diracx/testing/utils.py index ab2088926..e9e9dfc87 100644 --- a/diracx-testing/src/diracx/testing/utils.py +++ b/diracx-testing/src/diracx/testing/utils.py @@ -46,8 +46,8 @@ from uuid_utils import uuid7 from diracx.core.extensions import DiracEntryPoint -from diracx.core.settings import FactorySettings from diracx.core.models import AccessTokenPayload +from diracx.core.settings import FactorySettings if TYPE_CHECKING: from diracx.core.settings import (