A zero-dependency AT Protocol Personal Data Server written in JavaScript
atproto pds

test: add failing tests for direct OAuth authorization flow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+106
test
+106
test/e2e.test.js
··· 1450 1450 'Should show full access warning', 1451 1451 ); 1452 1452 }); 1453 + 1454 + it('supports direct authorization without PAR', async () => { 1455 + const clientId = 'http://localhost:3000'; 1456 + const redirectUri = 'http://localhost:3000/callback'; 1457 + const codeVerifier = 'test-verifier-for-direct-auth-flow-min-43-chars!!'; 1458 + const challengeBuffer = await crypto.subtle.digest( 1459 + 'SHA-256', 1460 + new TextEncoder().encode(codeVerifier), 1461 + ); 1462 + const codeChallenge = Buffer.from(challengeBuffer).toString('base64url'); 1463 + const state = 'test-direct-auth-state'; 1464 + 1465 + // Step 1: GET authorize with direct parameters (no PAR) 1466 + const authorizeUrl = new URL(`${BASE}/oauth/authorize`); 1467 + authorizeUrl.searchParams.set('client_id', clientId); 1468 + authorizeUrl.searchParams.set('redirect_uri', redirectUri); 1469 + authorizeUrl.searchParams.set('response_type', 'code'); 1470 + authorizeUrl.searchParams.set('scope', 'atproto'); 1471 + authorizeUrl.searchParams.set('code_challenge', codeChallenge); 1472 + authorizeUrl.searchParams.set('code_challenge_method', 'S256'); 1473 + authorizeUrl.searchParams.set('state', state); 1474 + authorizeUrl.searchParams.set('login_hint', DID); 1475 + 1476 + const getRes = await fetch(authorizeUrl.toString()); 1477 + assert.strictEqual(getRes.status, 200, 'Direct authorize GET should succeed'); 1478 + 1479 + const html = await getRes.text(); 1480 + assert.ok(html.includes('Authorize'), 'Should show consent page'); 1481 + assert.ok(html.includes('request_uri'), 'Should include request_uri in form'); 1482 + }); 1483 + 1484 + it('completes full direct authorization flow', async () => { 1485 + const clientId = 'http://localhost:3000'; 1486 + const redirectUri = 'http://localhost:3000/callback'; 1487 + const codeVerifier = 'test-verifier-for-direct-auth-flow-min-43-chars!!'; 1488 + const challengeBuffer = await crypto.subtle.digest( 1489 + 'SHA-256', 1490 + new TextEncoder().encode(codeVerifier), 1491 + ); 1492 + const codeChallenge = Buffer.from(challengeBuffer).toString('base64url'); 1493 + const state = 'test-direct-auth-state'; 1494 + 1495 + // Step 1: GET authorize with direct parameters 1496 + const authorizeUrl = new URL(`${BASE}/oauth/authorize`); 1497 + authorizeUrl.searchParams.set('client_id', clientId); 1498 + authorizeUrl.searchParams.set('redirect_uri', redirectUri); 1499 + authorizeUrl.searchParams.set('response_type', 'code'); 1500 + authorizeUrl.searchParams.set('scope', 'atproto'); 1501 + authorizeUrl.searchParams.set('code_challenge', codeChallenge); 1502 + authorizeUrl.searchParams.set('code_challenge_method', 'S256'); 1503 + authorizeUrl.searchParams.set('state', state); 1504 + authorizeUrl.searchParams.set('login_hint', DID); 1505 + 1506 + const getRes = await fetch(authorizeUrl.toString()); 1507 + assert.strictEqual(getRes.status, 200); 1508 + const html = await getRes.text(); 1509 + 1510 + // Extract request_uri from the form 1511 + const requestUriMatch = html.match(/name="request_uri" value="([^"]+)"/); 1512 + assert.ok(requestUriMatch, 'Should have request_uri in form'); 1513 + const requestUri = requestUriMatch[1]; 1514 + 1515 + // Step 2: POST to authorize (user approval) 1516 + const authRes = await fetch(`${BASE}/oauth/authorize`, { 1517 + method: 'POST', 1518 + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 1519 + body: new URLSearchParams({ 1520 + request_uri: requestUri, 1521 + client_id: clientId, 1522 + password: PASSWORD, 1523 + }).toString(), 1524 + redirect: 'manual', 1525 + }); 1526 + 1527 + assert.strictEqual(authRes.status, 302, 'Should redirect after approval'); 1528 + const location = authRes.headers.get('location'); 1529 + assert.ok(location, 'Should have Location header'); 1530 + const locationUrl = new URL(location); 1531 + const code = locationUrl.searchParams.get('code'); 1532 + assert.ok(code, 'Should have authorization code'); 1533 + assert.strictEqual(locationUrl.searchParams.get('state'), state); 1534 + 1535 + // Step 3: Exchange code for tokens 1536 + const dpop = await DpopClient.create(); 1537 + const dpopProof = await dpop.createProof('POST', `${BASE}/oauth/token`); 1538 + 1539 + const tokenRes = await fetch(`${BASE}/oauth/token`, { 1540 + method: 'POST', 1541 + headers: { 1542 + 'Content-Type': 'application/x-www-form-urlencoded', 1543 + DPoP: dpopProof, 1544 + }, 1545 + body: new URLSearchParams({ 1546 + grant_type: 'authorization_code', 1547 + code, 1548 + redirect_uri: redirectUri, 1549 + client_id: clientId, 1550 + code_verifier: codeVerifier, 1551 + }).toString(), 1552 + }); 1553 + 1554 + assert.strictEqual(tokenRes.status, 200, 'Token exchange should succeed'); 1555 + const tokenData = await tokenRes.json(); 1556 + assert.ok(tokenData.access_token, 'Should have access_token'); 1557 + assert.strictEqual(tokenData.token_type, 'DPoP'); 1558 + }); 1453 1559 }); 1454 1560 1455 1561 describe('Foreign DID proxying', () => {