feat: bearer-token pairing auth across iOS, Android, macOS, CLI#64
Open
rafiki270 wants to merge 8 commits into
Open
feat: bearer-token pairing auth across iOS, Android, macOS, CLI#64rafiki270 wants to merge 8 commits into
rafiki270 wants to merge 8 commits into
Conversation
Closes 27 findings from Codex review. Token storage moved to SHA-256 hashes only (no fake macOS AES). Pair-status keyed by requestId nonce, not enumerable clientId. CSRF gate on POST /v1/pair. Default button is No, never Always. CLI session-scope tokens stay in-process. mDNS spoofing mitigated by deviceId+host+port fingerprint pinning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds PairRequest, PairResponse, PairScope, PairStatusState, PairStatusResponse, and DeviceInfoPairing types for the cross-platform pairing/auth feature. Registers kelpie_pair as a CLI-level MCP tool so MCP clients can drive the pairing flow when they hit UNAUTHORIZED. Refs: docs/plans/2026-05-16-pairing-auth.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the iOS half of the pairing/auth design in docs/plans/2026-05-16-pairing-auth.md: - PairingStore: file-backed token-hash store (atomic write, NSFileProtectionComplete, 0600 perms), in-memory pending requests + session approvals + denied-source suppression. SecRandomCopyBytes for all CSPRNG output. - AuthMiddleware: deny-by-default with exact-match decoded-path unauth allowlist (/v1/pair, /v1/pair/status, /v1/get-device-info, /health). CSRF gate on POST /v1/pair (Content-Type + Origin rejection). Duplicate Authorization / Content-Length and any Transfer-Encoding rejected pre-routing. - HTTPRequestParser: split out parsing into a pure helper with per-header duplicate tracking and percent-decoded path. - PairingHTTPResponse: shared response builder with Cache-Control: no-store on pair endpoints. CORS header removed entirely. - PairEndpoints: POST /v1/pair (issues requestId nonce), GET /v1/pair/status (binds to source address, returns token exactly once then deletes issuance), DELETE /v1/pair (derives clientId from bearer; ignores query), unauthenticated GET /v1/get-device-info with requiresPairing flag. - HTTPServer: wired through the middleware; unauth/authenticated route tables. - PairApprovalCoordinator + PairingDialog: SwiftUI modal sheet with default No button (cancel-action keyboard shortcut), Yes once, Always allow. - PairedClientsView: Settings sub-screen with persistent + session + recently denied lists and per-entry revoke buttons. - BrowserView attaches the pairing dialog modifier at the root so prompts surface regardless of which screen is active. Refs: docs/plans/2026-05-16-pairing-auth.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the iOS pairing implementation for full platform parity. - PairingStore: SHA-256 hashed token store backed by app-private JSON (filesDir/pairings.json), SecureRandom for tokens and request IDs, constant-time hex comparison, source-address binding, recent-deny suppression, untrusted clientName sanitisation - AuthMiddleware: deny-by-default with exact-match decoded-path unauth allowlist for /health, /v1/pair, /v1/pair/status, /v1/get-device-info; CSRF gate on POST /v1/pair (Content-Type + no Origin); duplicate Authorization / Content-Length and Transfer-Encoding rejected - PairEndpoints: POST /v1/pair, GET /v1/pair/status, DELETE /v1/pair, GET /v1/get-device-info; no-store cache headers on pairing responses - HTTPServer: per-route middleware evaluation, decoded path extraction, socket peer source-address capture, no CORS headers - PairApprovalCoordinator: StateFlow<PendingRequest?> for Compose - PairingDialog: Compose AlertDialog with No (default, dismiss) / Yes once / Always allow (orange) - PairedClientsScreen: persistent / sessions / recently denied sections with revoke buttons - MainActivity / BrowserScreen / SettingsScreen: wire coordinator, dialog and Paired Clients entry Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- File-backed PairingStore at <AppSupport>/Kelpie/pairings.json (mode 0600, atomic
write, parent dir 0700). No Keychain per project policy. Stores SHA-256 hashes
only; plaintext tokens returned exactly once at issuance.
- AuthMiddleware enforces deny-by-default with an exact-match decoded-path
allowlist for {/v1/pair, /v1/pair/status, /v1/get-device-info, /health}.
Bearer header parsed strictly; constant-time hex compare. Duplicate
Authorization / duplicate Content-Length / Transfer-Encoding all rejected
before routing. POST /v1/pair gated by Content-Type + no Origin (CSRF).
- HTTPRequestParser splits parsing from routing; preserves existing 16KiB
header cap, 32MiB body cap, 30s deadline, strict Content-Length parsing,
and 411 for POSTs without Content-Length.
- PairEndpoints handles request / status / revoke; status poll is keyed by
requestId nonce and bound to the original source address.
- PairingHTTPResponse drops Access-Control-Allow-Origin (CLI is not a browser)
and sets Cache-Control: no-store on pair endpoints.
- PairApprovalCoordinator drives PairingDialog and PairedClientsView. Both
views use AppKit-backed NSButton via NSViewRepresentable per the WebView-
window CRITICAL rule. Default button is "No" (Return-bound, destructive
style); "Always allow" is orange to slow muscle-memory taps.
- ServerState wires PairingStore + coordinator into HTTPServer and refreshes
the coordinator on pending-pair-changed callbacks.
- SettingsView gains a Paired Clients row (AppKit-backed Manage button)
opening PairedClientsView; BrowserView hosts the pairing sheet so prompts
surface from any screen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New `packages/cli/src/auth/`:
- token-store.ts: `~/.kelpie/tokens.json` (mode 0600, atomic, parent dir 0700).
Refuses to operate on symlinked paths, refuses files with unsafe perms
unless owned by the current user. Keys tokens by `<deviceId>:<host>:<port>`
so mDNS spoofing at a new socket address forces a re-pair. Companion
`SessionTokenCache` keeps `Yes, once` tokens in process memory only.
- pairing.ts: POST /v1/pair + status-polling state machine. Returns
`approved | denied | expired | not_found | error`. Caller persists
persistent-scope tokens to disk and session-scope tokens to memory.
Also exports `redact()` / `redactObject()` helpers (Authorization header,
`token`/`bearer` body fields, bearer-shaped 40+-char regex).
- `packages/cli/src/client/http-client.ts` now sets `Authorization: Bearer …`
from the appropriate store and, on a `401`, runs the pair flow once before
retrying the original call. Stale tokens are dropped before the retry.
- New `kelpie pair` command (`packages/cli/src/commands/pair.ts`) for scripts
that want to prime credentials before automation runs. Registered in
`commands/index.ts` and in the MCP `kelpie_pair` tool.
- `kelpie mcp --http` now binds to `127.0.0.1` by default. Non-loopback
binding requires `--unsafe-host` and emits a loud warning that anyone
reaching the MCP port can drive paired devices.
- Docs: `docs/cli.md` documents `kelpie pair`; command-metadata adds the
pair entry consumed by `--llm-help`.
- Tests: 24 new unit tests covering token-store + pairing flow + MCP bind
safety; updated existing tool/help/server counts for the new `kelpie_pair`
MCP tool.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…lity Replaces the old "no authentication" notes with the actual deny-by-default bearer-token protocol shipped on iOS, Android, macOS, and the CLI: - architecture.md "Shared Network Risk": pairing flow, token lifecycle, source-address binding, CLI store layout, MCP loopback default. - api/README.md: bearer requirement on /v1, pairing endpoint table, CSRF gate, error codes, kelpie_pair MCP tool. - functionality.md: new Pairing & Authentication section between Device Discovery and Browser Control, with the three on-device responses and scope semantics. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 3-arg startForeground(id, notification, serviceType) overload was added in Android 10 (API 29 / Q); minSdk is 28. Branch on Build.VERSION.SDK_INT so API 28 uses the 2-arg call, while API 29+ still gets the connectedDevice service type. The manifest still declares foregroundServiceType=connectedDevice for runtime grant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the prior "no authentication on local network" stance with a deny-by-default bearer-token pairing protocol. All
/v1/*routes require a valid bearer token; the unauth allowlist is exactly/v1/pair,/v1/pair/status,/v1/get-device-info, and/health. Pairing requires explicit on-device approval (Yes once/Always/No). CLI stores token hashes in~/.kelpie/tokens.json(mode 0600, parent dir 0700), keyed bydeviceId:host:portto defeat mDNS spoofing.kelpie mcp --httpbinds to 127.0.0.1 by default; non-loopback requires explicit--unsafe-host.What lands
PairingStore,AuthMiddleware,PairEndpoints,PairApprovalCoordinator, on-device pairing dialog, paired-clients management UI. SHA-256 token hashing + constant-time comparison; CSPRNG-generated 32-byte request IDs; source-address binding on/v1/pair/status; CSRF gate onPOST /v1/pair(Content-Type + Origin);Cache-Control: no-storeon pairing responses; no CORS header anywhere (CLI is not a browser).kelpie paircommand,kelpie_pairMCP tool, deviceId+host+port fingerprint binding. Symlink rejection on the token store path.docs/architecture.md,docs/api/README.md, anddocs/functionality.mdupdated to describe the protocol, error codes, dialog flow, and store layout.Test plan
pnpm lint && pnpm build && pnpm testinpackages/cli(24 files, 362 tests pass)make lint-swift(0 violations across 182 files)./gradlew ktlintCheck(passes)./gradlew assembleDebug(compiles cleanly)kelpie pairkelpie mcp --http --bind 0.0.0.0rejection without--unsafe-hostNote:
./gradlew buildsurfaces a pre-existing lint failure inKelpieNetworkService.startForeground(from PR #58, already failing onmain). Out of scope for this PR.