Skip to content

remote/oidc: PKCE + ephemeral loopback receiver (T1, T2) - #197

Merged
platypii merged 2 commits into
oidc-client-loginfrom
chunk1-pkce-loopback
Jun 29, 2026
Merged

remote/oidc: PKCE + ephemeral loopback receiver (T1, T2)#197
platypii merged 2 commits into
oidc-client-loginfrom
chunk1-pkce-loopback

Conversation

@platypii

Copy link
Copy Markdown
Contributor

Chunk 1 of the multi-tenant OIDC client login (LLP 0046-0048), milestone 1 local primitives.

What

  • src/core/remote/pkce.js - createPkcePair(), S256 challenge over stdlib crypto. Client's downstream PKCE leg (LLP 0046 D3).
  • src/core/remote/loopback.js - startLoopbackReceiver({ state, timeoutMs }), single-shot 127.0.0.1:0 listener on /callback; resolves { code } on a state-matched callback, rejects on mismatch / error= / timeout. RFC 8252 ephemeral redirect (LLP 0046 D2).

Tests

test/core/remote-pkce.test.js and test/core/remote-loopback.test.js, 7 tests green. npm run typecheck clean.

Base is the oidc-client-login integration branch, not master.

🤖 Generated with Claude Code

Two dependency-free local primitives for the browser login flow:
- pkce.js: createPkcePair() -> { verifier, challenge }, S256 over stdlib
crypto. The client's downstream PKCE leg (LLP 0046 D3).
- loopback.js: startLoopbackReceiver({ state, timeoutMs }) binds a
single-shot 127.0.0.1:0 HTTP listener serving /callback, returns its
redirectUri up front, and resolves { code } on a state-matched
callback (rejecting on mismatch, error=, or timeout). RFC 8252
ephemeral redirect (LLP 0046 D2).
Unit tests cover the SHA-256 challenge derivation, fresh randomness,
and the loopback success / state-mismatch / error / timeout paths.
@platypii

Copy link
Copy Markdown
ContributorAuthor

Review (independent agent review)

Verdict: approve-with-nits — PKCE encoding is correct (32-byte verifier, S256 = base64url(SHA-256(verifier)), padding/+// stripped) and the loopback's core guarantees (state-CSRF check before reading the code, single-shot close on first /callback, timeout rejection) hold and pass reliably. Conventions clean (no semicolons, no em dashes, @import types).

Findings

  • major — close() did not reject a pending waitForCode(). Calling close() while a waitForCode() promise was outstanding set settled and cleared the timeout but never called rejectCode, leaking a permanently-pending promise (a latent public-API hang).
  • minor — shared post-listen error handler. After listen resolved, a later server error event hit the pre-listen branch (rejectStart, a no-op) and left a pending waitForCode() unsettled and the server unclosed.
  • nits — test gaps: no close() test; no assertion that a non-/callback path 404s without consuming the single shot.

Addressed in this PR

Fixed both: close() now rejects an in-flight (or future) waitForCode() with "closed before a code arrived"; post-listen errors route through fail() (settle + close). Added a started flag, a close() test, and a non-/callback-probe test. 9 tests green, typecheck clean.

The state-mismatch-aborts-the-flow behavior is intentional and acceptable under the loopback-only threat model.

…-listen errors
Review follow-ups (PR #197):
- close() before a code arrives now rejects an in-flight (or future)
waitForCode() instead of leaving it permanently pending.
- a post-listen server 'error' now settles a pending waitForCode() and
closes the server (via fail()), not a no-op rejectStart.
- tests: close()-rejects-pending, and a non-/callback probe 404s without
consuming the single shot.
@platypii
platypii merged commit 0cc12cc into oidc-client-loginJun 29, 2026
6 checks passed
@platypii
platypii deleted the chunk1-pkce-loopback branch June 29, 2026 18:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@platypii