From 3e011e2819bbca27232e41bd23a17232d0752a09 Mon Sep 17 00:00:00 2001 From: Alex Nachbaur Date: Fri, 8 May 2026 10:57:39 -0400 Subject: [PATCH] Add resource parameter to CrossAppAccessFlow.start() Plumbs the RFC 8707 resource parameter through CrossAppAccessFlow.start() into the underlying token exchange, alongside existing audience and scope. Updates CHANGELOG and README, and adds a test covering audience + resource + scope with an access-token subject. --- CHANGELOG.md | 6 +++++ README.md | 14 ++++++++++++ src/okta_client/oauth2auth/cross_app.py | 4 ++++ tests/test_cross_app_flow.py | 30 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 828d0f7..bbaa80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- `CrossAppAccessFlow.start()` now accepts an optional `resource` parameter (RFC 8707), forwarded to the token exchange alongside `audience` and `scope`. + ## 0.2.0 ### Added diff --git a/README.md b/README.md index 10e8be6..5ad8336 100644 --- a/README.md +++ b/README.md @@ -535,6 +535,20 @@ issuer — so the values match. They are kept separate because the token-exchang audience is a logical parameter of the request, while the target is a structural configuration that determines which server the second leg talks to. +`start()` also accepts optional `resource` (RFC 8707 target resource URIs) +and `scope` parameters, which are forwarded on the token-exchange request to +further constrain the ID-JAG.: + +```python +result = await flow.start( + token="", + token_type="access_token", + audience="https://api.example.com", + resource=["https://api.example.com/v1/resource"], + scope=["openid", "custom_scope"], +) +``` + #### Path 1 — Automatic (key-provider auth) When the client uses `ClientAssertionAuthorization` with `assertion_claims` and diff --git a/src/okta_client/oauth2auth/cross_app.py b/src/okta_client/oauth2auth/cross_app.py index 8ac952a..aa7e129 100644 --- a/src/okta_client/oauth2auth/cross_app.py +++ b/src/okta_client/oauth2auth/cross_app.py @@ -289,6 +289,7 @@ async def start( *, token: str, audience: str | None = None, + resource: Sequence[str] | None = None, scope: Sequence[str] | None = None, token_type: Literal["id_token", "access_token"] = "id_token", context: CrossAppAccessContext | None = None, @@ -304,6 +305,8 @@ async def start( audience: Target audience for the ID-JAG. Defaults to :attr:`target.issuer ` when not supplied. + resource: Optional target resource URIs to include on the + token exchange (RFC 8707). scope: Optional scopes to request on the ID-JAG. token_type: Whether *token* is an ``"id_token"`` (default) or ``"access_token"``. @@ -341,6 +344,7 @@ async def start( subject_token=token, subject_token_type=subject_token_type, audience=audience, + resource=resource, scope=scope, requested_token_type=TokenType.ID_JAG, ) diff --git a/tests/test_cross_app_flow.py b/tests/test_cross_app_flow.py index 715ff17..6616406 100644 --- a/tests/test_cross_app_flow.py +++ b/tests/test_cross_app_flow.py @@ -339,6 +339,36 @@ def test_start_with_scope() -> None: assert body["scope"] == ["openid custom_scope"] +def test_start_with_audience_resource_and_scope_using_access_token() -> None: + """start() forwards audience, resource, and scope when exchanging an access token.""" + network = DummyNetwork() + client = _build_client(network) + flow = CrossAppAccessFlow( + client=client, + target=CrossAppAccessTarget(issuer="https://example.com/oauth2/my-auth-server"), + ) + + asyncio.run( + flow.start( + token="my-access-token", + token_type="access_token", + audience="https://api.example.com", + resource=["https://api.example.com/v1/resource"], + scope=["openid", "custom_scope"], + ) + ) + + body = network.last_exchange_body + assert body is not None + assert body["grant_type"] == ["urn:ietf:params:oauth:grant-type:token-exchange"] + assert body["subject_token"] == ["my-access-token"] + assert body["subject_token_type"] == ["urn:ietf:params:oauth:token-type:access_token"] + assert body["requested_token_type"] == ["urn:ietf:params:oauth:token-type:id-jag"] + assert body["audience"] == ["https://api.example.com"] + assert body["resource"] == ["https://api.example.com/v1/resource"] + assert body["scope"] == ["openid custom_scope"] + + def test_start_stores_id_jag_in_context() -> None: """start() stores the ID-JAG token and exchange result in the flow context.""" network = DummyNetwork()