Metadata-Version: 2.5
Name: veltro-suite-auth
Version: 4.0.0
Summary: Veltro-owned suite authentication contracts.
Project-URL: Repository, https://github.com/veltrosecurity/veltro
Author: Veltro Security
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: cryptography>=45.0.1
Requires-Dist: joserfc==1.7.5
Description-Content-Type: text/markdown

# veltro-suite-auth

Python session, browser-request and service-token contracts for product-owned identity stacks.

Licensed under [Apache-2.0](LICENSE). This package came from the AGPL-3.0-or-later `veltro-suite-auth` 1.3.0 implementation in VectorFlow; the code owner relicensed this package on 2026-10-18. See `NOTICE` for provenance.

## Breaking change in 4.0.0

Supported products are `vectorflow` and `sigmatic`. The retired `chad` and
`warden` IDs, roles and scopes, and the shared-stack `external:*` scopes, are
rejected. Upgrade the product and its pinned identity together. Existing suite
installations must keep their matching historical images and verifier versions.

## JOSE dependency (3.1.3)

3.1.3 uses `joserfc==1.7.5` and `cryptography>=45.0.1`. Auth.js cookies
keep `dir`/`A256CBC-HS512` encryption and cookie-specific HKDF keys.
Authority envelopes and browser request assertions keep ES256 signatures
and the existing claim, replay and request-binding checks.

The compact session limit remains 256,000 bytes. Existing large payloads,
chunked cookies and legacy DEF-compressed sessions remain supported.

Update the installed `veltro-suite-auth` pin to take this change. Older SDK
versions still install `python-jose` transitively, even when the product removes
its direct dependency.

## Removed in 3.0.0

3.0.0 drops the authority-revision module: `verify_authority_revision_v1`, the
watermark apply, restore and availability helpers, their constants and types,
and the `REV_*` error codes. Identity never minted revisions, and revocation
already applies on the next request because identity re-reads grants for every
assertion, token call and exchange (veltro#129). A product that imported any of
these has to delete that receiver before it takes 3.0.0.

## Native roles

`APP_ROLES` lists each product's native roles. VectorFlow and Sigmatic accept
`approver`, which maps to the coarse `editor` role on service tokens and
authority envelopes. Native roles remain bound to their own product.

## Authority envelope v2

Actorless `settings:read` and `settings:write` scopes are valid for the
`vectorflow` and `sigmatic` audiences. Identity uses these for product settings.

From 3.1.2, `vectorflow:bootstrap:operator` authorizes identity's one-time
first-admin operator delivery. It is valid only for the `vectorflow` audience,
with `sub` and `azp` both `veltro-identity` and no actor claims, including null
actor fields. The scope implies no other scope. HS256 compatibility tokens
cannot mint or verify it. Receivers still provide the existing issuer,
generation, public key ring and replay consumer.

`verify_authority_envelope_v2` verifies and consumes short-lived ES256
`veltro-suite-service+jwt` envelopes. The receiver supplies the exact issuer,
exact audience, active generation, that audience's public P-256 JWK ring, required
scope, and an atomic synchronous or asynchronous replay consumer. The consumer
runs exactly once after all stateless checks and must return literal `True`.

This package deliberately provides no v2 minting/signing API, private-key type,
key generator, settings/environment adapter, or consumer runtime wiring. The
verifier does no network I/O; the optional `veltro_suite_auth.jwks` module
below fetches the key ring. HS256 compatibility remains separate, with the
same supported product vocabulary.

Actor envelopes may also carry `actor_sub` (identity's subject for the person,
a lowercase uuid) and the pair `actor_scope_kind`/`actor_team_id` (the grant the
exchange picked: `realm` with a null team, or `team` with a team id). They're
only valid next to the actor tuple. The verifier returns all three, as `None`
when the token leaves them out, so a product can resolve the actor by subject
first and fall back to email for older tokens.

## Account-section write

`sigmatic:peer:account:write` authorizes a person's Sigmatic account-section save.
It requires an actor and grants only itself: a status-read token cannot save
settings, and an account-write token cannot satisfy status-read authority.
VectorFlow has no account section or corresponding write scope.

## Product grant read

`product_grants_path(subject=None)` builds the path for identity's
`GET /api/authority/grants`, and `parse_product_grant_list(body, product=...,
subject=None)` checks the response strictly: the right product, canonical
subjects, native roles, a team id exactly when the grant is team scoped, and at
most the one requested subject. Bad bodies raise `SuiteAuthError` with
`GRANTS_INVALID_RESPONSE`. The product makes the HTTP call itself with its
workload credential; this package does no network I/O.

## Browser request assertion verifier

From 2.6.1, duplicate member names are checked per JSON object, so a nested
object (a team entry's `role` beside the grant's `role`) is no longer refused.
Older verifiers refuse any assertion that carries the grant's `teams` list.

`verify_browser_request_assertion` verifies and consumes short-lived ES256
`veltro-browser-request+jwt` request assertions minted by Veltro identity. The
receiver supplies the exact issuer, exact audience, active generation, that
audience's public P-256 JWK ring, the request being authorized (`request` with
`method` and `path`), and an atomic synchronous or asynchronous replay consumer.

The verifier enforces every binding the mint records: exact `typ`, `alg=ES256`
allowlist, audience key ring and `kid`, signature, issuer/audience, active
generation, nbf/iat/expiry capped at 60s, fixed realm `default`, grant-product
consistency, and — the point of the artifact — that the signed method and
canonical path match the request being authorized, using the same shared
`normalize_request_path` contract the mint uses. An assertion minted for
`GET /sigmatic/api/alerts` never authorizes `POST /sigmatic/api/rules`.

One-use semantics are the receiver's: after all stateless checks, the verifier
calls the replay consumer exactly once with a frozen `{issuer, audience,
generation, jti, expires_at}` tuple. The consumer must return literal `True`
only if it has not previously seen that tuple; a replayed `jti` is rejected when
the consumer returns `False`. The consumer must persist at least
`{issuer, audience, generation, jti}` until `expires_at` to enforce one-use
semantics across the token's validity window.

This package deliberately provides no request-assertion minting/signing API,
private-key type, key generator, settings/environment adapter, or consumer
runtime wiring. The verifier does no network I/O.

## JWKS key source

From 3.2.0, `veltro_suite_auth.jwks` loads identity's public signing keys from
a JWKS URL. The package `__init__` never imports it, so the verifiers stay
offline and you can still hand them a ring yourself. It adds no dependency.

```python
from veltro_suite_auth import verify_authority_envelope_v2
from veltro_suite_auth.jwks import JwksKeySource, verify_with_jwks

keys = JwksKeySource("http://identity:3000/.well-known/jwks.json", "sigmatic")
claims = await verify_with_jwks(
    keys,
    lambda rings: verify_authority_envelope_v2(
        token,
        expected_issuer=issuer,
        expected_audience="sigmatic",
        active_generation=generation,
        public_jwk_rings=rings,
        required_scope=scope,
        consume_jti=consume_jti,
    ),
)
```

`await rings()` returns `{audience: keys}`, the shape both verifiers take. The
issuer and active generation still come from your own settings, so whoever
answers the JWKS URL can't choose them.

The first `rings()` call fetches. Keys are cached for `max_age_seconds` (300).
Fetches happen at most once per `min_refetch_interval_seconds` (30), whatever
triggered them, and concurrent callers share one request; cancelling one
caller doesn't cancel it for the others. A failed fetch or a bad document keeps
the last good keys. With no good keys yet, `rings()` raises `SuiteAuthError`
with code `JWKS_UNAVAILABLE`, chained from the failure.

`verify_with_jwks` awaits one verifier call. On `BR_INVALID_KID` or
`V2_INVALID_KID` it awaits `refresh_for_unknown_kid()` and retries exactly
once. Both verifiers check the kid before the replay consumer, so the failed
attempt doesn't spend the jti. `refresh_for_unknown_kid()` returns `True` when
the key set changed.

A document must be `{"keys": [...]}` with no other member and no repeated
member names, and its keys get the same check as a ring you pass in: exactly
the eight public ES256 members, canonical 32-byte coordinates, unique key ids
and at least one key. The shared `conformance/suite-auth-jwks-v1` vectors pin
this for both libraries.

By default the request runs through the standard library's `http.client` on
a one-thread executor that belongs to the source, so a slow identity never
ties up the event loop's default executor. It's http(s) only, with no proxy
and no redirects. One deadline of `timeout_seconds` (5, at most 2,147,483)
covers connecting, headers and body, read in chunks of at most 8 KiB, and the
socket is shut down when it passes, so a server trickling bytes can't hold the
thread. The body is capped at `max_bytes` (65,536). The URL has to be absolute
http(s) without credentials. Pass `fetch` to route the request through your
own egress guard: an async callable
`fetch(url, *, timeout_seconds, max_bytes) -> bytes` that returns the body of
a 200 response and raises on anything else, redirects included. The source
still applies the timeout and the size cap
around it. `clock` (monotonic seconds) is there for tests. Bad options raise
`JWKS_INVALID_CONFIGURATION`.

## Session team pick

From 3.0.0, a session's `selectedTeamId` is kept only when it matches the chrome
manifest's team id shape, `^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`, and is
null otherwise. Nothing is trimmed. The TS and Python decoders share the rule
and the `session-v3` vectors that pin it.
