mirror of
https://github.com/ruvnet/RuView
synced 2026-08-03 19:21:42 +00:00
b09625ece7
(a) Test fixture still invented an `iss` claim.
`bearer_auth.rs`'s `token_with_scope` added `"iss": ISSUER` to its tokens.
Harmless today because the verifier ignores `iss` — but it is the same
fixture-invents-reality pattern that hid the original `iss` bug for a day,
sitting in the second-largest auth test suite. Removed, with a pointer to
ruview-auth's regression test.
(b) Cross-process refresh race -> session revocation.
The single-flight guarantee was per-PROCESS. Every CLI invocation is a new
process with its own Session and mutex over one shared credential file, so
two commands run close together inside the 60s refresh window would each
present the same rotating refresh token — and the second is replay, which
identity answers by revoking the whole session family. The user gets logged
out for running two commands at once.
Now guarded by an advisory file lock, taken NON-BLOCKING. A busy lock means
another process is already refreshing, so we wait and re-read its result
rather than race it (20 x 150ms, then proceed anyway — the lock is advisory,
not a correctness barrier, and a dead holder must not wedge us). Blocking on
the lock would have parked the async executor, which is the exact mistake
just fixed in jwks.rs.
Unix only. On other platforms it is a documented no-op — a lock that does
nothing while claiming to protect is worse than none.
(c) One principal could exhaust the global ticket pool.
The 512 cap was global with no per-caller quota, so a single authenticated
`sensing:read` client looping on POST /api/v1/ws-ticket could hold every
slot for 30s and 503 everyone else — denial of service by the
lowest-privilege account the product issues. Added a 16-ticket
per-principal cap; a page needs a handful. Test asserts a noisy user hits
its own cap while a second user is still served and the global pool is
never exhausted.
(d) Debug impls printed live credentials.
`AuthState` derived Debug over the raw RUVIEW_API_TOKEN and
`StoredCredentials` over both OAuth tokens. Not leaking today — I checked
every call site — but this PR had already hand-written redacting Debug for
`OAuthState` and `TicketStore` for exactly this reason, and the two types
actually holding secrets were the ones that missed out. Both now redact.
Tests: 87 ruview-auth (2 new: lock exclusivity + non-blocking, Debug
redaction), 534 sensing-server (1 new: per-principal quota).
Co-Authored-By: Ruflo & AQE
60 lines
2.8 KiB
TOML
60 lines
2.8 KiB
TOML
[package]
|
|
name = "ruview-auth"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
description = "Cognitum OAuth access-token verification for RuView (ADR-271)"
|
|
publish = false
|
|
|
|
[dependencies]
|
|
# Same major as the service that ISSUES these tokens
|
|
# (cognitum-one/dashboard `services/identity`, workspace `jsonwebtoken = "9"`).
|
|
# Signature math is delegated to this crate; nothing here hand-rolls crypto.
|
|
jsonwebtoken = "9"
|
|
|
|
# `ureq`, not `reqwest`: `wifi-densepose-sensing-server` — the first consumer —
|
|
# deliberately chose ureq as "the smallest" HTTP client (see its Cargo.toml).
|
|
# Adding reqwest here would silently reverse that decision for the whole
|
|
# dependency graph. Optional so a caller can supply its own transport via
|
|
# `JwksFetcher` and take no HTTP dependency at all.
|
|
ureq = { version = "2", default-features = false, features = ["tls", "json"], optional = true }
|
|
|
|
serde = { workspace = true }
|
|
serde_json = { workspace = true }
|
|
thiserror = { workspace = true }
|
|
tracing = { workspace = true }
|
|
|
|
# --- `login` feature only (ADR-271 phase 2) -------------------------------
|
|
# The login flow is an interactive client concern: a browser, a loopback
|
|
# listener, a token exchange. The sensing server needs none of it and must not
|
|
# pay for it, so every dependency here is optional and off by default. A server
|
|
# built with default features gets the verifier and nothing more.
|
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"], optional = true }
|
|
tokio = { workspace = true, optional = true }
|
|
rand = { version = "0.8", optional = true }
|
|
sha2 = { workspace = true, optional = true }
|
|
base64 = { version = "0.21", optional = true }
|
|
url = { version = "2", optional = true }
|
|
# Advisory cross-process file lock around the refresh critical section (Unix).
|
|
libc = { version = "0.2", optional = true }
|
|
|
|
[features]
|
|
default = ["ureq-transport"]
|
|
ureq-transport = ["dep:ureq"]
|
|
# Interactive OAuth login: PKCE, loopback callback, OOB paste fallback,
|
|
# credential storage, single-flight refresh. Opt in from a CLI or desktop app.
|
|
login = ["dep:reqwest", "dep:tokio", "dep:rand", "dep:sha2", "dep:base64", "dep:url", "dep:libc"]
|
|
|
|
[dev-dependencies]
|
|
# Test-only: sign real ES256 tokens so the negative matrix exercises the same
|
|
# code path production does, rather than asserting against hand-built strings.
|
|
jsonwebtoken = "9"
|
|
serde_json = { workspace = true }
|
|
|
|
# Keypairs are GENERATED AT TEST RUNTIME, never committed. A checked-in
|
|
# `-----BEGIN PRIVATE KEY-----` is inert here but it trains scanners and readers
|
|
# to treat committed key material as normal, and this repo has no such
|
|
# precedent (zero tracked `.pem` files). Generating also makes the matrix
|
|
# self-contained: no fixture can drift out of sync with the JWKS it is served by.
|
|
p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
|
|
base64 = "0.21"
|