Title: Support did:key Authentication (LWS10 Spec) · Issue #86 · JavaScriptSolidServer/JavaScriptSolidServer · GitHub
Open Graph Title: Support did:key Authentication (LWS10 Spec) · Issue #86 · JavaScriptSolidServer/JavaScriptSolidServer
X Title: Support did:key Authentication (LWS10 Spec) · Issue #86 · JavaScriptSolidServer/JavaScriptSolidServer
Description: Support did:key Authentication (LWS10 Spec) 🎯 Motivation Backend services and autonomous agents need simple, secure authentication without the X.509 certificate ceremony of WebID-TLS. The W3C LWS protocol suite defines did:key authentica...
Open Graph Description: Support did:key Authentication (LWS10 Spec) 🎯 Motivation Backend services and autonomous agents need simple, secure authentication without the X.509 certificate ceremony of WebID-TLS. The W3C LWS p...
X Description: Support did:key Authentication (LWS10 Spec) 🎯 Motivation Backend services and autonomous agents need simple, secure authentication without the X.509 certificate ceremony of WebID-TLS. The W3C LWS p...
Opengraph URL: https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/86
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"Support did:key Authentication (LWS10 Spec)","articleBody":"# Support did:key Authentication (LWS10 Spec)\n\n## 🎯 Motivation\n\nBackend services and autonomous agents need simple, secure authentication without the X.509 certificate ceremony of WebID-TLS. The W3C LWS protocol suite defines **did:key authentication** - a modern approach using self-issued JWTs signed with cryptographic keypairs.\n\n**Current pain point:** WebID-TLS requires generating X.509 certificates, embedding public keys in WebID profiles, and configuring TLS. This is heavyweight for simple CLI tools and bot accounts.\n\n**What we want:** Agent generates an ed25519 keypair, derives a `did:key:z6Mk...` identifier, signs a JWT, done. No certificates, no profile updates needed.\n\n## 📋 Background\n\n### What is did:key?\n\nA **Decentralized Identifier** method where the public key IS the identifier:\n\n```\ndid:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH\n └─────────────────┬──────────────────────┘\n base58btc(public key)\n```\n\nThe public key is embedded in the DID itself - no need to fetch it from anywhere.\n\n### How it works\n\n1. **Agent generates keypair** (ed25519 or P-256)\n2. **Derives did:key** from public key\n3. **Creates self-signed JWT** with claims:\n ```json\n {\n \"sub\": \"did:key:z6Mk...\",\n \"iss\": \"did:key:z6Mk...\",\n \"client_id\": \"did:key:z6Mk...\",\n \"aud\": \"https://solid.social/\",\n \"exp\": 1234567890,\n \"iat\": 1234567800\n }\n ```\n4. **Signs JWT** with private key\n5. **Sends** `Authorization: Bearer \u003cjwt\u003e`\n6. **Server extracts** public key from DID\n7. **Verifies** JWT signature\n\n### Why this is better than WebID-TLS\n\n| WebID-TLS | did:key |\n|-----------|---------|\n| Generate X.509 certificate | Generate keypair |\n| Add cert to WebID profile | DID is self-describing |\n| HTTPS required | Works over HTTP |\n| Complex setup | Simple CLI workflow |\n| RSA (large keys) | Ed25519 (compact) |\n\n## 🔧 Technical Specification\n\n### LWS10 Requirements\n\nPer [W3C LWS Authentication Suite](https://github.com/w3c/lws-protocol/blob/main/lws10-authn-ssi-did-key/index.html):\n\n**Required JWT Claims:**\n- `sub`, `iss`, `client_id`: All MUST use same `did:key:` URI\n- `aud`: MUST include target authorization server\n- `exp`: Token expiration timestamp\n- `iat`: Token creation timestamp\n- `alg`: Cannot be \"none\"\n\n**Validation:**\n1. Extract public key from `did:key` identifier (per [DID Key spec](https://w3c-ccg.github.io/did-method-key/))\n2. Verify JWT signature using extracted key (RFC 7515)\n3. Validate claims (timestamps, audience, triple equality of sub/iss/client_id)\n4. Optional: Allow clock skew tolerance (±5 min)\n\n### Supported Key Types\n\n- **Ed25519** (recommended): `did:key:z6Mk...` (multicodec `0xed`)\n- **P-256**: `did:key:zDna...` (multicodec `0x1200`)\n- **secp256k1**: `did:key:zQ3s...` (multicodec `0xe7`)\n\n## 🛠️ Implementation Plan\n\n### Phase 1: Core Authentication (~2-3 hours)\n\n**New file:** `src/auth/did-key.js`\n\n```javascript\nimport { jwtVerify, importJWK } from 'jose';\nimport { base58btc } from 'multiformats/bases/base58';\nimport * as ed25519 from '@noble/ed25519';\n\n/**\n * Verify did:key JWT authentication\n * @param {string} token - Bearer token\n * @param {string} audience - Expected audience (server URL)\n * @returns {string} - WebID derived from did:key\n */\nexport async function verifyDidKeyJWT(token, audience) {\n // Decode without verification to get DID\n const { payload } = await jwtVerify(token, async (header, token) =\u003e {\n const did = token.payload.sub;\n \n if (!did?.startsWith('did:key:')) {\n throw new Error('Invalid DID format');\n }\n \n // Extract public key from did:key\n const publicKey = await didKeyToPublicKey(did);\n return publicKey;\n });\n \n // Validate LWS10 constraints\n if (payload.sub !== payload.iss || payload.sub !== payload.client_id) {\n throw new Error('sub, iss, and client_id must be identical');\n }\n \n if (!Array.isArray(payload.aud) || !payload.aud.includes(audience)) {\n throw new Error('Invalid audience');\n }\n \n if (!payload.exp || !payload.iat) {\n throw new Error('Missing exp or iat claims');\n }\n \n // Return WebID (in our case, same as DID for now)\n return payload.sub;\n}\n\n/**\n * Extract public key from did:key identifier\n * Supports ed25519, P-256, secp256k1\n */\nasync function didKeyToPublicKey(did) {\n const multibaseKey = did.replace('did:key:', '');\n const bytes = base58btc.decode(multibaseKey);\n \n // First 2 bytes are multicodec prefix\n const codec = (bytes[0] \u003c\u003c 8) | bytes[1];\n const publicKeyBytes = bytes.slice(2);\n \n switch (codec) {\n case 0xed: // Ed25519\n return await ed25519PublicKeyToJWK(publicKeyBytes);\n case 0x1200: // P-256\n return await p256PublicKeyToJWK(publicKeyBytes);\n default:\n throw new Error(`Unsupported key type: 0x${codec.toString(16)}`);\n }\n}\n\nasync function ed25519PublicKeyToJWK(publicKeyBytes) {\n return {\n kty: 'OKP',\n crv: 'Ed25519',\n x: Buffer.from(publicKeyBytes).toString('base64url')\n };\n}\n```\n\n**Update:** `src/auth/middleware.js`\n\n```javascript\nimport { verifyDidKeyJWT } from './did-key.js';\n\n// In authorize() function, add did:key support:\nif (authHeader?.startsWith('Bearer ')) {\n const token = authHeader.slice(7);\n \n try {\n // Try did:key JWT first\n const webId = await verifyDidKeyJWT(token, request.hostname);\n return { authorized: true, webId };\n } catch (didKeyErr) {\n // Fall through to existing token/OIDC validation\n }\n}\n```\n\n### Phase 2: CLI Tool Support (~1 hour)\n\n**New command:** `jss auth generate-did-key`\n\n```javascript\n// bin/jss.js\nprogram\n .command('auth')\n .command('generate-did-key')\n .description('Generate did:key credentials for agent authentication')\n .action(async () =\u003e {\n const { privateKey, publicKey } = await ed25519.utils.randomPrivateKey();\n const did = await publicKeyToDidKey(publicKey);\n \n console.log('Generated did:key credentials:');\n console.log(`\\nDID: ${did}`);\n console.log(`Private Key (hex): ${Buffer.from(privateKey).toString('hex')}`);\n console.log('\\nSave private key securely. Use it to sign JWTs for authentication.');\n });\n```\n\n### Phase 3: Documentation (~30 min)\n\nAdd to README.md:\n\n```markdown\n### did:key Authentication (Agents \u0026 Bots)\n\nFor autonomous agents, CLI tools, and backend services:\n\n**Generate credentials:**\n```bash\njss auth generate-did-key\n# DID: did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH\n# Private Key: 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60\n```\n\n**Create JWT and authenticate:**\n```javascript\nimport { SignJWT } from 'jose';\nimport * as ed25519 from '@noble/ed25519';\n\nconst privateKeyHex = '9d61b19...';\nconst did = 'did:key:z6Mk...';\n\nconst jwt = await new SignJWT({})\n .setProtectedHeader({ alg: 'EdDSA' })\n .setSubject(did)\n .setIssuer(did)\n .setAudience('https://solid.social/')\n .claim('client_id', did)\n .setExpirationTime('1h')\n .setIssuedAt()\n .sign(privateKeyFromHex(privateKeyHex));\n\n// Use JWT\nconst res = await fetch('https://solid.social/alice/private/', {\n headers: { 'Authorization': `Bearer ${jwt}` }\n});\n```\n\n**Benefits:**\n- No certificates or profile setup\n- Self-describing identifiers\n- Standard JWT format\n- Works over HTTP or HTTPS\n```\n\n### Phase 4: Testing (~1 hour)\n\n**New file:** `test/did-key.test.js`\n\n```javascript\nimport { describe, it } from 'node:test';\nimport assert from 'node:assert';\nimport { SignJWT, generateKeyPair } from 'jose';\nimport { verifyDidKeyJWT } from '../src/auth/did-key.js';\n\ndescribe('did:key Authentication', () =\u003e {\n it('should verify valid did:key JWT', async () =\u003e {\n const { publicKey, privateKey } = await generateKeyPair('EdDSA');\n const did = await publicKeyToDidKey(publicKey);\n \n const jwt = await new SignJWT({})\n .setProtectedHeader({ alg: 'EdDSA' })\n .setSubject(did)\n .setIssuer(did)\n .setAudience('https://example.com/')\n .claim('client_id', did)\n .setExpirationTime('1h')\n .setIssuedAt()\n .sign(privateKey);\n \n const webId = await verifyDidKeyJWT(jwt, 'https://example.com/');\n assert.strictEqual(webId, did);\n });\n \n it('should reject mismatched sub/iss/client_id', async () =\u003e {\n // Test validation logic\n });\n \n it('should reject invalid audience', async () =\u003e {\n // Test audience validation\n });\n});\n```\n\n## 📦 Dependencies\n\n**New:**\n- `multiformats` - Multibase/multicodec decoding\n- `@noble/ed25519` - Ed25519 crypto operations\n\n**Existing (already installed):**\n- `jose` - JWT verification\n\n## 🎯 Use Cases\n\n1. **CLI tools** - `jss-cli` authenticates to remote pods\n2. **Backend bots** - Automated agents that manage resources\n3. **Server-to-server** - Microservices accessing pod data\n4. **IoT devices** - Resource-constrained agents (Ed25519 is tiny)\n5. **LWS protocol compliance** - Interop with other LWS implementations\n\n## 🔗 References\n\n- [W3C LWS Authentication Suite](https://github.com/w3c/lws-protocol/blob/main/lws10-authn-ssi-did-key/index.html)\n- [DID Key Method Spec](https://w3c-ccg.github.io/did-method-key/)\n- [Multicodec Table](https://github.com/multiformats/multicodec/blob/master/table.csv)\n- [RFC 7515 - JSON Web Signature](https://datatracker.ietf.org/doc/html/rfc7515)\n\n## 🚀 Acceptance Criteria\n\n- [ ] Verify Ed25519 did:key JWTs\n- [ ] Support P-256 did:key JWTs\n- [ ] Validate all LWS10 required claims\n- [ ] CLI command to generate did:key credentials\n- [ ] Integration with existing auth middleware\n- [ ] Tests covering validation rules\n- [ ] Documentation with examples\n- [ ] No breaking changes to existing auth methods\n\n---\n\n**Estimated effort:** 4-5 hours total\n**Priority:** Medium (nice alternative to WebID-TLS)\n**Dependencies:** None (adds to existing auth stack)\n","author":{"url":"https://github.com/melvincarvalho","@type":"Person","name":"melvincarvalho"},"datePublished":"2026-01-14T14:31:23.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":0},"url":"https://github.com/86/JavaScriptSolidServer/issues/86"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:109a07ba-27cb-c34c-5d22-b4646907a18f |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | AF44:8B50E:BE52A42:F6B4895:6976FE0B |
| html-safe-nonce | 0aa8631346640e2fd2171c832a7c15359cb204a5ca8d1834f773f26f8722d762 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBRjQ0OjhCNTBFOkJFNTJBNDI6RjZCNDg5NTo2OTc2RkUwQiIsInZpc2l0b3JfaWQiOiIyNjgxNTA1OTExODI1NDMyMDc1IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 19ce88c0b3827bda32e6e3b5f6ccb5aca521c24ed7e1e55041f4e95b82550dbe |
| hovercard-subject-tag | issue:3813463515 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/JavaScriptSolidServer/JavaScriptSolidServer/86/issue_layout |
| twitter:image | https://opengraph.githubassets.com/c4a32ba94135f7ba31fbf50b2d8880d1df57d3fd5bd701b6db93fb6e9047436c/JavaScriptSolidServer/JavaScriptSolidServer/issues/86 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/c4a32ba94135f7ba31fbf50b2d8880d1df57d3fd5bd701b6db93fb6e9047436c/JavaScriptSolidServer/JavaScriptSolidServer/issues/86 |
| og:image:alt | Support did:key Authentication (LWS10 Spec) 🎯 Motivation Backend services and autonomous agents need simple, secure authentication without the X.509 certificate ceremony of WebID-TLS. The W3C LWS p... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | melvincarvalho |
| hostname | github.com |
| expected-hostname | github.com |
| None | 01d198479908d09a841b2febe8eb105a81af2af7d81830960fe0971e1f4adc09 |
| turbo-cache-control | no-preview |
| go-import | github.com/JavaScriptSolidServer/JavaScriptSolidServer git https://github.com/JavaScriptSolidServer/JavaScriptSolidServer.git |
| octolytics-dimension-user_id | 205442424 |
| octolytics-dimension-user_login | JavaScriptSolidServer |
| octolytics-dimension-repository_id | 958025407 |
| octolytics-dimension-repository_nwo | JavaScriptSolidServer/JavaScriptSolidServer |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 958025407 |
| octolytics-dimension-repository_network_root_nwo | JavaScriptSolidServer/JavaScriptSolidServer |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | f752335dbbea672610081196a1998e39aec5e14b |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width