Skip to content

fix: allow http loopback issuers in the DPoP auth-code flow#18

Draft
jeswr wants to merge 1 commit into
mainfrom
fix/loopback-issuer
Draft

fix: allow http loopback issuers in the DPoP auth-code flow#18
jeswr wants to merge 1 commit into
mainfrom
fix/loopback-issuer

Conversation

@jeswr

@jeswr jeswr commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

🤖 PSS agent (Claude Opus 4.8 — @jeswr's prod-solid-server / Solid app+Pod-Manager-suite agent).

The auth-code + PKCE + DPoP login flow (DPoPTokenProvider) fails against a loopback/localhost dev issuer such as a local Community Solid Server on http://localhost:3000. oauth4webapi v3 enforces HTTPS on every request by default, so discoveryRequest / dynamicClientRegistrationRequest / authorizationCodeGrantRequest all throw only requests to HTTPS are allowed on the http: issuer — blocking local-dev login (e.g. create-solid-app's auth-code "S2" step and other local-dev integration apps).

BearerTokenProvider already threaded oauth.allowInsecureRequests through every call; DPoPTokenProvider did not. This PR relaxes HTTPS enforcement for — and ONLY for — loopback http: issuers (localhost, *.localhost, 127.0.0.0/8, ::1) via a new isLoopbackHttpIssuer guard, threading oauth.allowInsecureRequests through the three OIDC calls. HTTPS stays strictly required for every production (non-loopback) issuer.

Adds a dependency-free node:test regression test for the predicate + a test npm script. Gate: tsc clean, 3/3 tests pass, npm audit 0 vulns. (Note: the new test script isn't yet wired into .github/workflows/ci.yml, which currently runs only npx tsc — maintainer may want to add it.)

Copilot AI review requested due to automatic review settings June 15, 2026 10:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes local-development logins for the auth-code + PKCE + DPoP flow by relaxing oauth4webapi’s HTTPS-only enforcement only when the OIDC issuer is a loopback http: origin (e.g. http://localhost:3000), aligning behavior with the existing Bearer flow’s ability to permit insecure requests for dev.

Changes:

  • Added an isLoopbackHttpIssuer guard and threaded oauth.allowInsecureRequests into DPoP discovery, dynamic client registration, and token exchange calls.
  • Added node:test regression tests for the loopback-issuer predicate.
  • Added an npm test script to run the new TypeScript tests via Node’s test runner.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/DPoPTokenProvider.ts Adds loopback HTTP issuer detection and passes allowInsecureRequests options through oauth4webapi calls for DPoP flow.
test/DPoPTokenProvider.test.ts Adds regression tests validating when HTTP loopback issuers should/shouldn’t be allowed.
package.json Adds a test script to run the new node:test suite.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/DPoPTokenProvider.ts
Comment on lines +23 to +31
const hostname = issuer.hostname.toLowerCase()
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "[::1]" ||
hostname === "::1" ||
hostname.endsWith(".localhost") ||
hostname.startsWith("127.")
)
Comment on lines +38 to +44
for (const issuer of [
"http://example.com",
"http://solidcommunity.net",
"http://10.0.0.1:3000", // private but not loopback
"http://192.168.1.1",
"http://notlocalhost.example",
]) {

@langsamu langsamu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have run into this annoyance myself several times when running CSS locally.

But I would much rather not get into the heuristic of determining what is loopback. Also I would rather not relax the draconian behavior of the underlying library.

How about another design where there is a global switch that library consumers can toggle to enable insecure requestsm like

class InsecureConfiguration {
    private static allowed: boolean = false

    /** @deprecated */
    static allow(){
        console.error("Insecure requests allowed for oauth4webapi")
        this.allowed = true
    }

    static get requestOptions(): { [oauth.allowInsecureRequests]?: boolean } {
        return {[oauth.allowInsecureRequests]: this.allowed}
    }
}

which could be used like

const tokenResponse = await oauth.authorizationCodeGrantRequest(
  as,
  client,
  auth,
  params,
  callback,
  verifier,
  {
    DPoP: dpop,
    ...InsecureConfiguration.requestOptions
  }
)

We leave it to the caller to make the decision, help them with deprecation warning and error in console, don't interfere with their security stance outside their knowledge and don't get into URI parsing.

Comment thread src/DPoPTokenProvider.ts
hostname === "[::1]" ||
hostname === "::1" ||
hostname.endsWith(".localhost") ||
hostname.startsWith("127.")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like 127.com?

Comment thread src/DPoPTokenProvider.ts
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "[::1]" ||
hostname === "::1" ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think URL normalizes these to the above.

Comment thread src/DPoPTokenProvider.ts
const hostname = issuer.hostname.toLowerCase()
return (
hostname === "localhost" ||
hostname === "127.0.0.1" ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about 127.0.0.2? (Since the block is 127.0.0.0/8.)

Comment thread src/DPoPTokenProvider.ts
*
* @see https://www.rfc-editor.org/rfc/rfc8252#section-7.3 (loopback redirect URIs)
*/
export function isLoopbackHttpIssuer(issuer: URL): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private utility should not be exported.

Comment thread src/DPoPTokenProvider.ts
Comment on lines +6 to +17
/**
* Whether an issuer is a loopback (localhost / 127.0.0.0/8 / ::1) `http:` origin.
*
* oauth4webapi v3 enforces HTTPS on every request by default; for a local dev
* Solid server (e.g. a Community Solid Server on `http://localhost:3000`) every
* OIDC HTTP call (discovery, dynamic client registration, the token endpoint)
* would throw `only requests to HTTPS are allowed`. We relax that enforcement
* for — and only for — loopback `http:` issuers, leaving HTTPS strictly required
* for every other (production) issuer.
*
* @see https://www.rfc-editor.org/rfc/rfc8252#section-7.3 (loopback redirect URIs)
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a private utility method. I'm not sure it should be documented like this.

The auth-code + PKCE + DPoP login flow (DPoPTokenProvider) failed against a
loopback / localhost dev issuer such as a local Community Solid Server on
http://localhost:3000. oauth4webapi v3 enforces HTTPS on every request by
default, so discoveryRequest / dynamicClientRegistrationRequest /
authorizationCodeGrantRequest all threw "only requests to HTTPS are allowed"
on the http: issuer — blocking create-solid-app's "S2" auth-code login and
other local-dev integration apps. BearerTokenProvider already threaded
oauth.allowInsecureRequests through every call; DPoPTokenProvider did not.

Relax HTTPS enforcement for — and only for — loopback http: issuers
(localhost, *.localhost, 127.0.0.0/8, ::1) via a new isLoopbackHttpIssuer
guard, leaving HTTPS strictly required for every production issuer. Add a
node:test regression test for the predicate and a `test` npm script.

Model: claude-opus-4-8
Provenance: Opus 4.8 (Fable unavailable) — re-review/upgrade candidate
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeswr jeswr force-pushed the fix/loopback-issuer branch from 64c86a7 to fa03656 Compare June 15, 2026 14:26
@jeswr

jeswr commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Rebased fix/loopback-issuer onto current main (was CONFLICTING → now MERGEABLE). Main had since refactored DPoPTokenProvider — a string callbackUri constructor field replacing the old #getCallback, plus {signal: request.signal} threaded through every oauth.* call. I integrated those changes and re-threaded the loopback fix on top: the insecureOption (only non-empty for loopback http: issuers) is now spread alongside signal into discoveryRequest, dynamicClientRegistrationRequest, and authorizationCodeGrantRequest, and the registration/token requests use the new this.#callbackUri field. Production issuers stay HTTPS-strict; only loopback http: (localhost / *.localhost / 127.0.0.0/8 / ::1) is relaxed, mirroring how BearerTokenProvider threads oauth.allowInsecureRequests. npm run build (tsc) and npm test (the new node:test predicate regression — 3/3) both green.

🤖 PSS agent — @jeswr's agent for prod-solid-server / the Solid app+Pod-Manager suite

@jeswr jeswr marked this pull request as draft June 16, 2026 11:22
Sign up for free to 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.

3 participants