Summary
Extend the Nostr authentication (Phase 1, #4) to link did:nostr identities to WebID profiles, enabling:
- Account Linking: Connect existing WebID accounts to Nostr keys
- Nostr-First Registration: Create new pods/WebIDs using only a Nostr key
- Identity Resolution: Authenticate with Nostr, resolve to canonical WebID
- Profile Enrichment: Import Nostr profile metadata (kind:0) into WebID
This creates a bridge between the Nostr ecosystem and Solid, allowing users to maintain a single cryptographic identity across both.
Background
Current State (Phase 1 ✓)
Phase 1 (#4) implemented direct did:nostr authentication:
Authorization: Nostr <token>
↓
Agent Identity: did:nostr:63fe6318dc58583...
↓
ACL Match: acl:agent <did:nostr:63fe6318dc58583...>
Limitations:
- No WebID profile (no name, avatar, preferences)
- Can't use WebID-specific features (solid:oidcIssuer, pim:storage)
- Different identity format than traditional Solid apps expect
- No discoverability via WebID lookup
Phase 2 Goal
Link did:nostr to a WebID profile:
Authorization: Nostr <token>
↓
Nostr Identity: did:nostr:63fe6318dc58583...
↓
Lookup: "Which WebID has owl:sameAs did:nostr:63fe6318..."?
↓
Canonical Identity: https://example.com/alice/#me
↓
ACL Match: acl:agent <https://example.com/alice/#me>
Specifications & Standards
Proposed Implementation
A. WebID Profile with Nostr Link
Add owl:sameAs and optional nostr:pubkey to WebID profiles:
Turtle Format:
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
@prefix owl: <http://www.w3.org/2002/07/owl#>.
@prefix nostr: <https://w3id.org/nostr/vocab#>.
<#me>
a foaf:Person;
foaf:name "Alice";
# Identity linking - this is the key relationship
owl:sameAs <did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed>;
# Optional: Raw pubkey for apps that need it
nostr:pubkey "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed";
# Optional: Nostr relays for discovery
nostr:relay "wss://relay.damus.io", "wss://nos.lol";
# Standard Solid profile properties
solid:oidcIssuer <https://example.com/>;
solid:storage <https://example.com/alice/>.
JSON-LD Format:
{
"@context": {
"foaf": "http://xmlns.com/foaf/0.1/",
"solid": "http://www.w3.org/ns/solid/terms#",
"owl": "http://www.w3.org/2002/07/owl#",
"nostr": "https://w3id.org/nostr/vocab#"
},
"@id": "#me",
"@type": "foaf:Person",
"foaf:name": "Alice",
"owl:sameAs": {
"@id": "did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
},
"nostr:pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"solid:oidcIssuer": { "@id": "https://example.com/" },
"solid:storage": { "@id": "https://example.com/alice/" }
}
B. Identity Resolution Flow
When a user authenticates with Nostr, resolve to their WebID:
┌─────────────────────────────────────────────────────────────────────────┐
│ Identity Resolution Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Nostr Authentication │
│ Authorization: Nostr <token> │
│ → Verify Schnorr signature │
│ → Extract: did:nostr:abc123... │
│ │
│ 2. WebID Lookup (New Step) │
│ → Query: SELECT ?webid WHERE { │
│ ?webid owl:sameAs <did:nostr:abc123...> } │
│ → Check local index OR │
│ → Scan account profiles │
│ │
│ 3. Identity Selection │
│ IF WebID found: │
│ → Use WebID as canonical identity │
│ → Set request.webId = "https://example.com/alice/#me" │
│ ELSE: │
│ → Use did:nostr directly (Phase 1 behavior) │
│ → Set request.webId = "did:nostr:abc123..." │
│ │
│ 4. ACL Checking │
│ → Check ACL for resolved identity │
│ → Both WebID and did:nostr can be in ACLs │
│ │
└─────────────────────────────────────────────────────────────────────────┘
C. Account Linking Flow
Allow existing users to link their Nostr key:
┌─────────────────────────────────────────────────────────────────────────┐
│ Account Linking Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. User logs in (traditional OIDC) │
│ → WebID: https://example.com/alice/#me │
│ │
│ 2. User visits /idp/settings/nostr │
│ → UI shows "Link Nostr Identity" │
│ → Prompts for NIP-07 extension signing │
│ │
│ 3. User signs challenge with Nostr key │
│ → Browser extension (nos2x, Alby) signs │
│ → Server receives signed challenge + pubkey │
│ │
│ 4. Server verifies and links │
│ → Verify Schnorr signature │
│ → Store nostrPubkey in account: { ..., nostrPubkey: "abc123" } │
│ → Update WebID profile with owl:sameAs │
│ │
│ 5. Future logins │
│ → User can authenticate with either: │
│ - Traditional OIDC (username/password) │
│ - Nostr NIP-98 (Schnorr signature) │
│ → Both resolve to same WebID │
│ │
└─────────────────────────────────────────────────────────────────────────┘
D. Nostr-First Registration Flow
Allow new users to create a pod using only their Nostr key:
┌─────────────────────────────────────────────────────────────────────────┐
│ Nostr-First Registration Flow │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. User visits /idp/register/nostr │
│ → UI: "Create Pod with Nostr" │
│ → Prompts for NIP-07 signature │
│ │
│ 2. User signs registration challenge │
│ → Challenge includes: server URL, timestamp, intent │
│ → Browser extension signs │
│ │
│ 3. Server verifies signature │
│ → Extract pubkey │
│ → Check if pubkey already linked to account │
│ │
│ 4. Optional: Fetch Nostr profile (kind:0) │
│ → Query relays for profile metadata │
│ → Extract: name, about, picture, nip05 │
│ │
│ 5. Create account and pod │
│ → Generate username from npub or NIP-05 │
│ → Create pod structure │
│ → Generate WebID profile with owl:sameAs │
│ → Store nostrPubkey in account │
│ │
│ 6. Return session │
│ → User is now logged in │
│ → WebID: https://example.com/npub1abc123/#me │
│ → Can authenticate via Nostr in future │
│ │
└─────────────────────────────────────────────────────────────────────────┘
E. Profile Enrichment from Nostr
Import profile metadata from Nostr relays:
Nostr kind:0 event:
{
"kind": 0,
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"content": "{\"name\":\"Alice\",\"about\":\"Solid + Nostr enthusiast\",\"picture\":\"https://example.com/alice.jpg\",\"nip05\":\"alice@example.com\"}",
"tags": []
}
Mapped to WebID profile:
<#me>
a foaf:Person;
foaf:name "Alice"; # from kind:0 name
foaf:bio "Solid + Nostr enthusiast"; # from kind:0 about
foaf:img <https://example.com/alice.jpg>; # from kind:0 picture
nostr:nip05 "alice@example.com"; # from kind:0 nip05
owl:sameAs <did:nostr:63fe6318...>.
Sync options:
- On registration: Import once when creating account
- On demand: User clicks "Sync from Nostr" in settings
- Periodic: Background job updates profiles (opt-in)
F. Bidirectional Linking (Optional)
For full interoperability, the Nostr profile can also link back to the WebID:
NIP-39: External Identity Claim
{
"kind": 0,
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"content": "{\"name\":\"Alice\"}",
"tags": [
["i", "solid:https://example.com/alice/#me", "<proof>"]
]
}
This allows Nostr clients to discover the user's Solid pod, and Solid apps to verify the bidirectional claim.
Files to Create/Modify
| File |
Change |
Priority |
src/idp/accounts.js |
Add nostrPubkey field to accounts |
High |
src/idp/nostr-link.js |
NEW - Nostr linking endpoints |
High |
src/idp/nostr-register.js |
NEW - Nostr-first registration |
High |
src/auth/nostr.js |
Add WebID resolution lookup |
High |
src/webid/profile.js |
Add owl:sameAs to profile generation |
High |
src/idp/views/nostr-link.html |
NEW - NIP-07 linking UI |
Medium |
src/idp/views/nostr-register.html |
NEW - Nostr registration UI |
Medium |
src/nostr/relay.js |
NEW - Relay query for kind:0 |
Medium |
src/nostr/profile-sync.js |
NEW - Profile import logic |
Low |
API Endpoints
Link Nostr to Existing Account
POST /idp/nostr/link
Request:
{
"challenge": "<server-provided-challenge>",
"signature": "<schnorr-signature>",
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
}
Response:
{
"success": true,
"webId": "https://example.com/alice/#me",
"didNostr": "did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
}
Nostr-First Registration
POST /idp/nostr/register
Request:
{
"challenge": "<server-provided-challenge>",
"signature": "<schnorr-signature>",
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"preferredUsername": "alice", // Optional
"importProfile": true // Optional: fetch kind:0
}
Response:
{
"success": true,
"webId": "https://example.com/alice/#me",
"podUrl": "https://example.com/alice/",
"accessToken": "<jwt-token>"
}
Get Linking Challenge
GET /idp/nostr/challenge
Response:
{
"challenge": "nostr-link:example.com:1766931639:randomhex",
"expiresAt": 1766931699
}
Lookup WebID by Nostr Pubkey
GET /idp/nostr/lookup/:pubkey
Response:
{
"pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
"webId": "https://example.com/alice/#me",
"linked": true
}
Database Schema Changes
Account Model Update
// Current account structure
{
id: "uuid",
username: "alice",
email: "alice@example.com",
passwordHash: "...",
webId: "https://example.com/alice/#me",
podName: "alice",
createdAt: "2024-01-01T00:00:00Z"
}
// Updated with Nostr
{
id: "uuid",
username: "alice",
email: "alice@example.com",
passwordHash: "...", // Optional if Nostr-only
webId: "https://example.com/alice/#me",
podName: "alice",
createdAt: "2024-01-01T00:00:00Z",
// New fields
nostrPubkey: "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
nostrLinkedAt: "2024-06-15T12:00:00Z",
nostrProfileSyncedAt: "2024-06-15T12:00:00Z", // Last kind:0 import
authMethods: ["password", "nostr"] // Available auth methods
}
Nostr-Pubkey Index
For fast WebID lookup by pubkey:
// Index file: data/.idp/nostr-index.json
{
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed": {
"accountId": "uuid-123",
"webId": "https://example.com/alice/#me"
},
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2": {
"accountId": "uuid-456",
"webId": "https://example.com/bob/#me"
}
}
Client-Side Integration (NIP-07)
Linking UI JavaScript
// Check for NIP-07 extension
if (!window.nostr) {
showError('Please install a Nostr extension (nos2x, Alby, etc.)');
return;
}
// Get pubkey
const pubkey = await window.nostr.getPublicKey();
console.log('Nostr pubkey:', pubkey);
// Get challenge from server
const { challenge } = await fetch('/idp/nostr/challenge').then(r => r.json());
// Sign challenge
const signature = await window.nostr.signEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', window.location.origin + '/idp/nostr/link'],
['method', 'POST'],
['challenge', challenge]
],
content: ''
});
// Submit to server
const result = await fetch('/idp/nostr/link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge,
signature: signature.sig,
pubkey
})
});
if (result.ok) {
showSuccess('Nostr identity linked!');
}
Security Considerations
Challenge-Response
Linking requires proving ownership of the Nostr key:
- Server generates random challenge with timestamp
- Challenge expires after 60 seconds
- User signs challenge with Nostr key
- Server verifies signature and timestamp
- Prevents replay attacks and unauthorized linking
One-to-One Mapping
- Each Nostr pubkey can only link to ONE WebID
- Each WebID can only link to ONE Nostr pubkey
- Prevents identity confusion and ACL bypass
Unlinking
Users should be able to unlink their Nostr key:
POST /idp/nostr/unlink
Authorization: Bearer <session-token>
→ Removes nostrPubkey from account
→ Removes owl:sameAs from WebID profile
→ Nostr auth no longer resolves to this WebID
Passwordless Accounts
If user registers with Nostr-only (no password):
- Must have Nostr key to authenticate
- Should prompt to add password as backup
- Consider recovery options (other linked accounts?)
Effort Estimate
| Task |
Effort |
Priority |
| Account schema update |
1 hour |
High |
| Nostr-pubkey index |
2 hours |
High |
| WebID resolution in auth |
2 hours |
High |
| Account linking API |
3 hours |
High |
| Linking UI (NIP-07) |
3 hours |
High |
| Nostr-first registration API |
3 hours |
Medium |
| Registration UI |
2 hours |
Medium |
| Profile generation update |
1 hour |
Medium |
| Kind:0 profile import |
4 hours |
Low |
| Relay query library |
3 hours |
Low |
| Bidirectional NIP-39 |
2 hours |
Low |
Total: ~26 hours
Testing Plan
Unit Tests
-
Account linking
- Valid signature links account
- Invalid signature rejected
- Duplicate pubkey rejected
- Challenge expiration works
-
WebID resolution
- Linked pubkey resolves to WebID
- Unlinked pubkey returns did:nostr
- Resolution cached properly
-
Profile generation
- owl:sameAs included when linked
- nostr:pubkey included when linked
- Unlinked profiles unchanged
Integration Tests
-
Full linking flow
- Login with password
- Link Nostr key
- Logout
- Login with Nostr
- Verify same WebID
-
Nostr-first registration
- Register with Nostr only
- Verify pod created
- Verify WebID has owl:sameAs
- Authenticate with Nostr
- Access pod resources
-
ACL behavior
- ACL with WebID grants access via Nostr auth
- ACL with did:nostr grants access (backward compat)
Migration Path
Existing Phase 1 Users
Users who created ACLs with did:nostr:pubkey:
# Old ACL (still works!)
acl:agent <did:nostr:63fe6318...>
After linking:
# New ACL (recommended)
acl:agent <https://example.com/alice/#me>
# Or both for transition period
acl:agent <https://example.com/alice/#me>;
acl:agent <did:nostr:63fe6318...>.
Backward Compatibility
- Phase 1 behavior preserved: unlinked Nostr auth returns
did:nostr
- ACLs with
did:nostr continue to work
- No breaking changes for existing deployments
Future Enhancements (Phase 3+)
- Multiple Nostr Keys: Link multiple keys to one WebID (different devices)
- Key Rotation: Replace linked key while preserving account
- Social Graph Import: Import Nostr follows as foaf:knows
- Relay Discovery: Advertise preferred relays in WebID
- NIP-05 Verification: Verify domain ownership via NIP-05
- Cross-Server Resolution: Federated pubkey→WebID lookup
References
Related Issues
Summary
Extend the Nostr authentication (Phase 1, #4) to link
did:nostridentities to WebID profiles, enabling:This creates a bridge between the Nostr ecosystem and Solid, allowing users to maintain a single cryptographic identity across both.
Background
Current State (Phase 1 ✓)
Phase 1 (#4) implemented direct
did:nostrauthentication:Limitations:
Phase 2 Goal
Link
did:nostrto a WebID profile:Specifications & Standards
Proposed Implementation
A. WebID Profile with Nostr Link
Add
owl:sameAsand optionalnostr:pubkeyto WebID profiles:Turtle Format:
JSON-LD Format:
{ "@context": { "foaf": "http://xmlns.com/foaf/0.1/", "solid": "http://www.w3.org/ns/solid/terms#", "owl": "http://www.w3.org/2002/07/owl#", "nostr": "https://w3id.org/nostr/vocab#" }, "@id": "#me", "@type": "foaf:Person", "foaf:name": "Alice", "owl:sameAs": { "@id": "did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" }, "nostr:pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "solid:oidcIssuer": { "@id": "https://example.com/" }, "solid:storage": { "@id": "https://example.com/alice/" } }B. Identity Resolution Flow
When a user authenticates with Nostr, resolve to their WebID:
C. Account Linking Flow
Allow existing users to link their Nostr key:
D. Nostr-First Registration Flow
Allow new users to create a pod using only their Nostr key:
E. Profile Enrichment from Nostr
Import profile metadata from Nostr relays:
Nostr kind:0 event:
{ "kind": 0, "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "content": "{\"name\":\"Alice\",\"about\":\"Solid + Nostr enthusiast\",\"picture\":\"https://example.com/alice.jpg\",\"nip05\":\"alice@example.com\"}", "tags": [] }Mapped to WebID profile:
Sync options:
F. Bidirectional Linking (Optional)
For full interoperability, the Nostr profile can also link back to the WebID:
NIP-39: External Identity Claim
{ "kind": 0, "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "content": "{\"name\":\"Alice\"}", "tags": [ ["i", "solid:https://example.com/alice/#me", "<proof>"] ] }This allows Nostr clients to discover the user's Solid pod, and Solid apps to verify the bidirectional claim.
Files to Create/Modify
src/idp/accounts.jsnostrPubkeyfield to accountssrc/idp/nostr-link.jssrc/idp/nostr-register.jssrc/auth/nostr.jssrc/webid/profile.jsowl:sameAsto profile generationsrc/idp/views/nostr-link.htmlsrc/idp/views/nostr-register.htmlsrc/nostr/relay.jssrc/nostr/profile-sync.jsAPI Endpoints
Link Nostr to Existing Account
POST /idp/nostr/link
Request:
{ "challenge": "<server-provided-challenge>", "signature": "<schnorr-signature>", "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" }Response:
{ "success": true, "webId": "https://example.com/alice/#me", "didNostr": "did:nostr:63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" }Nostr-First Registration
POST /idp/nostr/register
Request:
{ "challenge": "<server-provided-challenge>", "signature": "<schnorr-signature>", "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "preferredUsername": "alice", // Optional "importProfile": true // Optional: fetch kind:0 }Response:
{ "success": true, "webId": "https://example.com/alice/#me", "podUrl": "https://example.com/alice/", "accessToken": "<jwt-token>" }Get Linking Challenge
GET /idp/nostr/challenge
Response:
{ "challenge": "nostr-link:example.com:1766931639:randomhex", "expiresAt": 1766931699 }Lookup WebID by Nostr Pubkey
GET /idp/nostr/lookup/:pubkey
Response:
{ "pubkey": "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", "webId": "https://example.com/alice/#me", "linked": true }Database Schema Changes
Account Model Update
Nostr-Pubkey Index
For fast WebID lookup by pubkey:
Client-Side Integration (NIP-07)
Linking UI JavaScript
Security Considerations
Challenge-Response
Linking requires proving ownership of the Nostr key:
One-to-One Mapping
Unlinking
Users should be able to unlink their Nostr key:
Passwordless Accounts
If user registers with Nostr-only (no password):
Effort Estimate
Total: ~26 hours
Testing Plan
Unit Tests
Account linking
WebID resolution
Profile generation
Integration Tests
Full linking flow
Nostr-first registration
ACL behavior
Migration Path
Existing Phase 1 Users
Users who created ACLs with
did:nostr:pubkey:After linking:
Backward Compatibility
did:nostrdid:nostrcontinue to workFuture Enhancements (Phase 3+)
References
Related Issues