Skip to content
Merged
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
103 changes: 37 additions & 66 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,81 +5,52 @@ All notable changes to this project are documented in this file. The format foll

## [Unreleased]

### Security
## [0.7.0] - 2026-11-02

- The acknowledgements `Poller.stop()` flushes went to the poll endpoint without the URI policy
check of a poll, so that an endpoint that had moved to plain `http` meanwhile would have
received the access token. The check now sits in the one method every poll request passes, as
in easyssf 0.7.0 for Java.
- A response body larger than the maximum fails the call with the new `ResponseTooLargeError`
instead of being cut off at 4 MiB and handed on; it counts as the transmitter being unavailable
and is not tried again. The maximum is the new setting `easyssf.receiver.http.max-response-size`
(`4MB` by default, bound as Spring's `DataSize`: `DataSize`, `parse_data_size`,
`format_data_size`), and `max_response_size` on `send`, `get_json`, `exchange`,
`MetadataResolver`, `StreamClient`, `PollerOptions`, `ClientCredentialsOptions` and
`HttpJwkSetSource`. The default fetch of `HttpJwkSetSource` read the JWK Set without any bound;
it goes through `send` now, and follows a redirect within the origin as every other call does.

### Changed

- `StreamClient` checks the issuer of every stream configuration the transmitter returns, not only
of a created one: `stream`, `streams`, `update_stream` and `replace_stream` raise
`IssuerMismatchError` as `create_stream` did, as the SETs of a stream with another issuer would
not verify. `StreamRegistrar` deletes a stream it created with the wrong issuer, as before, and
leaves an existing one alone; it fails without retrying either way.

- A SET whose claim was taken over by another instance while the handlers ran (they outlived
the `dedup.lease`) is no longer acknowledged by the late instance: `Processor.process` raises
`InProgressError`, the push endpoint answers `503` and the poller does not acknowledge, so that
the acknowledgement comes from the instance that records the success, and a failure there is
not hidden from the transmitter. `JtiDedupStore.processed` returns whether it recorded the
success (`False` when the claim was taken over), which closes the gap between the last renewal
and the record; only an explicit `True` acknowledges, a store that returns nothing does not. The Java, Go, .NET and Rust ports still return the result of the handlers in
this case.
- `Receiver.stop()` stops every transmitter even when one fails to, and raises the first failure
afterwards; `Receiver.aclose()` closes every one of the receiver's own HTTP clients even when
`stop()` or the closing of one of them fails.
- `ReceiverConfig.from_mapping` reports a key it does not know as a configuration failure, with
the key it resembles (`push.expected-auth-headers: unknown setting, did you mean
'expected-auth-header'?`), instead of ignoring it silently as the Spring Boot, .NET and Go
bindings do; `from_mapping(..., strict=False)` restores that behaviour.
- `Receiver` owns the HTTP clients it makes itself: `Receiver.aclose()` stops the receiver and
closes the `httpx.AsyncClient` it created when none was given, and the one that adds the
configured `user-agent`; a client given as `http_client=` stays open. `async with receiver:`
and `receiver_lifespan` close the receiver on exit; `stop()` alone leaves it ready for another
`start()`. `Receiver.closed` tells, and `start()` on a closed receiver raises `RuntimeError`.
- `Receiver.start()` rolls back when a transmitter fails to start: the transmitters started before
it are stopped again and the receiver is left stopped, so that `start()` can be tried again
instead of doing nothing.
- The distributions pin each other to the `0.7` series (`easyssf-core>=0.7,<0.8`,
`easyssf-receiver>=0.7,<0.8`), and CI installs the built wheels in a clean environment.

### Fixed

- A SET the poll response lists under a key other than its `jti` (RFC 8936, section 2.4) is
reported to the transmitter as `invalid_request` under that key instead of being handled and
acknowledged under a key that is not its own.
- `StreamRegistrar.stop()` deletes only a stream published since the last `stop()`, not again the
one a previous `stop()` deleted, and `start()` waits for a `stop()` under way, so that a
registration started meanwhile does not adopt the stream being deleted.

- `UriPolicy.is_loopback` took any host that starts with `127.` for a loopback address, so that
`http://127.attacker.example/` was exempt from the https requirement; it now uses the parsed IP
address (`127.0.0.0/8`, `::1`) and the `localhost` names only.
- The ASGI push app and the FastAPI push router buffer no more than one byte beyond
`MAX_SET_SIZE` of a request body, whatever the size of the chunks the server hands over; before,
one large chunk was appended in full before the limit was checked.
The first release: the port of easyssf 0.7.0 for Java to Python, after the Go, .NET and Rust
ports, at feature parity with the Java library.

### Added

- The port of easyssf 0.7.0 for Java to Python, after the Go, .NET and Rust ports: `easyssf-core`
(the vocabulary), `easyssf-receiver` (the asyncio receiver), `easyssf-test` (the in-process test
transmitter) and the conformance harness on testcontainers-python.
- `easyssf-core` (`easyssf.core`, the vocabulary: SETs, subjects, stream configuration, transmitter
metadata and the typed CAEP, RISC and SCIM events), `easyssf-receiver` (`easyssf.receiver`, the
asyncio receiver: verification, de-duplication, push and poll, stream management, typed
handlers, token revocation and session termination) and `easyssf-test` (`easyssf.testing`, the
in-process test transmitter). The distributions pin each other to the `0.7` series
(`easyssf-core>=0.7,<0.8`).
- `easyssf-receiver-fastapi` (`easyssf.fastapi`): the push endpoint as an `APIRouter`
(`push_router`, `push_router_for`), the rejection of revoked access tokens as a dependency
(`revocation_dependency`) or as the ASGI `RevocationMiddleware`, the receiver's lifecycle as a
lifespan (`receiver_lifespan`) and its health as a route (`health_router`).
- The hardening of easyssf 0.7.0 for Java: `StreamClient` checks the issuer of every stream
configuration the transmitter returns (SSF 1.0, section 8.1.1) and raises `IssuerMismatchError`;
`StreamRegistrar` deletes a stream it created with the wrong issuer and leaves an existing one
alone. Every poll request passes the URI policy check, the acknowledgements `Poller.stop()`
flushes included. A SET the poll response lists under a key other than its `jti` (RFC 8936,
section 2.4) is reported to the transmitter as `invalid_request` under that key. A response
body larger than `easyssf.receiver.http.max-response-size` (`4MB` by default, bound as Spring's
`DataSize`: `DataSize`, `parse_data_size`, `format_data_size`) fails the call with
`ResponseTooLargeError`, counts as the transmitter being unavailable and is not tried again.
- A SET whose claim was taken over by another instance while the handlers ran (they outlived the
`dedup.lease`) is not acknowledged by the late instance: `Processor.process` raises
`InProgressError`, the push endpoint answers `503` and the poller does not acknowledge, so that
the acknowledgement comes from the instance that records the success. `JtiDedupStore.processed`
returns whether it recorded the success, and only an explicit `True` acknowledges. The Java,
Go, .NET and Rust ports return the result of the handlers in this case.
- `ReceiverConfig.from_mapping` reports a key it does not know as a configuration failure, with
the key it resembles (`push.expected-auth-headers: unknown setting, did you mean
'expected-auth-header'?`), where the Spring Boot, .NET and Go bindings ignore it;
`from_mapping(..., strict=False)` ignores it as well.
- `Receiver` owns the HTTP clients it makes itself: `Receiver.aclose()` stops the receiver and
closes them, a client given as `http_client=` stays open; `async with receiver:` and
`receiver_lifespan` close the receiver on exit. `Receiver.start()` rolls back when a transmitter
fails to start, and `Receiver.stop()` stops every transmitter even when one fails to.
- The conformance harness (`conformance/`, not published): the OpenID conformance suite's four SSF
receiver test plans against the receiver, with the suite started by testcontainers-python.
- `examples/fastapi/resource-server`, the port of the Spring Boot resource server example after
the Go one: an OAuth2 resource server on FastAPI against the Keycloak of `examples/keycloak`
that registers its stream (push, or `--poll`) and rejects access tokens once their session was
revoked.

[Unreleased]: https://github.com/easyssf/easyssf-python/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/easyssf/easyssf-python/releases/tag/v0.7.0
Loading