Loading src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py +11 −5 Changes for src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -20,7 +20,11 @@ from open_exposure_gateway.api.camara.edge_application_management.v0_1_0_alpha_1 from open_exposure_gateway.application.services.edge_application_management_service import ( EdgeApplicationManagementService, ) from open_exposure_gateway.core.authorization import require_role, require_three_legged from open_exposure_gateway.core.authorization import ( require_role, require_scope, require_three_legged, ) from open_exposure_gateway.core.exceptions import ( AbortedException, AlreadyExistsException, Loading @@ -44,11 +48,13 @@ from open_exposure_gateway.schemas.common import ErrorInfo # by this package, {apiRoot} by the deployment. BASE_PATH = "/edge-application-management/v0.1alpha1" # require_role/require_three_legged apply to every route in this router, looking each # request's requirements up in the one explicit table (core/authorization.py) -- so # authorization is enforced identically here without touching individual handlers. # require_role/require_three_legged/require_scope apply to every route in this router, # looking each request's requirements up in the one explicit table # (core/authorization.py) -- so authorization is enforced identically here without # touching individual handlers. router = APIRouter( prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged)] prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged), Depends(require_scope)], ) EdgeAppService = Annotated[EdgeApplicationManagementService, Depends(get_edge_app_service)] Loading src/open_exposure_gateway/api/camara/location_retrieval/v0_5_0/router.py +11 −5 Changes for src/open_exposure_gateway/api/camara/location_retrieval/v0_5_0/router.py: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -10,7 +10,11 @@ from open_exposure_gateway.api.camara.location_retrieval.v0_5_0.schemas import ( from open_exposure_gateway.application.services.location_retrieval_service import ( LocationRetrievalService, ) from open_exposure_gateway.core.authorization import require_role, require_three_legged from open_exposure_gateway.core.authorization import ( require_role, require_scope, require_three_legged, ) from open_exposure_gateway.core.exceptions import ( BadRequestException, DownstreamServiceException, Loading @@ -31,11 +35,13 @@ from open_exposure_gateway.schemas.common import ErrorInfo # v0.5.0), mounted bare per ADR-0020. BASE_PATH = "/location-retrieval/v0.5" # require_role/require_three_legged apply to every route in this router, looking each # request's requirements up in the one explicit table (core/authorization.py) -- so # authorization is enforced identically here without touching individual handlers. # require_role/require_three_legged/require_scope apply to every route in this router, # looking each request's requirements up in the one explicit table # (core/authorization.py) -- so authorization is enforced identically here without # touching individual handlers. router = APIRouter( prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged)] prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged), Depends(require_scope)], ) LocationService = Annotated[LocationRetrievalService, Depends(get_location_retrieval_service)] Loading src/open_exposure_gateway/api/camara/quality_on_demand/v0_10_1/router.py +11 −5 Changes for src/open_exposure_gateway/api/camara/quality_on_demand/v0_10_1/router.py: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -11,7 +11,11 @@ from open_exposure_gateway.api.camara.quality_on_demand.v0_10_1.schemas import ( from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.authorization import require_role, require_three_legged from open_exposure_gateway.core.authorization import ( require_role, require_scope, require_three_legged, ) from open_exposure_gateway.core.exceptions import ( BadRequestException, ConflictException, Loading @@ -27,11 +31,13 @@ from open_exposure_gateway.schemas.common import ErrorInfo # CAMARA base path: the spec serves at {apiRoot}/qod/v0 (wire version of v0.10.1). BASE_PATH = "/qod/v0" # require_role/require_three_legged apply to every route in this router, looking each # request's requirements up in the one explicit table (core/authorization.py) -- so # authorization is enforced identically here without touching individual handlers. # require_role/require_three_legged/require_scope apply to every route in this router, # looking each request's requirements up in the one explicit table # (core/authorization.py) -- so authorization is enforced identically here without # touching individual handlers. router = APIRouter( prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged)] prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged), Depends(require_scope)], ) QoDService = Annotated[QualityOnDemandService, Depends(get_qod_service)] Loading src/open_exposure_gateway/core/authorization.py +77 −20 Changes for src/open_exposure_gateway/core/authorization.py: 77 added lines, 20 removed lines. Original line number Diff line number Diff line from dataclasses import dataclass from typing import Annotated from typing import Annotated, Optional import structlog from fastapi import Depends, Request Loading @@ -16,6 +16,16 @@ logger = structlog.get_logger(__name__) class OperationRequirements: role: str requires_three_legged: bool # The CAMARA scope this operation's spec declares (e.g. "qod-sessions-write"), or # None where the spec declares no scope at all (e.g. QoD's qos-profiles reads). scope: Optional[str] = None # QoD's spec offers two alternative security requirements per operation -- # `oAuth2ClientCredentials` (two-legged, no scope) OR `threeLegged` (a specific # scope required). EAM/Location Retrieval instead use `openIdConnect`, a single # scheme where the scope is required regardless of flow. False here reproduces # QoD's "two-legged callers are exempt" alternative; defaults to True (always # required) to match EAM/Location Retrieval. scope_required_for_two_legged: bool = True # One explicit table per CAMARA operation, not per-handler if statements: this is what keeps Loading @@ -27,57 +37,79 @@ class OperationRequirements: OPERATION_REQUIREMENTS: dict[str, OperationRequirements] = { # Quality on Demand -- app-provider only; the spec allows a three-legged token on # get/delete/extend but never requires one, and createSession is two-legged only. "POST /qod/v0/sessions": OperationRequirements("app-provider", False), "GET /qod/v0/sessions/{sessionId}": OperationRequirements("app-provider", False), "DELETE /qod/v0/sessions/{sessionId}": OperationRequirements("app-provider", False), "POST /qod/v0/sessions/{sessionId}/extend": OperationRequirements("app-provider", False), # Scopes match qod-api.yaml's `security` block per operation: a two-legged # (oAuth2ClientCredentials) token needs no scope, a three-legged token must carry # the listed one. qos-profiles reads declare no scope at all in the spec. "POST /qod/v0/sessions": OperationRequirements( "app-provider", False, scope="qod-sessions-write", scope_required_for_two_legged=False ), "GET /qod/v0/sessions/{sessionId}": OperationRequirements( "app-provider", False, scope="qod-sessions-read", scope_required_for_two_legged=False ), "DELETE /qod/v0/sessions/{sessionId}": OperationRequirements( "app-provider", False, scope="qod-sessions-delete", scope_required_for_two_legged=False ), "POST /qod/v0/sessions/{sessionId}/extend": OperationRequirements( "app-provider", False, scope="qod-sessions-write", scope_required_for_two_legged=False ), "GET /qod/v0/qos-profiles": OperationRequirements("app-provider", False), "GET /qod/v0/qos-profiles/{name}": OperationRequirements("app-provider", False), # Edge Application Management -- app-provider manages its own apps/instances/deployments; # no end-user consent is ever involved. # no end-user consent is ever involved. Scopes match # edge-application-management-0.1.0-alpha.1.yaml's `openId` security block per # operation, which (unlike QoD) requires the scope regardless of flow. "GET /edge-application-management/v0.1alpha1/edge-cloud-zones": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:edge-cloud-zones:read" ), "GET /edge-application-management/v0.1alpha1/apps": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:read" ), "GET /edge-application-management/v0.1alpha1/apps/{appId}": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:read" ), "POST /edge-application-management/v0.1alpha1/apps": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:write" ), "DELETE /edge-application-management/v0.1alpha1/apps/{appId}": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:delete" ), "POST /edge-application-management/v0.1alpha1/appinstances": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:instances:write" ), "GET /edge-application-management/v0.1alpha1/appinstances": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:instances:read" ), "DELETE /edge-application-management/v0.1alpha1/appinstances/{appInstanceId}": ( OperationRequirements("app-provider", False) OperationRequirements( "app-provider", False, scope="edge-application-management:instances:delete" ) ), "POST /edge-application-management/v0.1alpha1/deployments": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:deployments:write" ), "GET /edge-application-management/v0.1alpha1/deployments": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:deployments:read" ), "DELETE /edge-application-management/v0.1alpha1/deployments/{appDeploymentId}": ( OperationRequirements("app-provider", False) OperationRequirements( "app-provider", False, scope="edge-application-management:deployments:delete" ) ), "PATCH /edge-application-management/v0.1alpha1/deployments/{appDeploymentId}": ( OperationRequirements("app-provider", False) OperationRequirements( "app-provider", False, scope="edge-application-management:deployments:update" ) ), "GET /edge-application-management/v0.1alpha1/clusters": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:clusters:read" ), # Location Retrieval -- locates a real end user's device, so a role-correct # app-provider service-account token is not enough: this always requires a # three-legged (user-consented) token, regardless of role/audience validity. "POST /location-retrieval/v0.5/retrieve": OperationRequirements("app-provider", True), # Scope matches location-retrieval.yaml's `openId` security block. "POST /location-retrieval/v0.5/retrieve": OperationRequirements( "app-provider", True, scope="location-retrieval:read" ), } Loading Loading @@ -135,3 +167,28 @@ def require_three_legged( message="This operation requires a three-legged (user-consented) access token" ) return caller def require_scope( request: Request, caller: Annotated[CallerContext, Depends(get_caller_context)], settings: Annotated[Settings, Depends(get_settings)], ) -> CallerContext: requirements = _requirements_for(request) if requirements.scope is None: return caller if caller.flow == TokenFlow.TWO_LEGGED and not requirements.scope_required_for_two_legged: return caller if requirements.scope not in caller.scopes: if not settings.keycloak_settings.enforcement_enabled: logger.warning( "authz_would_reject", reason="missing_scope", required_scope=requirements.scope, path=request.url.path, ) return caller raise ForbiddenException( message=f"Caller is missing the required scope: {requirements.scope}" ) return caller src/open_exposure_gateway/dependencies.py +5 −0 Changes for src/open_exposure_gateway/dependencies.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -71,6 +71,9 @@ class CallerContext: # Location Retrieval derive `device` from the token instead of requiring it # in the request body (closes architecture.md F4). subject_id: Optional[str] # The token's `scope` claim, space-separated per RFC 6749/Keycloak convention, # split into individual CAMARA operation scopes (e.g. "qod-sessions-write"). scopes: list[str] def get_app_state(request: Request) -> AppState: Loading Loading @@ -107,6 +110,7 @@ def _unverified_caller_context(x_correlator: Optional[str]) -> CallerContext: roles=[], flow=TokenFlow.TWO_LEGGED, subject_id=None, scopes=[], ) Loading Loading @@ -149,6 +153,7 @@ async def get_caller_context( roles=roles, flow=decoded.flow, subject_id=claims["sub"] if decoded.flow == TokenFlow.THREE_LEGGED else None, scopes=claims.get("scope", "").split(), ) Loading Loading
src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py +11 −5 Changes for src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -20,7 +20,11 @@ from open_exposure_gateway.api.camara.edge_application_management.v0_1_0_alpha_1 from open_exposure_gateway.application.services.edge_application_management_service import ( EdgeApplicationManagementService, ) from open_exposure_gateway.core.authorization import require_role, require_three_legged from open_exposure_gateway.core.authorization import ( require_role, require_scope, require_three_legged, ) from open_exposure_gateway.core.exceptions import ( AbortedException, AlreadyExistsException, Loading @@ -44,11 +48,13 @@ from open_exposure_gateway.schemas.common import ErrorInfo # by this package, {apiRoot} by the deployment. BASE_PATH = "/edge-application-management/v0.1alpha1" # require_role/require_three_legged apply to every route in this router, looking each # request's requirements up in the one explicit table (core/authorization.py) -- so # authorization is enforced identically here without touching individual handlers. # require_role/require_three_legged/require_scope apply to every route in this router, # looking each request's requirements up in the one explicit table # (core/authorization.py) -- so authorization is enforced identically here without # touching individual handlers. router = APIRouter( prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged)] prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged), Depends(require_scope)], ) EdgeAppService = Annotated[EdgeApplicationManagementService, Depends(get_edge_app_service)] Loading
src/open_exposure_gateway/api/camara/location_retrieval/v0_5_0/router.py +11 −5 Changes for src/open_exposure_gateway/api/camara/location_retrieval/v0_5_0/router.py: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -10,7 +10,11 @@ from open_exposure_gateway.api.camara.location_retrieval.v0_5_0.schemas import ( from open_exposure_gateway.application.services.location_retrieval_service import ( LocationRetrievalService, ) from open_exposure_gateway.core.authorization import require_role, require_three_legged from open_exposure_gateway.core.authorization import ( require_role, require_scope, require_three_legged, ) from open_exposure_gateway.core.exceptions import ( BadRequestException, DownstreamServiceException, Loading @@ -31,11 +35,13 @@ from open_exposure_gateway.schemas.common import ErrorInfo # v0.5.0), mounted bare per ADR-0020. BASE_PATH = "/location-retrieval/v0.5" # require_role/require_three_legged apply to every route in this router, looking each # request's requirements up in the one explicit table (core/authorization.py) -- so # authorization is enforced identically here without touching individual handlers. # require_role/require_three_legged/require_scope apply to every route in this router, # looking each request's requirements up in the one explicit table # (core/authorization.py) -- so authorization is enforced identically here without # touching individual handlers. router = APIRouter( prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged)] prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged), Depends(require_scope)], ) LocationService = Annotated[LocationRetrievalService, Depends(get_location_retrieval_service)] Loading
src/open_exposure_gateway/api/camara/quality_on_demand/v0_10_1/router.py +11 −5 Changes for src/open_exposure_gateway/api/camara/quality_on_demand/v0_10_1/router.py: 11 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -11,7 +11,11 @@ from open_exposure_gateway.api.camara.quality_on_demand.v0_10_1.schemas import ( from open_exposure_gateway.application.services.quality_on_demand_service import ( QualityOnDemandService, ) from open_exposure_gateway.core.authorization import require_role, require_three_legged from open_exposure_gateway.core.authorization import ( require_role, require_scope, require_three_legged, ) from open_exposure_gateway.core.exceptions import ( BadRequestException, ConflictException, Loading @@ -27,11 +31,13 @@ from open_exposure_gateway.schemas.common import ErrorInfo # CAMARA base path: the spec serves at {apiRoot}/qod/v0 (wire version of v0.10.1). BASE_PATH = "/qod/v0" # require_role/require_three_legged apply to every route in this router, looking each # request's requirements up in the one explicit table (core/authorization.py) -- so # authorization is enforced identically here without touching individual handlers. # require_role/require_three_legged/require_scope apply to every route in this router, # looking each request's requirements up in the one explicit table # (core/authorization.py) -- so authorization is enforced identically here without # touching individual handlers. router = APIRouter( prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged)] prefix=BASE_PATH, dependencies=[Depends(require_role), Depends(require_three_legged), Depends(require_scope)], ) QoDService = Annotated[QualityOnDemandService, Depends(get_qod_service)] Loading
src/open_exposure_gateway/core/authorization.py +77 −20 Changes for src/open_exposure_gateway/core/authorization.py: 77 added lines, 20 removed lines. Original line number Diff line number Diff line from dataclasses import dataclass from typing import Annotated from typing import Annotated, Optional import structlog from fastapi import Depends, Request Loading @@ -16,6 +16,16 @@ logger = structlog.get_logger(__name__) class OperationRequirements: role: str requires_three_legged: bool # The CAMARA scope this operation's spec declares (e.g. "qod-sessions-write"), or # None where the spec declares no scope at all (e.g. QoD's qos-profiles reads). scope: Optional[str] = None # QoD's spec offers two alternative security requirements per operation -- # `oAuth2ClientCredentials` (two-legged, no scope) OR `threeLegged` (a specific # scope required). EAM/Location Retrieval instead use `openIdConnect`, a single # scheme where the scope is required regardless of flow. False here reproduces # QoD's "two-legged callers are exempt" alternative; defaults to True (always # required) to match EAM/Location Retrieval. scope_required_for_two_legged: bool = True # One explicit table per CAMARA operation, not per-handler if statements: this is what keeps Loading @@ -27,57 +37,79 @@ class OperationRequirements: OPERATION_REQUIREMENTS: dict[str, OperationRequirements] = { # Quality on Demand -- app-provider only; the spec allows a three-legged token on # get/delete/extend but never requires one, and createSession is two-legged only. "POST /qod/v0/sessions": OperationRequirements("app-provider", False), "GET /qod/v0/sessions/{sessionId}": OperationRequirements("app-provider", False), "DELETE /qod/v0/sessions/{sessionId}": OperationRequirements("app-provider", False), "POST /qod/v0/sessions/{sessionId}/extend": OperationRequirements("app-provider", False), # Scopes match qod-api.yaml's `security` block per operation: a two-legged # (oAuth2ClientCredentials) token needs no scope, a three-legged token must carry # the listed one. qos-profiles reads declare no scope at all in the spec. "POST /qod/v0/sessions": OperationRequirements( "app-provider", False, scope="qod-sessions-write", scope_required_for_two_legged=False ), "GET /qod/v0/sessions/{sessionId}": OperationRequirements( "app-provider", False, scope="qod-sessions-read", scope_required_for_two_legged=False ), "DELETE /qod/v0/sessions/{sessionId}": OperationRequirements( "app-provider", False, scope="qod-sessions-delete", scope_required_for_two_legged=False ), "POST /qod/v0/sessions/{sessionId}/extend": OperationRequirements( "app-provider", False, scope="qod-sessions-write", scope_required_for_two_legged=False ), "GET /qod/v0/qos-profiles": OperationRequirements("app-provider", False), "GET /qod/v0/qos-profiles/{name}": OperationRequirements("app-provider", False), # Edge Application Management -- app-provider manages its own apps/instances/deployments; # no end-user consent is ever involved. # no end-user consent is ever involved. Scopes match # edge-application-management-0.1.0-alpha.1.yaml's `openId` security block per # operation, which (unlike QoD) requires the scope regardless of flow. "GET /edge-application-management/v0.1alpha1/edge-cloud-zones": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:edge-cloud-zones:read" ), "GET /edge-application-management/v0.1alpha1/apps": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:read" ), "GET /edge-application-management/v0.1alpha1/apps/{appId}": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:read" ), "POST /edge-application-management/v0.1alpha1/apps": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:write" ), "DELETE /edge-application-management/v0.1alpha1/apps/{appId}": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:apps:delete" ), "POST /edge-application-management/v0.1alpha1/appinstances": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:instances:write" ), "GET /edge-application-management/v0.1alpha1/appinstances": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:instances:read" ), "DELETE /edge-application-management/v0.1alpha1/appinstances/{appInstanceId}": ( OperationRequirements("app-provider", False) OperationRequirements( "app-provider", False, scope="edge-application-management:instances:delete" ) ), "POST /edge-application-management/v0.1alpha1/deployments": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:deployments:write" ), "GET /edge-application-management/v0.1alpha1/deployments": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:deployments:read" ), "DELETE /edge-application-management/v0.1alpha1/deployments/{appDeploymentId}": ( OperationRequirements("app-provider", False) OperationRequirements( "app-provider", False, scope="edge-application-management:deployments:delete" ) ), "PATCH /edge-application-management/v0.1alpha1/deployments/{appDeploymentId}": ( OperationRequirements("app-provider", False) OperationRequirements( "app-provider", False, scope="edge-application-management:deployments:update" ) ), "GET /edge-application-management/v0.1alpha1/clusters": OperationRequirements( "app-provider", False "app-provider", False, scope="edge-application-management:clusters:read" ), # Location Retrieval -- locates a real end user's device, so a role-correct # app-provider service-account token is not enough: this always requires a # three-legged (user-consented) token, regardless of role/audience validity. "POST /location-retrieval/v0.5/retrieve": OperationRequirements("app-provider", True), # Scope matches location-retrieval.yaml's `openId` security block. "POST /location-retrieval/v0.5/retrieve": OperationRequirements( "app-provider", True, scope="location-retrieval:read" ), } Loading Loading @@ -135,3 +167,28 @@ def require_three_legged( message="This operation requires a three-legged (user-consented) access token" ) return caller def require_scope( request: Request, caller: Annotated[CallerContext, Depends(get_caller_context)], settings: Annotated[Settings, Depends(get_settings)], ) -> CallerContext: requirements = _requirements_for(request) if requirements.scope is None: return caller if caller.flow == TokenFlow.TWO_LEGGED and not requirements.scope_required_for_two_legged: return caller if requirements.scope not in caller.scopes: if not settings.keycloak_settings.enforcement_enabled: logger.warning( "authz_would_reject", reason="missing_scope", required_scope=requirements.scope, path=request.url.path, ) return caller raise ForbiddenException( message=f"Caller is missing the required scope: {requirements.scope}" ) return caller
src/open_exposure_gateway/dependencies.py +5 −0 Changes for src/open_exposure_gateway/dependencies.py: 5 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -71,6 +71,9 @@ class CallerContext: # Location Retrieval derive `device` from the token instead of requiring it # in the request body (closes architecture.md F4). subject_id: Optional[str] # The token's `scope` claim, space-separated per RFC 6749/Keycloak convention, # split into individual CAMARA operation scopes (e.g. "qod-sessions-write"). scopes: list[str] def get_app_state(request: Request) -> AppState: Loading Loading @@ -107,6 +110,7 @@ def _unverified_caller_context(x_correlator: Optional[str]) -> CallerContext: roles=[], flow=TokenFlow.TWO_LEGGED, subject_id=None, scopes=[], ) Loading Loading @@ -149,6 +153,7 @@ async def get_caller_context( roles=roles, flow=decoded.flow, subject_id=claims["sub"] if decoded.flow == TokenFlow.THREE_LEGGED else None, scopes=claims.get("scope", "").split(), ) Loading