A Deno-compatible AT Protocol OAuth client that serves as a drop-in replacement for @atproto/oauth-client-node
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 110 lines 3.6 kB view raw
1import { assertEquals, assertNotEquals } from "@std/assert"; 2import { generateDPoPKeyPair, generateDPoPProof } from "../src/dpop.ts"; 3import { decodeJwt } from "@panva/jose"; 4 5Deno.test("DPoP proof - htu normalization", async (t) => { 6 const keyPair = await generateDPoPKeyPair(); 7 8 await t.step("strips query parameters from htu", async () => { 9 const proof = await generateDPoPProof( 10 "GET", 11 "https://example.com/api?foo=bar&baz=qux", 12 keyPair.privateKey, 13 keyPair.publicKeyJWK, 14 ); 15 const payload = decodeJwt(proof); 16 assertEquals(payload.htu, "https://example.com/api"); 17 }); 18 19 await t.step("strips fragment from htu", async () => { 20 const proof = await generateDPoPProof( 21 "POST", 22 "https://example.com/api#section", 23 keyPair.privateKey, 24 keyPair.publicKeyJWK, 25 ); 26 const payload = decodeJwt(proof); 27 assertEquals(payload.htu, "https://example.com/api"); 28 }); 29 30 await t.step("preserves path in htu", async () => { 31 const proof = await generateDPoPProof( 32 "GET", 33 "https://example.com/oauth/token", 34 keyPair.privateKey, 35 keyPair.publicKeyJWK, 36 ); 37 const payload = decodeJwt(proof); 38 assertEquals(payload.htu, "https://example.com/oauth/token"); 39 }); 40 41 await t.step("includes nonce when provided", async () => { 42 const proof = await generateDPoPProof( 43 "POST", 44 "https://example.com/oauth/token", 45 keyPair.privateKey, 46 keyPair.publicKeyJWK, 47 undefined, 48 "server-nonce-123", 49 ); 50 const payload = decodeJwt(proof); 51 assertEquals(payload.nonce, "server-nonce-123"); 52 }); 53 54 await t.step("generates unique jti for each proof", async () => { 55 const proof1 = await generateDPoPProof( 56 "GET", 57 "https://example.com/api", 58 keyPair.privateKey, 59 keyPair.publicKeyJWK, 60 ); 61 const proof2 = await generateDPoPProof( 62 "GET", 63 "https://example.com/api", 64 keyPair.privateKey, 65 keyPair.publicKeyJWK, 66 ); 67 const payload1 = decodeJwt(proof1); 68 const payload2 = decodeJwt(proof2); 69 assertNotEquals(payload1.jti, payload2.jti); 70 }); 71}); 72 73Deno.test("DPoP nonce cache", async (t) => { 74 // Import cache functions 75 const { getCachedNonce, updateNonceCache } = await import("../src/dpop.ts"); 76 77 await t.step("returns undefined for unknown origins", () => { 78 const nonce = getCachedNonce("https://unknown-origin.example.com/path"); 79 assertEquals(nonce, undefined); 80 }); 81 82 await t.step("stores and retrieves nonce per origin", () => { 83 const mockResponse = new Response(null, { 84 headers: { "DPoP-Nonce": "nonce-abc" }, 85 }); 86 updateNonceCache("https://cache-test.example.com/oauth/token", mockResponse); 87 88 assertEquals(getCachedNonce("https://cache-test.example.com/other"), "nonce-abc"); 89 }); 90 91 await t.step("updates nonce from new response", () => { 92 const response1 = new Response(null, { 93 headers: { "DPoP-Nonce": "nonce-1" }, 94 }); 95 updateNonceCache("https://update-test.example.com/a", response1); 96 assertEquals(getCachedNonce("https://update-test.example.com/b"), "nonce-1"); 97 98 const response2 = new Response(null, { 99 headers: { "DPoP-Nonce": "nonce-2" }, 100 }); 101 updateNonceCache("https://update-test.example.com/c", response2); 102 assertEquals(getCachedNonce("https://update-test.example.com/d"), "nonce-2"); 103 }); 104 105 await t.step("ignores responses without DPoP-Nonce header", () => { 106 const response = new Response(null); 107 updateNonceCache("https://no-nonce.example.com/path", response); 108 assertEquals(getCachedNonce("https://no-nonce.example.com/path"), undefined); 109 }); 110});