Summary
Add support for Nostr-based authentication to JSS, enabling both:
- Single Sign-On (SSO): Login with Nostr keys to obtain a WebID/session
- Direct Authorization: Use
did:nostr:pubkey URIs directly in ACL files
This brings decentralized, passwordless authentication using Schnorr signatures (secp256k1) to Solid.
Background & Research
Relevant Specifications
Existing Implementations
Browser Extensions (Client-side signing)
- nos2x - Chrome extension implementing NIP-07
- Alby - Lightning + Nostr wallet with NIP-07
- nostr-keyx - Uses OS keychain/YubiKey for key storage
Proposed Implementation
Phase 1: Direct NIP-98 Authorization (ACL-based)
Goal: Accept Authorization: Nostr <base64-event> header for resource access
Flow:
Client JSS
| |
|-- Authorization: Nostr ... ->|
| | 1. Decode base64 event
| | 2. Verify Schnorr signature (BIP-340)
| | 3. Validate: kind=27235, timestamp, URL, method
| | 4. Extract pubkey → did:nostr:pubkey
| | 5. Check ACL for did:nostr:pubkey
|<-- 200 OK -------------------|
ACL Example:
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
<#nostrUser>
a acl:Authorization;
acl:agent <did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed>;
acl:accessTo </shared/data>;
acl:mode acl:Read, acl:Write.
Changes Required:
- Add
nostr-tools dependency
- Create
src/auth/nostr.js with NIP-98 validation
- Update
src/auth/token.js:getWebIdFromRequestAsync() to detect Nostr scheme
- Return
did:nostr:pubkey as the agent identifier (ACL checker already handles this!)
Phase 2: SSO with WebID Linking
Goal: Login with Nostr to get a session, optionally create/link a WebID
Two Sub-flows:
A. Nostr → Existing WebID (Account Linking)
User has both a Nostr key AND a JSS account. Link them:
- User logs in normally (username/password)
- User links Nostr pubkey in settings
- Account stores
nostrPubkey field
- WebID profile gets
owl:sameAs or foaf:account:
{
"@id": "https://example.com/alice/#me",
"owl:sameAs": "did:nostr:63fe6318dc58583..."
}
- Future logins can use NIP-98 to authenticate
B. Nostr-First Registration (New Users)
User has only a Nostr key, wants a pod:
- User visits
/idp/nostr-login
- Browser extension signs NIP-98 event
- JSS verifies signature, extracts pubkey
- If pubkey not linked → offer to create account:
- Auto-generate username from npub (e.g.,
npub1abc... → nostr_abc123)
- Create pod with WebID containing
owl:sameAs did:nostr:...
- Issue session token
WebID Profile with Nostr Identity:
{
"@context": {
"nostr": "https://w3id.org/nostr/vocab#",
"owl": "http://www.w3.org/2002/07/owl#"
},
"@id": "https://example.com/alice/#me",
"@type": "foaf:Person",
"foaf:name": "alice",
"owl:sameAs": "did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"nostr:pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
}
Phase 3: Enhanced Integration (Optional)
- Relay Discovery: Query Nostr relays for kind 0 (profile metadata) to populate WebID
- Bidirectional Linking: Add WebID to Nostr profile's
alsoKnownAs field
- NIP-97 Support: Implement
/.well-known/nostr/nip97 endpoint
Technical Details
NIP-98 Event Structure
{
"id": "<sha256-hash>",
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"created_at": 1682327852,
"kind": 27235,
"tags": [
["u", "https://example.com/resource"],
["method", "GET"],
["payload", "<sha256-of-body>"], // optional, for POST/PUT
["webid", "https://example.com/alice/#me"] // optional
],
"content": "",
"sig": "<schnorr-signature>"
}
Validation Requirements (per spec)
kind MUST be 27235
created_at within ±60 seconds
u tag MUST match absolute request URL
method tag MUST match HTTP method
sig MUST be valid secp256k1 Schnorr signature (BIP-340)
- Optional:
payload tag matches SHA-256 of request body
did:nostr Format
did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed
└─────────────────────────────────────────────────────────────────┘
64-char lowercase hex pubkey
Files to Modify/Create
| File |
Change |
package.json |
Add nostr-tools dependency |
src/auth/nostr.js |
NEW - NIP-98 validation logic |
src/auth/token.js |
Add Nostr scheme detection in getWebIdFromRequestAsync() |
src/idp/accounts.js |
Add nostrPubkey field to accounts |
src/idp/index.js |
Add /idp/nostr-login route |
src/idp/views.js |
Add Nostr login page with NIP-07 integration |
src/webid/profile.js |
Add owl:sameAs/nostr:pubkey to profile generation |
Effort Estimate
| Phase |
Effort |
Priority |
| Phase 1 (ACL auth) |
2-3 hours |
High |
| Phase 2a (Account linking) |
2 hours |
Medium |
| Phase 2b (Nostr-first registration) |
3 hours |
Medium |
| Phase 3 (Enhanced) |
4+ hours |
Low |
Security Considerations
- Replay Protection: 60-second timestamp window
- URL Binding: Event signed with exact URL prevents cross-site use
- Key Security: Private keys never leave client (NIP-07 extensions)
- HTTPS Required: Except for .onion services
References
Summary
Add support for Nostr-based authentication to JSS, enabling both:
did:nostr:pubkeyURIs directly in ACL filesThis brings decentralized, passwordless authentication using Schnorr signatures (secp256k1) to Solid.
Background & Research
Relevant Specifications
window.nostr)Existing Implementations
validateToken,unpackEvent,verifyEvent)Browser Extensions (Client-side signing)
Proposed Implementation
Phase 1: Direct NIP-98 Authorization (ACL-based)
Goal: Accept
Authorization: Nostr <base64-event>header for resource accessFlow:
ACL Example:
Changes Required:
nostr-toolsdependencysrc/auth/nostr.jswith NIP-98 validationsrc/auth/token.js:getWebIdFromRequestAsync()to detectNostrschemedid:nostr:pubkeyas the agent identifier (ACL checker already handles this!)Phase 2: SSO with WebID Linking
Goal: Login with Nostr to get a session, optionally create/link a WebID
Two Sub-flows:
A. Nostr → Existing WebID (Account Linking)
User has both a Nostr key AND a JSS account. Link them:
nostrPubkeyfieldowl:sameAsorfoaf:account:{ "@id": "https://example.com/alice/#me", "owl:sameAs": "did:nostr:63fe6318dc58583..." }B. Nostr-First Registration (New Users)
User has only a Nostr key, wants a pod:
/idp/nostr-loginnpub1abc...→nostr_abc123)owl:sameAs did:nostr:...WebID Profile with Nostr Identity:
{ "@context": { "nostr": "https://w3id.org/nostr/vocab#", "owl": "http://www.w3.org/2002/07/owl#" }, "@id": "https://example.com/alice/#me", "@type": "foaf:Person", "foaf:name": "alice", "owl:sameAs": "did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "nostr:pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" }Phase 3: Enhanced Integration (Optional)
alsoKnownAsfield/.well-known/nostr/nip97endpointTechnical Details
NIP-98 Event Structure
{ "id": "<sha256-hash>", "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "created_at": 1682327852, "kind": 27235, "tags": [ ["u", "https://example.com/resource"], ["method", "GET"], ["payload", "<sha256-of-body>"], // optional, for POST/PUT ["webid", "https://example.com/alice/#me"] // optional ], "content": "", "sig": "<schnorr-signature>" }Validation Requirements (per spec)
kindMUST be 27235created_atwithin ±60 secondsutag MUST match absolute request URLmethodtag MUST match HTTP methodsigMUST be valid secp256k1 Schnorr signature (BIP-340)payloadtag matches SHA-256 of request bodydid:nostr Format
Files to Modify/Create
package.jsonnostr-toolsdependencysrc/auth/nostr.jssrc/auth/token.jsgetWebIdFromRequestAsync()src/idp/accounts.jsnostrPubkeyfield to accountssrc/idp/index.js/idp/nostr-loginroutesrc/idp/views.jssrc/webid/profile.jsowl:sameAs/nostr:pubkeyto profile generationEffort Estimate
Security Considerations
References