Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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="<access-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
Expand Down
4 changes: 4 additions & 0 deletions src/okta_client/oauth2auth/cross_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -304,6 +305,8 @@ async def start(
audience: Target audience for the ID-JAG. Defaults to
:attr:`target.issuer <CrossAppAccessTarget.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"``.
Expand Down Expand Up @@ -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,
)
Expand Down
30 changes: 30 additions & 0 deletions tests/test_cross_app_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down