v154 · webtransport headers

Authenticating at connect time

Two ways to prove who you are: in the handshake, or in the first message after it. Both are run here against this server, for real, and timed — because the difference is not only how long the client waits but how long the server holds a session it has no reason to trust.

Run both shapes

Credentials in the handshake

  1. send the request, token attached
  2. server decides
  3. client learns the verdict

time to verdict

Credentials in the first message

  1. open the session, anonymously
  2. session is open, peer unproven
  3. send the token
  4. server acknowledges it

time to verdict

Measured on this page, against this origin
shapems to verdictround tripsunproven session held
Not run yet.
Press the button.

The second shape is measured over a WebSocket, not a WebTransport session: a WebTransport server needs HTTP/3 and a certificate this host does not serve. The stand-in is honest about the thing being measured — a session that opens before credentials arrive — because that is a property of the shape, not of the protocol. The numbers are real; the protocol is not the point.

The window in the middle

The interesting number is not the total. It is the gap between "session open" and "token acknowledged" — every millisecond of which the server spent tracking a connection from a peer that had proven nothing. Multiply it by your connection rate and it is a budget an attacker can spend for free.

headers removes the gap rather than shortening it. The token travels in the CONNECT, so a session either exists and is authenticated, or was never created. There is no half-trusted state to write code for, and no half-trusted state to attack.

the shape it replaces

// Before: connect first, prove yourself second.
const transport = new WebTransport(url);
await transport.ready;                       // the server now has a session
const writer = transport.datagrams.writable.getWriter();
await writer.write(encode({ token }));       // …and only now knows who you are

// After: the CONNECT carries it, so an unauthenticated session never exists.
const transport = new WebTransport(url, {
  headers: { authorization: `Bearer ${token}` },
});
await transport.ready;                       // resolving means you were accepted

see also