Repository navigation
feat(auth)!: an auth handle that outlives its session - #6109
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix the reconnect test, which shadowed its token and never compiled under `--all-targets` with tcp; its OOM was the build on a loaded machine, not the test (it now runs in 0.15s). Hold issued grants instead of leaking them. Extend `a_handle_backs_one_active_session` to cover a draining session keeping an unanswered token beside its successor, and a token added after the drain riding only the successor. Point the relay auth bench at `moq_net::auth`, update the relay refresh tests for a transient refusal that now retries instead of failing `add()`, add MIGRATE entries for moq-net and moq-tokio, and apply rustfmt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirror the moq-net change in js/net. `Auth.Handle` is now a class: a
session-independent handle passed to `connect({ auth })` presents every
token it holds on each session built on it, so a route naming one of its
`Auth`s keeps it across a reconnect. A handle backs one active session at a
time, plus the ones draining after a GOAWAY; attaching another rejects with
`Auth.Duplicate`. Only the peer's verdict, or the handle's end, ends a token.
The reconnect loop owns one handle (`Reload.auth`), surfaced as
`Connection.auth`, and aborts it when the loop closes. A transiently refused
token on moq-lite is presented again after a 1s backoff doubling to 30s, as
in Rust. `Auth.None` is removed.
Update the docs that said to add the token again and re-announce after a
reconnect, and add a MIGRATE entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark the wire view's auth field internal so the declaration build no longer imports an internal type, and satisfy clippy's field_reassign_with_default in the moq-tokio reconnect test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the
Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 88e31cd9fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # rs/moq-tokio/MIGRATE.md
mriise
left a comment
There was a problem hiding this comment.
Read-only review of 4cf66d6d6. I ran no tests. The happy path meets the quest: routes keep their Auth across a reconnect without a re-add or re-announce, and there is no wire change. The findings are at the edges.
Bugs
1. A token that ends without a verdict leaves add() and closed() pending.
Link::ended (rs/moq-net/src/auth.rs:1036-1066) records a non-verdict end only on the link's slot. state.end_token runs only for a verdict. Auth::poll_answered (auth.rs:1344) and Selector::ended read the handle-level state.tokens[id], which stays unset.
- Per-session handle after a GOAWAY:
session.auth().add(t)ends withError::GoingAway(lite/session.rs:583-586, and the ietf loop).add()then stays pending until the drained session closes. Before this PR it rejected at once. - ietf stream reset before an answer:
Present::runreturnsError::Stream(_), which is not a verdict, so Rustadd()hangs. JS maps the same case toUnsupportedand rejects (auth.test.ts:869). - Shared handle, stream lost mid-session: a lite reset with a non-Cancel code after a grant, or a failed
poll_open, leaves the token unpresented on the live session.grant()readsNone, andclosed()waits for a reconnect that may never come.
Suggested fix:
- When the handle is bound to one session, make any end final (
end_token). - For a shared handle, re-queue the token on the live link, or end it.
- A Rust test for the ietf reset case, mirroring the JS one, would pin this.
2. JS presents on a draining session; Rust does not.
Rust refuses new AUTH streams after a GOAWAY, and the PR says "a token added after the GOAWAY rides only the successor".
JS does not. HandleState.add offers to every session (js/net/src/auth_session.ts:231). AuthSession.offer (:586) and the retry in #run check #closed but never #draining. If the draining peer resets that stream before answering, #run maps the StreamError to Unsupported. That is a verdict, so the token ends on the successor too.
Suggested fix: have offer and the post-backoff retry skip draining sessions, and add a JS test for a token added after a GOAWAY.
Risks
3. abort on a connection-owned handle quietly breaks reconnects.
- moq-tokio
Connection::auth(), JSReload.authand pooledConnection.authall exposeabort. - After an abort,
Link::attachand JSvacant()reject every later connect. They do so only after dialing the relay, while the current session keeps running. - With pooling, one consumer of a URL breaks reconnects for every other consumer of it.
Consider returning a view without abort, or having the loop close loudly when its handle ends.
4. A clean close after a grant counts as a verdict (auth.rs:1046-1049).
- A server that drops its
Issuedbefore the transport closes ends the client's token on every future session. - The moq-tokio test
a_token_is_presented_on_every_sessionalready has to keepissuedalive pastsession.abortto avoid this. - A natural per-session server task that drops
Issuedon GOAWAY drain would trigger it.
Consider treating only an explicit AUTH_ERROR as a verdict, or documenting the requirement on acceptors.
5. Docs.
Requests::next/poll_nextstill say "Noneonce the session ends". With a shared handle it is the handle's end.doc/lib/js/net.mdsays "AConnection'sauthhandle", but pooledConnection.authis aGetter<Handle | undefined>.
Questions
- A JS reload loop keeps one handle across URL changes, so a token minted for relay A is presented to relay B. Is that intended, or should a URL change abort the handle?
Handle::authorizeand the acceptor queue span sessions, so a limit set for one peer carries over to the next. That is fine for the same relay. Is it intended after a GOAWAY redirect to another one?- Rust wraps the ietf presenter in the retry loop (
ietf/auth.rs:~298), butfrom_codenever yields a transient code there. Delete the loop, or keep it for parity with lite? - JS
Handle.requests()throws a plainErrorwhere Rust returnsError::Duplicate. Should JS throwAuth.Duplicatenow that it exists?
(Written by Claude Opus 5.5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
moq-dev#6109 replaces moq-tokio's connection auth with one moq-net auth handle that spans a connection's sessions, so: - moq-ffi's temporary dispatch enum collapses onto moq_net::auth::{Handle, Watch, Auth}. - Configured tokens are held once per connection on that handle (connection/tokens.rs) instead of re-added per session, which would have presented each one twice while a GOAWAY'd session drained. Each dial tells the holder its connection credential. - A configured token a moq-transport session cannot send is kept for a later session with AUTH rather than dropped, and logged once per dial. - js/net holds configured tokens once per auth handle, so a reloading connection's shared handle presents each once across reconnects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the conflicts with #6137: moq-tokio's connection::auth stays deleted, so moq-ffi's auth wrapper collapses onto the one moq_net::auth::Handle, as #6137 decided for whichever landed second. Tests from main that build a session's handle use auth::Link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review Merged (Written by Claude Opus 5.5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 73bc832ed9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A token added after a GOAWAY was offered to the draining session too: a draining moq-lite session without AUTH refused it as Unsupported, which ended it on the successor, and in JS a draining session opened a fresh AUTH stream for it. Offer new tokens only to active sessions, and in JS stop a transient retry once the session drains. JS also points the handle's grants at the successor as soon as it attaches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ed59324f6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review d53b13b fixes the four open findings (c0e3034) and merges main:
The merges port main's erased transport (moq-dev/moq#6176, moq-dev/moq#6539) to
(Written by Claude Opus 5.5) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d53b13bad9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…lock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review (Written by Claude Opus 5.5) |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Ready to merge at 2f6b5dd.
(Written by Claude Opus 5.5) |
Resolve main's connection auth replay (#6487) onto the session-independent handle: drop the replay, fold the auth-abort mapping into Shared::end, and build the wasm dial on a Handle::new() passed through Client::with_auth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d while a browser dial fails Main's Manual.install returns an Installed to close. The wasm UniFFI case checks a token added mid-dial rejects once the dial is refused, rather than waiting forever on the dial's handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review a52d00b merges main past moq-dev/moq#6487, which had built a token replay on the module this PR deletes:
(Written by Claude Opus 5.5) |
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's uring extensions test expected the replaced connection handle to refuse a token without MoQ Auth. The session-independent handle keeps it for the requests that name it instead, which Answer::Requests reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@codex review 37fb3dc merges main again. The one semantic change: main's new uring relay test (moq-dev/moq#6646) expected (Written by Claude Opus 5.5) |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The tests #6109 added set backoff through the removed MIN_BACKOFF and destructure a driver from a server request's ok(), which now returns the session alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Takes moq-dev#6109 as it landed, resolved against the c0b800c head this branch had merged, and main's transport-config and binding-config refactors. - Token moves to Setup Parameter 0x7: main gave 0x6 to Implementation. - Handle::add returns how the peer accepted a token, so the connection's token holder reads Answer::Requests instead of inferring it from an unknown grant. - MoqClient::set_tokens becomes MoqClient::tokens() -> MoqTokens with set(tokens), mirrored in every wrapper and matching moq-tokio's Client::tokens(). - A refused replacement still withdraws the token it replaced (tested). - Drops the finished token-in-band quest's remaining links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #6109, Handle::add resolves once the peer grants the token, so a watch created afterwards already holds that grant and changed() waited forever. relay_auth_refresh hung before its first case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finishes quest
m1/auth/issuer(its file is deleted in this PR). An auth handle now outlives its session, so a route naming a token keeps it across a reconnect, in both Rust and JS.Problem
A token added on
session.auth()ended with its session, so after a reconnect the application had to add it again and re-announce every route naming it. moq-tokio papered over this with a secondconnection::authhandle that replayed tokens onto each new session, which aRoutecould not name.Approach
moq-net (
rs/moq-net/src/auth.rs, client, session, goaway, lite/ietf drivers)auth::Handleis session-independent:Handle::new()plusClient::with_auth(handle). Each session built on it presents every token the handle holds, so aRoutenaming one of itsAuths keeps it across a reconnect with no re-add or re-announce.Error::Duplicate.Auth::grant()andHandle::grant()follow the newest open session, and areNonewhile disconnected.Unsupported, or a clean close after a grant. A session ending does not.Internal,TooManyRequests,Timeout) is presented again after a backoff of 1s doubling to 30s. moq-transport AUTH_ERROR has no transient code, so its presenter has no retry loop. This logic moved here from moq-tokio.Unsupported, and a shared handle keeps the token.Handle::abort(err)ends a handle; a handle a session creates for itself still ends with it.GoingAway.GoingAway, neverCancel, so it never reads as the peer's clean close.Handle::addresolves with(Auth, Answer):Answer::Grantedwhen the peer granted the token on its AUTH stream,Answer::Requestswhen only the requests naming it carry it (moq-transport without AUTH). The answer is fixed once given, so it holds after the session drops.Answer::Requeststoken, instead of inferring that from a missing grant, which a dropped session also reads.add()resolving with the token alone.moq-tokio
Connection::auth()returnsmoq_net::auth::Handle, and every dial is built on it. Aborting it ends the loop at its next reconnect.Shared::endends the handle with the loop:Closedfor a stop,Transportfor a non-moq-net failure,Unauthorizedfor a rejection, or the session's error. NeverCancel, so moq-ffi'sMoqAuthToken::closed()reports it as an error, as before.connection::authmodule, its replay loop, and thetokensreplay list in the connection state are deleted.moq-ffi
MoqAuthwrapsmoq_net::auth::Handledirectly; theHandle/Watch/Tokenenums and their Connection variants are gone.Handle::new()up front and passes it throughClient::with_auth, so a token added while dialing waits for the session. The dial ends the handle when it fails, is stopped, or its session closes, so that token never waits forever. This replaces the separate wasmDialvariant.js/net mirrors the design:
Auth.Handleis a class.connect({ auth })builds a session on one.Connectionowns one asconnection.auth.Auth.Duplicateis new.connection.authgives the loop up at its next reconnect.requests()throwsAuth.Duplicate.Consumers and docs
auth/refresh.rstests now expect the retry instead of an error.doc/concept/{moq-lite,standard}.md,doc/lib/{js/net,rs/moq-net}.md,doc/bin/relay/auth.md.Impact
Breaking, with no wire change.
auth::Handle::new()creates a handle that backs no session yet; pass it toClient::with_authand every session that client connects presents the tokens it holds. ARoutenaming one of itsAuths keeps that token across a reconnect, so drop the code that added the token again and re-announced after one.Client::with_auth,Handle::abortandauth::Answer(Granted,Requests).Error::Duplicate. Give independent concurrent sessions separate handles.Handle::grant()andAuth::grant()follow the newest open session, so they readNonewhile disconnected instead of keeping the last session's grant.Auth::closed()resolves only on the peer's verdict (a refusal,Unsupported, or a clean close after a grant) or the handle's end, whichHandle::aborttriggers; a session ending is not one. A handle a session creates for itself still ends with that session.Internal,TooManyRequests,Timeout) is presented again after a backoff of 1s doubling to 30s, instead ofaddfailing with that code.Handle::addresolves with(Auth, Answer)instead ofAuth: replacelet token = handle.add(jwt).await?;withlet (token, answer) = handle.add(jwt).await?;. Read the answer rather than inferring support from a missing grant: it holds after the session drops, unlikeAuth::grant().Connection::auth()returns amoq_net::auth::Handlethat every session of the connection is built on, and theconnection::authmodule (Handle,Auth,Watch) is removed. Replacemoq_tokio::connection::auth::{Handle, Auth, Watch}withmoq_net::auth::{Handle, Auth, Watch}.Route, and keeps riding that route's announcements on every reconnect.Nonewhile disconnected. A transiently refused token retries on a fixed backoff of 1s doubling to 30s rather than the reconnectbackoff.Auth.Handleis now a class:new Auth.Handle()backs no session yet, andconnect({ auth })builds the session on it, so every session built on one presents the tokens it holds.Connectionowns one asconnection.auth; a route naming one of itsAuth.Auths keeps that token across a reconnect, so drop the code that added the token again and re-announced after one.connect({ auth }),Connection.authandAuth.Duplicate. Connecting another session while one is still active rejects withAuth.Duplicate, and so does a secondrequests()call, which threw a plainErrorbefore.Established.authreturns the session's handle, so itsgrantand eachAuth.Auth'sgrantread undefined once the session closes, instead of an empty grant.Auth.Auth.closedsettles only on the peer's verdict or the handle's end (handle.abort()); a session ending is not one, except for a handle the session created for itself.Internal,TooManyRequests,Timeout) is presented again after a backoff of 1s doubling to 30s, instead ofaddrejecting with that code.Decisions
The maintainer confirmed all six, each the ✅ option this PR implements.
with_auth. A server's sessions are separate peers, so a shared handle would merge theirauthorize()andrequests().Server::with_auth.Handle.Error::Duplicate(JSAuth.Duplicate).presentsucceeds and the token ends withUnsupported.add()still fails at once withUnsupported.add()on a transient lite refusal. ✅ It retries instead of returning the code, matching the replay loop it replaces.Connection.authon a pooled connection. ✅ Expose it as aGetter<Auth.Handle | undefined>that follows the URL, likeorigin.Settled while addressing reviews on c6f331e, each the recommended option:
Unauthorized.Settled 2026-10-24 when merging main past moq-dev/moq#6487, which had built a token replay and a
Handle/Watch/Tokenenum on the module this PR removes:Shared::end.Dialvariant.Tests
a_shared_handle_presents_its_token_on_every_sessiona_handle_backs_one_active_session, including the draining casesa_token_queued_before_our_goaway_rides_only_the_successor: fails on moqt-17 and moqt-22 if the presenter checks only a received GOAWAY.lite_07_presents_a_transiently_refused_token_againlite::session::tests::a_lost_retry_keeps_a_shared_tokenmoqt_{16,22}_route_auth_survives_a_reconnectonly_a_verdict_ends_a_shared_tokenthe_answer_outlives_its_sessiona_grant_survives_its_session_dropping_before_add_returns: fails ifaddinfers request-only from a missing grant.a_transient_refusal_backs_offa_token_is_presented_on_every_session(token refresh across reconnects) andan_ended_auth_handle_ends_the_loop.auth.test.ts:migrate.test.ts: the connection's tokens ride every session across a GOAWAY.Checks
just check origin/mainpassed there (8853 Rust tests).uring_workers_offer_the_configured_extensions(moq-dev/moq#6646) expected the replaced connection handle to refuse a token without MoQ Auth. It now readsAnswer::Requeststhere andAnswer::Grantedwith the extension, the same distinction.just check origin/main: every selected module passed, including 8839 Rust tests.just test interop --all: all 164 checks passed.just test wasm --uniffi: every case passed, including the new mid-dial token case on lite, ietf and setup.cargo clippy -p moq-ffi --target wasm32-unknown-unknown -- -D warnings.Handshake::requests(moq-dev/moq#6470) ontoauth::Link, drops the moq-transport prefix tests that moq-dev/moq#6621 made obsolete, takes main's removal of the lite cap tests, and moves this PR's migration notes into Impact, where main now records how to migrate.just check origin/main(8426 Rust tests, includingnix flake check) andjust test interop --all(164 checks); 2f6b5dd passed 20 reruns of the JS reload and migration tests.moq-cliexport::redundant_importers_switch_through_bufferbloat_on_lite_07, tracked byquest/m1/test-flakes-2/redundant-failover-drift.md.Follow-ups
Server::with_auth, once a consumer needs it (decision 1).quest/m1/auth/token-represent.md.🤖 Generated with Claude Code
(Written by Claude Opus 5.5)