this repo has no description
1# Authentication & Sessions Implementation Plan 2 3> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 5**Goal:** Add authentication to the PDS so users can login from bsky.app and create records. 6 7**Architecture:** JWT-based authentication using HMAC-SHA256. Password checked against `PDS_PASSWORD` env var. Access tokens expire in 2 hours, refresh tokens in 90 days. Write endpoints (`createRecord`, `deleteRecord`) require valid access token. 8 9**Tech Stack:** Web Crypto API for HMAC signing, manual JWT encoding/decoding (no external deps) 10 11--- 12 13## Task 1: Add JWT Helper Functions 14 15**Files:** 16- Modify: `src/pds.js:461-469` (after existing `base64UrlDecode`) 17 18**Step 1: Write failing test for base64url encode/decode** 19 20Add to `test/pds.test.js`: 21 22```javascript 23import { 24 cborEncode, cborDecode, createCid, cidToString, cidToBytes, base32Encode, createTid, 25 generateKeyPair, importPrivateKey, sign, bytesToHex, hexToBytes, 26 getKeyDepth, varint, base32Decode, buildCarFile, 27 base64UrlEncode, base64UrlDecode 28} from '../src/pds.js' 29 30// Add new test block after existing tests: 31 32describe('JWT Base64URL', () => { 33 test('base64UrlEncode encodes bytes correctly', () => { 34 const input = new TextEncoder().encode('hello world') 35 const encoded = base64UrlEncode(input) 36 assert.strictEqual(encoded, 'aGVsbG8gd29ybGQ') 37 assert.ok(!encoded.includes('+')) 38 assert.ok(!encoded.includes('/')) 39 assert.ok(!encoded.includes('=')) 40 }) 41 42 test('base64UrlDecode decodes string correctly', () => { 43 const decoded = base64UrlDecode('aGVsbG8gd29ybGQ') 44 const str = new TextDecoder().decode(decoded) 45 assert.strictEqual(str, 'hello world') 46 }) 47 48 test('base64url roundtrip', () => { 49 const original = new Uint8Array([0, 1, 2, 255, 254, 253]) 50 const encoded = base64UrlEncode(original) 51 const decoded = base64UrlDecode(encoded) 52 assert.deepStrictEqual(decoded, original) 53 }) 54}) 55``` 56 57**Step 2: Run test to verify it fails** 58 59Run: `npm test -- --test-name-pattern "JWT Base64URL"` 60Expected: FAIL with "base64UrlEncode is not exported" 61 62**Step 3: Implement base64url functions** 63 64In `src/pds.js`, replace the existing `base64UrlDecode` function (around line 461) and add `base64UrlEncode`: 65 66```javascript 67/** 68 * Encode bytes as base64url string (no padding) 69 * @param {Uint8Array} bytes - Bytes to encode 70 * @returns {string} Base64url-encoded string 71 */ 72export function base64UrlEncode(bytes) { 73 let binary = '' 74 for (const byte of bytes) { 75 binary += String.fromCharCode(byte) 76 } 77 const base64 = btoa(binary) 78 return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') 79} 80 81/** 82 * Decode base64url string to bytes 83 * @param {string} str - Base64url-encoded string 84 * @returns {Uint8Array} Decoded bytes 85 */ 86export function base64UrlDecode(str) { 87 const base64 = str.replace(/-/g, '+').replace(/_/g, '/') 88 const pad = base64.length % 4 89 const padded = pad ? base64 + '='.repeat(4 - pad) : base64 90 const binary = atob(padded) 91 const bytes = new Uint8Array(binary.length) 92 for (let i = 0; i < binary.length; i++) { 93 bytes[i] = binary.charCodeAt(i) 94 } 95 return bytes 96} 97``` 98 99**Step 4: Run test to verify it passes** 100 101Run: `npm test -- --test-name-pattern "JWT Base64URL"` 102Expected: PASS 103 104**Step 5: Commit** 105 106```bash 107git add src/pds.js test/pds.test.js 108git commit -m "feat: add base64url encode/decode helpers for JWT" 109``` 110 111--- 112 113## Task 2: Add JWT Creation Functions 114 115**Files:** 116- Modify: `src/pds.js` (add after base64url functions, around line 490) 117 118**Step 1: Write failing test for JWT creation** 119 120Add to `test/pds.test.js`: 121 122```javascript 123import { 124 // ... existing imports ... 125 base64UrlEncode, base64UrlDecode, 126 createAccessJwt, createRefreshJwt 127} from '../src/pds.js' 128 129describe('JWT Creation', () => { 130 test('createAccessJwt creates valid JWT structure', async () => { 131 const did = 'did:web:test.example' 132 const secret = 'test-secret-key' 133 const jwt = await createAccessJwt(did, secret) 134 135 const parts = jwt.split('.') 136 assert.strictEqual(parts.length, 3) 137 138 // Decode header 139 const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[0]))) 140 assert.strictEqual(header.typ, 'at+jwt') 141 assert.strictEqual(header.alg, 'HS256') 142 143 // Decode payload 144 const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[1]))) 145 assert.strictEqual(payload.scope, 'com.atproto.access') 146 assert.strictEqual(payload.sub, did) 147 assert.strictEqual(payload.aud, did) 148 assert.ok(payload.iat > 0) 149 assert.ok(payload.exp > payload.iat) 150 }) 151 152 test('createRefreshJwt creates valid JWT with jti', async () => { 153 const did = 'did:web:test.example' 154 const secret = 'test-secret-key' 155 const jwt = await createRefreshJwt(did, secret) 156 157 const parts = jwt.split('.') 158 const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[0]))) 159 assert.strictEqual(header.typ, 'refresh+jwt') 160 161 const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(parts[1]))) 162 assert.strictEqual(payload.scope, 'com.atproto.refresh') 163 assert.ok(payload.jti) // has unique token ID 164 }) 165}) 166``` 167 168**Step 2: Run test to verify it fails** 169 170Run: `npm test -- --test-name-pattern "JWT Creation"` 171Expected: FAIL with "createAccessJwt is not exported" 172 173**Step 3: Implement JWT creation functions** 174 175Add to `src/pds.js` after base64url functions: 176 177```javascript 178/** 179 * Create HMAC-SHA256 signature for JWT 180 * @param {string} data - Data to sign (header.payload) 181 * @param {string} secret - Secret key 182 * @returns {Promise<string>} Base64url-encoded signature 183 */ 184async function hmacSign(data, secret) { 185 const key = await crypto.subtle.importKey( 186 'raw', 187 new TextEncoder().encode(secret), 188 { name: 'HMAC', hash: 'SHA-256' }, 189 false, 190 ['sign'] 191 ) 192 const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data)) 193 return base64UrlEncode(new Uint8Array(sig)) 194} 195 196/** 197 * Create an access JWT for ATProto 198 * @param {string} did - User's DID (subject and audience) 199 * @param {string} secret - JWT signing secret 200 * @param {number} [expiresIn=7200] - Expiration in seconds (default 2 hours) 201 * @returns {Promise<string>} Signed JWT 202 */ 203export async function createAccessJwt(did, secret, expiresIn = 7200) { 204 const header = { typ: 'at+jwt', alg: 'HS256' } 205 const now = Math.floor(Date.now() / 1000) 206 const payload = { 207 scope: 'com.atproto.access', 208 sub: did, 209 aud: did, 210 iat: now, 211 exp: now + expiresIn 212 } 213 214 const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header))) 215 const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload))) 216 const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret) 217 218 return `${headerB64}.${payloadB64}.${signature}` 219} 220 221/** 222 * Create a refresh JWT for ATProto 223 * @param {string} did - User's DID (subject and audience) 224 * @param {string} secret - JWT signing secret 225 * @param {number} [expiresIn=7776000] - Expiration in seconds (default 90 days) 226 * @returns {Promise<string>} Signed JWT 227 */ 228export async function createRefreshJwt(did, secret, expiresIn = 7776000) { 229 const header = { typ: 'refresh+jwt', alg: 'HS256' } 230 const now = Math.floor(Date.now() / 1000) 231 // Generate random jti (token ID) 232 const jtiBytes = new Uint8Array(32) 233 crypto.getRandomValues(jtiBytes) 234 const jti = base64UrlEncode(jtiBytes) 235 236 const payload = { 237 scope: 'com.atproto.refresh', 238 sub: did, 239 aud: did, 240 jti, 241 iat: now, 242 exp: now + expiresIn 243 } 244 245 const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header))) 246 const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload))) 247 const signature = await hmacSign(`${headerB64}.${payloadB64}`, secret) 248 249 return `${headerB64}.${payloadB64}.${signature}` 250} 251``` 252 253**Step 4: Run test to verify it passes** 254 255Run: `npm test -- --test-name-pattern "JWT Creation"` 256Expected: PASS 257 258**Step 5: Commit** 259 260```bash 261git add src/pds.js test/pds.test.js 262git commit -m "feat: add JWT creation functions for access and refresh tokens" 263``` 264 265--- 266 267## Task 3: Add JWT Verification Function 268 269**Files:** 270- Modify: `src/pds.js` (add after JWT creation functions) 271 272**Step 1: Write failing test for JWT verification** 273 274Add to `test/pds.test.js`: 275 276```javascript 277import { 278 // ... existing imports ... 279 createAccessJwt, createRefreshJwt, 280 verifyAccessJwt 281} from '../src/pds.js' 282 283describe('JWT Verification', () => { 284 test('verifyAccessJwt returns payload for valid token', async () => { 285 const did = 'did:web:test.example' 286 const secret = 'test-secret-key' 287 const jwt = await createAccessJwt(did, secret) 288 289 const payload = await verifyAccessJwt(jwt, secret) 290 assert.strictEqual(payload.sub, did) 291 assert.strictEqual(payload.scope, 'com.atproto.access') 292 }) 293 294 test('verifyAccessJwt throws for wrong secret', async () => { 295 const did = 'did:web:test.example' 296 const jwt = await createAccessJwt(did, 'correct-secret') 297 298 await assert.rejects( 299 () => verifyAccessJwt(jwt, 'wrong-secret'), 300 /invalid signature/i 301 ) 302 }) 303 304 test('verifyAccessJwt throws for expired token', async () => { 305 const did = 'did:web:test.example' 306 const secret = 'test-secret-key' 307 // Create token that expired 1 second ago 308 const jwt = await createAccessJwt(did, secret, -1) 309 310 await assert.rejects( 311 () => verifyAccessJwt(jwt, secret), 312 /expired/i 313 ) 314 }) 315 316 test('verifyAccessJwt throws for refresh token', async () => { 317 const did = 'did:web:test.example' 318 const secret = 'test-secret-key' 319 const jwt = await createRefreshJwt(did, secret) 320 321 await assert.rejects( 322 () => verifyAccessJwt(jwt, secret), 323 /invalid token type/i 324 ) 325 }) 326}) 327``` 328 329**Step 2: Run test to verify it fails** 330 331Run: `npm test -- --test-name-pattern "JWT Verification"` 332Expected: FAIL with "verifyAccessJwt is not exported" 333 334**Step 3: Implement JWT verification** 335 336Add to `src/pds.js` after JWT creation functions: 337 338```javascript 339/** 340 * Verify and decode an access JWT 341 * @param {string} jwt - JWT string to verify 342 * @param {string} secret - JWT signing secret 343 * @returns {Promise<Object>} Decoded payload 344 * @throws {Error} If token is invalid, expired, or wrong type 345 */ 346export async function verifyAccessJwt(jwt, secret) { 347 const parts = jwt.split('.') 348 if (parts.length !== 3) { 349 throw new Error('Invalid JWT format') 350 } 351 352 const [headerB64, payloadB64, signatureB64] = parts 353 354 // Verify signature 355 const expectedSig = await hmacSign(`${headerB64}.${payloadB64}`, secret) 356 if (signatureB64 !== expectedSig) { 357 throw new Error('Invalid signature') 358 } 359 360 // Decode header and payload 361 const header = JSON.parse(new TextDecoder().decode(base64UrlDecode(headerB64))) 362 const payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64))) 363 364 // Check token type 365 if (header.typ !== 'at+jwt') { 366 throw new Error('Invalid token type: expected access token') 367 } 368 369 // Check expiration 370 const now = Math.floor(Date.now() / 1000) 371 if (payload.exp && payload.exp < now) { 372 throw new Error('Token expired') 373 } 374 375 return payload 376} 377``` 378 379**Step 4: Run test to verify it passes** 380 381Run: `npm test -- --test-name-pattern "JWT Verification"` 382Expected: PASS 383 384**Step 5: Commit** 385 386```bash 387git add src/pds.js test/pds.test.js 388git commit -m "feat: add JWT verification function" 389``` 390 391--- 392 393## Task 4: Add createSession Endpoint 394 395**Files:** 396- Modify: `src/pds.js:869-940` (add to pdsRoutes) 397- Modify: `src/pds.js` (add handler method to PersonalDataServer class) 398 399**Step 1: Add route to pdsRoutes** 400 401In `src/pds.js`, add to the `pdsRoutes` object (around line 902, after describeServer): 402 403```javascript 404 '/xrpc/com.atproto.server.createSession': { 405 method: 'POST', 406 handler: (pds, req, url) => pds.handleCreateSession(req) 407 }, 408``` 409 410**Step 2: Add handler method** 411 412Add to `PersonalDataServer` class (after `handleDescribeServer`, around line 1427): 413 414```javascript 415 async handleCreateSession(request) { 416 const body = await request.json() 417 const { identifier, password } = body 418 419 if (!identifier || !password) { 420 return Response.json({ 421 error: 'InvalidRequest', 422 message: 'Missing identifier or password' 423 }, { status: 400 }) 424 } 425 426 // Check password against env var 427 const expectedPassword = this.env?.PDS_PASSWORD 428 if (!expectedPassword || password !== expectedPassword) { 429 return Response.json({ 430 error: 'AuthenticationRequired', 431 message: 'Invalid identifier or password' 432 }, { status: 401 }) 433 } 434 435 // Resolve identifier to DID 436 let did = identifier 437 if (!identifier.startsWith('did:')) { 438 // Try to resolve handle 439 const handleMap = await this.state.storage.get('handleMap') || {} 440 did = handleMap[identifier] 441 if (!did) { 442 return Response.json({ 443 error: 'InvalidRequest', 444 message: 'Unable to resolve handle' 445 }, { status: 400 }) 446 } 447 } 448 449 // Get handle for response 450 const handle = await this.getHandleForDid(did) 451 452 // Create tokens 453 const jwtSecret = this.env?.JWT_SECRET 454 if (!jwtSecret) { 455 return Response.json({ 456 error: 'InternalServerError', 457 message: 'Server not configured for authentication' 458 }, { status: 500 }) 459 } 460 461 const accessJwt = await createAccessJwt(did, jwtSecret) 462 const refreshJwt = await createRefreshJwt(did, jwtSecret) 463 464 return Response.json({ 465 accessJwt, 466 refreshJwt, 467 handle: handle || did, 468 did, 469 active: true 470 }) 471 } 472 473 async getHandleForDid(did) { 474 // Check if this DID has a handle registered 475 const handleMap = await this.state.storage.get('handleMap') || {} 476 for (const [handle, mappedDid] of Object.entries(handleMap)) { 477 if (mappedDid === did) return handle 478 } 479 // Check instance's own handle 480 const instanceDid = await this.getDid() 481 if (instanceDid === did) { 482 return await this.state.storage.get('handle') 483 } 484 return null 485 } 486``` 487 488**Step 3: Add route in main handleRequest** 489 490In `src/pds.js`, in the `handleRequest` function (around line 1796), add handling for createSession right after describeServer: 491 492```javascript 493 // createSession - handle on default DO (has handleMap for identifier resolution) 494 if (url.pathname === '/xrpc/com.atproto.server.createSession') { 495 const defaultId = env.PDS.idFromName('default') 496 const defaultPds = env.PDS.get(defaultId) 497 return defaultPds.fetch(request) 498 } 499``` 500 501**Step 4: Test manually** 502 503Deploy and test: 504```bash 505npx wrangler deploy 506curl -X POST 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.server.createSession' \ 507 -H 'Content-Type: application/json' \ 508 -d '{"identifier":"chad-pds.chad-53c.workers.dev","password":"YOUR_PASSWORD"}' 509``` 510 511Expected: JSON response with `accessJwt`, `refreshJwt`, `handle`, `did`, `active` 512 513**Step 5: Commit** 514 515```bash 516git add src/pds.js 517git commit -m "feat: add com.atproto.server.createSession endpoint" 518``` 519 520--- 521 522## Task 5: Add getSession Endpoint 523 524**Files:** 525- Modify: `src/pds.js` (add route and handler) 526 527**Step 1: Add route to pdsRoutes** 528 529In `src/pds.js`, add to the `pdsRoutes` object (after createSession): 530 531```javascript 532 '/xrpc/com.atproto.server.getSession': { 533 handler: (pds, req, url) => pds.handleGetSession(req) 534 }, 535``` 536 537**Step 2: Add handler method** 538 539Add to `PersonalDataServer` class (after `handleCreateSession`): 540 541```javascript 542 async handleGetSession(request) { 543 const authHeader = request.headers.get('Authorization') 544 if (!authHeader || !authHeader.startsWith('Bearer ')) { 545 return Response.json({ 546 error: 'AuthenticationRequired', 547 message: 'Missing or invalid authorization header' 548 }, { status: 401 }) 549 } 550 551 const token = authHeader.slice(7) // Remove 'Bearer ' 552 const jwtSecret = this.env?.JWT_SECRET 553 if (!jwtSecret) { 554 return Response.json({ 555 error: 'InternalServerError', 556 message: 'Server not configured for authentication' 557 }, { status: 500 }) 558 } 559 560 try { 561 const payload = await verifyAccessJwt(token, jwtSecret) 562 const did = payload.sub 563 const handle = await this.getHandleForDid(did) 564 565 return Response.json({ 566 handle: handle || did, 567 did, 568 active: true 569 }) 570 } catch (err) { 571 return Response.json({ 572 error: 'InvalidToken', 573 message: err.message 574 }, { status: 401 }) 575 } 576 } 577``` 578 579**Step 3: Add route in main handleRequest** 580 581In `src/pds.js`, in the `handleRequest` function, add handling for getSession (after createSession): 582 583```javascript 584 // getSession - route to default DO 585 if (url.pathname === '/xrpc/com.atproto.server.getSession') { 586 const defaultId = env.PDS.idFromName('default') 587 const defaultPds = env.PDS.get(defaultId) 588 return defaultPds.fetch(request) 589 } 590``` 591 592**Step 4: Test manually** 593 594```bash 595# First get a token 596TOKEN=$(curl -s -X POST 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.server.createSession' \ 597 -H 'Content-Type: application/json' \ 598 -d '{"identifier":"chad-pds.chad-53c.workers.dev","password":"YOUR_PASSWORD"}' | jq -r '.accessJwt') 599 600# Then test getSession 601curl 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.server.getSession' \ 602 -H "Authorization: Bearer $TOKEN" 603``` 604 605Expected: JSON response with `handle`, `did`, `active` 606 607**Step 5: Commit** 608 609```bash 610git add src/pds.js 611git commit -m "feat: add com.atproto.server.getSession endpoint" 612``` 613 614--- 615 616## Task 6: Add Auth Middleware and Protect Write Endpoints 617 618**Files:** 619- Modify: `src/pds.js` (add requireAuth helper, modify createRecord/deleteRecord handlers) 620 621**Step 1: Add requireAuth helper function** 622 623Add to `src/pds.js` (before the `handleRequest` function, around line 1774): 624 625```javascript 626/** 627 * Verify auth and return DID from token 628 * @param {Request} request - HTTP request with Authorization header 629 * @param {Object} env - Environment with JWT_SECRET 630 * @returns {Promise<{did: string} | {error: Response}>} DID or error response 631 */ 632async function requireAuth(request, env) { 633 const authHeader = request.headers.get('Authorization') 634 if (!authHeader || !authHeader.startsWith('Bearer ')) { 635 return { 636 error: Response.json({ 637 error: 'AuthenticationRequired', 638 message: 'Authentication required' 639 }, { status: 401 }) 640 } 641 } 642 643 const token = authHeader.slice(7) 644 const jwtSecret = env?.JWT_SECRET 645 if (!jwtSecret) { 646 return { 647 error: Response.json({ 648 error: 'InternalServerError', 649 message: 'Server not configured for authentication' 650 }, { status: 500 }) 651 } 652 } 653 654 try { 655 const payload = await verifyAccessJwt(token, jwtSecret) 656 return { did: payload.sub } 657 } catch (err) { 658 return { 659 error: Response.json({ 660 error: 'InvalidToken', 661 message: err.message 662 }, { status: 401 }) 663 } 664 } 665} 666``` 667 668**Step 2: Modify createRecord in handleRequest** 669 670In `src/pds.js`, find the createRecord handling in `handleRequest` (around line 1854) and update it: 671 672```javascript 673 // POST repo endpoints have repo in body - REQUIRE AUTH 674 if (url.pathname === '/xrpc/com.atproto.repo.createRecord') { 675 // Check auth first 676 const auth = await requireAuth(request, env) 677 if (auth.error) return auth.error 678 679 // Clone request to read body 680 const body = await request.json() 681 const repo = body.repo 682 if (!repo) { 683 return Response.json({ error: 'InvalidRequest', message: 'missing repo param' }, { status: 400 }) 684 } 685 686 // Verify authenticated user matches repo 687 if (auth.did !== repo) { 688 return Response.json({ 689 error: 'Forbidden', 690 message: 'Cannot write to another user\'s repo' 691 }, { status: 403 }) 692 } 693 694 const id = env.PDS.idFromName(repo) 695 const pds = env.PDS.get(id) 696 return pds.fetch(new Request(request.url, { 697 method: 'POST', 698 headers: request.headers, 699 body: JSON.stringify(body) 700 })) 701 } 702``` 703 704**Step 3: Modify deleteRecord in handleRequest** 705 706Update the deleteRecord handling similarly: 707 708```javascript 709 if (url.pathname === '/xrpc/com.atproto.repo.deleteRecord') { 710 // Check auth first 711 const auth = await requireAuth(request, env) 712 if (auth.error) return auth.error 713 714 const body = await request.json() 715 const repo = body.repo 716 if (!repo) { 717 return Response.json({ error: 'InvalidRequest', message: 'missing repo param' }, { status: 400 }) 718 } 719 720 // Verify authenticated user matches repo 721 if (auth.did !== repo) { 722 return Response.json({ 723 error: 'Forbidden', 724 message: 'Cannot modify another user\'s repo' 725 }, { status: 403 }) 726 } 727 728 const id = env.PDS.idFromName(repo) 729 const pds = env.PDS.get(id) 730 return pds.fetch(new Request(request.url, { 731 method: 'POST', 732 headers: request.headers, 733 body: JSON.stringify(body) 734 })) 735 } 736``` 737 738**Step 4: Test auth protection** 739 740```bash 741# Without auth - should fail 742curl -X POST 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.repo.createRecord' \ 743 -H 'Content-Type: application/json' \ 744 -d '{"repo":"did:web:chad-pds.chad-53c.workers.dev","collection":"app.bsky.feed.post","record":{"text":"test","createdAt":"2024-01-01T00:00:00Z"}}' 745# Expected: 401 AuthenticationRequired 746 747# With auth - should work 748TOKEN=$(curl -s -X POST 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.server.createSession' \ 749 -H 'Content-Type: application/json' \ 750 -d '{"identifier":"chad-pds.chad-53c.workers.dev","password":"YOUR_PASSWORD"}' | jq -r '.accessJwt') 751 752curl -X POST 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.repo.createRecord' \ 753 -H 'Content-Type: application/json' \ 754 -H "Authorization: Bearer $TOKEN" \ 755 -d '{"repo":"did:web:chad-pds.chad-53c.workers.dev","collection":"app.bsky.feed.post","record":{"text":"test","createdAt":"2024-01-01T00:00:00Z"}}' 756# Expected: 200 with uri, cid, commit 757``` 758 759**Step 5: Commit** 760 761```bash 762git add src/pds.js 763git commit -m "feat: protect createRecord and deleteRecord with JWT auth" 764``` 765 766--- 767 768## Task 7: Configure Environment Variables 769 770**Files:** 771- Modify: `wrangler.toml` (optional - can use wrangler secret instead) 772 773**Step 1: Set secrets using wrangler** 774 775```bash 776# Set the password for login 777npx wrangler secret put PDS_PASSWORD 778# Enter your password when prompted 779 780# Set the JWT signing secret (generate a random string) 781npx wrangler secret put JWT_SECRET 782# Enter a long random string (e.g., openssl rand -base64 32) 783``` 784 785**Step 2: Deploy and verify** 786 787```bash 788npx wrangler deploy 789``` 790 791**Step 3: Test full flow** 792 793```bash 794# Login 795curl -X POST 'https://chad-pds.chad-53c.workers.dev/xrpc/com.atproto.server.createSession' \ 796 -H 'Content-Type: application/json' \ 797 -d '{"identifier":"chad-pds.chad-53c.workers.dev","password":"YOUR_PASSWORD"}' 798``` 799 800--- 801 802## Task 8: Test with Bluesky App 803 804**Step 1: Open bsky.app** 805 806Go to https://bsky.app and click "Sign in" 807 808**Step 2: Enter custom PDS** 809 810Click "Hosting provider" and enter your PDS URL: `chad-pds.chad-53c.workers.dev` 811 812**Step 3: Login** 813 814Enter your handle (e.g., `chad-pds.chad-53c.workers.dev`) and password. 815 816**Step 4: Verify login works** 817 818You should see your profile. Try creating a post to verify write access works. 819 820**Step 5: Final commit** 821 822```bash 823git add -A 824git commit -m "feat: complete authentication implementation for Bluesky app login" 825``` 826 827--- 828 829## Summary of Changes 830 8311. **New exports in `src/pds.js`:** 832 - `base64UrlEncode(bytes)` - Encode bytes to base64url 833 - `base64UrlDecode(str)` - Decode base64url to bytes 834 - `createAccessJwt(did, secret)` - Create access token 835 - `createRefreshJwt(did, secret)` - Create refresh token 836 - `verifyAccessJwt(jwt, secret)` - Verify access token 837 8382. **New endpoints:** 839 - `POST /xrpc/com.atproto.server.createSession` - Login 840 - `GET /xrpc/com.atproto.server.getSession` - Verify session 841 8423. **Modified endpoints:** 843 - `POST /xrpc/com.atproto.repo.createRecord` - Now requires auth 844 - `POST /xrpc/com.atproto.repo.deleteRecord` - Now requires auth 845 8464. **Environment variables:** 847 - `PDS_PASSWORD` - Password for login 848 - `JWT_SECRET` - Secret for signing JWTs