Skip to content

auth: did:nostr resolver prefers local index before external (#423)#424

Merged
melvincarvalho merged 1 commit into
gh-pagesfrom
issue-423-did-nostr-local-first
May 12, 2026
Merged

auth: did:nostr resolver prefers local index before external (#423)#424
melvincarvalho merged 1 commit into
gh-pagesfrom
issue-423-did-nostr-local-first

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

When the requesting Nostr pubkey belongs to a local account, resolve to its WebID via the in-process well-known index instead of round-tripping through the configured external resolver.

Closes #423.

Why

The dominant SSO case is a user signing into their own pod. With the previous ordering inside resolveDidNostrToWebId, that case always reached the configured external resolver (https://nostr.social/.well-known/did/nostr/<pubkey> by default). When the external host is unreachable — network outage, expired SSL certificate, etc. — auth fails entirely even though the pod has the authoritative DID document for the user in memory.

Real-world incident that motivated this: nostr.social's SSL certificate expired, causing fetch() to fail with SSRF protection/fetch failed and the user's own write requests to come back 401 because the agent couldn't be resolved to a WebID.

What

In src/auth/did-nostr.js, resolveDidNostrToWebId now consults the local well-known index first (via dynamic import of resolveDidNostrLocally from src/idp/well-known-did-nostr.js) before any HTTP fetch. Falls back to the external resolver only when no local account claims the pubkey, so cross-pod identities still work.

The local binding is already verified by construction — the indexer only admits VMs declared in verificationMethod AND referenced from authentication of a local profile — so the backlink-verification round-trip the external path requires is also skipped on the local path.

Defense-in-depth

verifyNostrAuth (src/auth/nostr.js) already calls resolveDidNostrLocally ahead of resolveDidNostrToWebId in its resolver chain. Duplicating the local-first preference inside the resolver itself protects callers from other code paths (and any future caller that forgets the chain ordering) without behavioural cost.

Dynamic import keeps the IdP module optional: pods built/run without IdP don't pull it in, and any import or lookup error falls through to the external resolver.

Test plan

  • All 27 existing test/did-nostr.test.js cases pass
  • All 100 cases across test/well-known-did-nostr.test.js, test/nostr-event.test.js, test/nostr-cid-vm.test.js pass
  • Manual: deploy to solid.social and verify the user's write-after-NIP-98-SSO flow returns the correct WebID without hitting the external resolver. Confirmed locally via curl against the pod's own .well-known/did/nostr/<pubkey> returning the correct DID document with alsoKnownAs pointing at the WebID granted Write in the test ACL.

When the requesting Nostr pubkey belongs to a local account, resolve
to its WebID via the in-process well-known index instead of round-
tripping through the configured external resolver. The local binding
is already verified by construction (the index only admits VMs that
are declared in `verificationMethod` AND referenced from
`authentication` of a local profile), so we also skip the backlink
verification the external path requires.

Eliminates the most common SSO failure mode: user signs into their
own pod, the auth path tries the external resolver
(`nostr.social/.well-known/did/nostr/<pubkey>`) and the external host
is unreachable (network down, SSL cert expired, etc.). With this
change the external resolver is only consulted for cross-pod pubkeys
that no local account claims.

`verifyNostrAuth` (src/auth/nostr.js) already tries the local
resolver first in its chain; duplicating that ordering inside
`resolveDidNostrToWebId` itself is defense-in-depth — any caller from
another code path (or any future caller that forgets the chain
ordering) still gets the cheap, reliable answer for the dominant
case. Dynamic import keeps the IdP module optional for builds that
don't use it.

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 updates did:nostr → WebID resolution so that, when a pubkey belongs to a local account, the server resolves it via the in-process well-known index instead of always round-tripping through the configured external resolver (e.g. nostr.social). This improves reliability for the dominant “user signing into their own pod” SSO flow when external resolvers are unavailable.

Changes:

  • Reorders resolveDidNostrToWebId() to consult the local well-known index first via dynamic import of resolveDidNostrLocally.
  • Keeps the external resolver path as a fallback for non-local pubkeys, preserving cross-pod identity resolution.

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

Comment thread src/auth/did-nostr.js
Comment on lines +208 to +223
// Local-first: consult the in-process well-known index before any
// HTTP fetch. The index is built from local WebID profiles whose
// `verificationMethod` declares this Nostr pubkey AND is referenced
// from `authentication` (see src/idp/well-known-did-nostr.js) — so
// the binding is already verified by construction and we skip the
// backlink round-trip required for external answers.
//
// Dynamic import keeps the IdP module optional: pods built/run
// without the IdP layer don't pull it in. Any import or lookup
// error falls through to the external resolver.
try {
const { resolveDidNostrLocally } = await import('../idp/well-known-did-nostr.js');
const localWebId = await resolveDidNostrLocally(pubkey);
if (localWebId) return localWebId;
} catch {
// IdP module unavailable or local lookup threw — fall through.
Comment thread src/auth/did-nostr.js
Comment on lines +208 to +224
// Local-first: consult the in-process well-known index before any
// HTTP fetch. The index is built from local WebID profiles whose
// `verificationMethod` declares this Nostr pubkey AND is referenced
// from `authentication` (see src/idp/well-known-did-nostr.js) — so
// the binding is already verified by construction and we skip the
// backlink round-trip required for external answers.
//
// Dynamic import keeps the IdP module optional: pods built/run
// without the IdP layer don't pull it in. Any import or lookup
// error falls through to the external resolver.
try {
const { resolveDidNostrLocally } = await import('../idp/well-known-did-nostr.js');
const localWebId = await resolveDidNostrLocally(pubkey);
if (localWebId) return localWebId;
} catch {
// IdP module unavailable or local lookup threw — fall through.
}
@melvincarvalho
melvincarvalho merged commit f8904e9 into gh-pages May 12, 2026
4 checks passed
@melvincarvalho
melvincarvalho deleted the issue-423-did-nostr-local-first branch May 12, 2026 15:31
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.

did:nostr resolver should prefer local well-known endpoint before external

2 participants