refactor(auth): shared JWT, DPoP and base64url primitives #118

Closed
opened 2026-09-24 12:39:02 +00:00 by Grandiras · 0 comments
Owner

Part of the v0.7.0 update cycle (wave 2). Base64url, JWT and DPoP code is written independently in core and Server, and the client and server DPoP copies have drifted. This issue gives each of them one internal implementation.

Scope

  • InternalsVisibleTo ATProtoNet.Server on the core project, so Server stops re-rolling primitives.
  • Use System.Buffers.Text.Base64Url (in-box) everywhere, replacing 6 encoders and 5 decoders. Delete the internal Base64Url class in PlcOperationBuilder, which shadows the BCL type.
  • Internal Jwt.Sign(header, payload, AtProtoKey) and Jwt.TryDecode, plus a GetStringOrNull JSON helper, replacing 3 signers and 4 parsers (DPoPProofGenerator, ServiceAuthGenerator, SpaceTokens, the server DPoPProof, ISpaceServiceAuthVerifier). Use one jti scheme. Each caller keeps its own exception wrapping and rationale comments.
  • One internal DPoP helper (NormalizeHtu, AccessTokenHash, the RFC 7638 Thumbprint, TokenType) shared by client and server. Today the client keeps user:pw@ in htu while the server strips it, and there are two separate thumbprint implementations.
  • DPoPProofGenerator wraps AtProtoKey, which fixes a stored K-256 key silently producing proofs labelled ES256. Use a typed JWK and compute the thumbprint once.
  • Allocation-light proofs: pre-encode the header once per key, cache ath per access token, write the payload with Utf8JsonWriter, and sign into a stack buffer. Measured 18.6 → 15.0 µs and 11.2 → 2.1 KB per proof. Apply the same pattern to ServiceAuthGenerator and SpaceTokens.Create.
  • Stop parsing space tokens twice: add SpaceTokens.Verify(SpaceToken parsed, …).
  • One bounded response reader (ReadBoundedAsync) replaces LengthLimitedStream and the OAuth discovery copy. Add size-cap tests first; nothing tests the caps today.

Characterization tests (add first)

  • DPoP interop: the client generator's output validated by the server DPoPProofValidator, with a bound thumbprint and access token.
  • Base64url: padded, unpadded and invalid input for every decoder entry point.
  • JWT: valid, expired, wrong-audience and malformed cases for SpaceTokens and the service-auth verifier.

No intended public behaviour change; CHANGELOG Changed (allocation) and Fixed (curve label, htu drift).

Part of the v0.7.0 update cycle (wave 2). Base64url, JWT and DPoP code is written independently in core and Server, and the client and server DPoP copies have drifted. This issue gives each of them one internal implementation. ## Scope - [ ] `InternalsVisibleTo ATProtoNet.Server` on the core project, so Server stops re-rolling primitives. - [ ] Use `System.Buffers.Text.Base64Url` (in-box) everywhere, replacing 6 encoders and 5 decoders. Delete the internal `Base64Url` class in `PlcOperationBuilder`, which shadows the BCL type. - [ ] Internal `Jwt.Sign(header, payload, AtProtoKey)` and `Jwt.TryDecode`, plus a `GetStringOrNull` JSON helper, replacing 3 signers and 4 parsers (`DPoPProofGenerator`, `ServiceAuthGenerator`, `SpaceTokens`, the server `DPoPProof`, `ISpaceServiceAuthVerifier`). Use one `jti` scheme. Each caller keeps its own exception wrapping and rationale comments. - [ ] One internal DPoP helper (`NormalizeHtu`, `AccessTokenHash`, the RFC 7638 `Thumbprint`, `TokenType`) shared by client and server. Today the client keeps `user:pw@` in `htu` while the server strips it, and there are two separate thumbprint implementations. - [ ] `DPoPProofGenerator` wraps `AtProtoKey`, which fixes a stored K-256 key silently producing proofs labelled `ES256`. Use a typed JWK and compute the thumbprint once. - [ ] Allocation-light proofs: pre-encode the header once per key, cache `ath` per access token, write the payload with `Utf8JsonWriter`, and sign into a stack buffer. Measured 18.6 → 15.0 µs and 11.2 → 2.1 KB per proof. Apply the same pattern to `ServiceAuthGenerator` and `SpaceTokens.Create`. - [ ] Stop parsing space tokens twice: add `SpaceTokens.Verify(SpaceToken parsed, …)`. - [ ] One bounded response reader (`ReadBoundedAsync`) replaces `LengthLimitedStream` and the OAuth discovery copy. Add size-cap tests first; nothing tests the caps today. ## Characterization tests (add first) - [ ] DPoP interop: the client generator's output validated by the server `DPoPProofValidator`, with a bound thumbprint and access token. - [ ] Base64url: padded, unpadded and invalid input for every decoder entry point. - [ ] JWT: valid, expired, wrong-audience and malformed cases for `SpaceTokens` and the service-auth verifier. No intended public behaviour change; CHANGELOG `Changed` (allocation) and `Fixed` (curve label, `htu` drift).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/ATProto.NET#118
No description provided.