chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// The DPoP handshake is a two-step: the server answers the first proof (which
// has no nonce) with `use_dpop_nonce` and a `DPoP-Nonce` header, and the client
// retries with that nonce. A browser client can only do this if it can READ the
// `DPoP-Nonce` header — which requires it to be listed in
// Access-Control-Expose-Headers. Without that, browser-based apps fail to log in
// on `use_dpop_nonce` while native apps (no CORS) work: this pins the header.
import { describe, expect, it } from 'vitest';
import { PersonalDataServer } from '../packages/core/src/pds.js';
import { createDpopProof, generateKeyPair } from './helpers/dpop.js';
const HOSTNAME = 'pds.example.com';
function createServer() {
return new PersonalDataServer({
actorStorage: /** @type {any} */ ({}),
sharedStorage: /** @type {any} */ ({
checkAndStoreDpopJti: async () => true,
}),
blobs: /** @type {any} */ ({}),
jwtSecret: 'test-secret',
hostname: HOSTNAME,
});
}
describe('DPoP nonce is readable by browser clients', () => {
it('exposes DPoP-Nonce on the use_dpop_nonce response', async () => {
const { privateKey, jwk } = await generateKeyPair();
const url = `https://${HOSTNAME}/oauth/par`;
// A valid DPoP proof with no nonce — the first step of the handshake.
const proof = await createDpopProof({
privateKey,
jwk,
method: 'POST',
url,
omitNonce: true,
});
const res = await createServer().fetch(
new Request(url, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
DPoP: proof,
},
body: 'client_id=https://client.example/metadata.json',
}),
);
expect(res.status).toBe(400);
expect((await res.json()).error).toBe('use_dpop_nonce');
// The nonce is present…
expect(res.headers.get('DPoP-Nonce')).toBeTruthy();
// …and a browser is allowed to read it, so it can retry.
const expose = res.headers.get('Access-Control-Expose-Headers') || '';
expect(
expose
.split(',')
.map((h) => h.trim().toLowerCase())
.includes('dpop-nonce'),
).toBe(true);
});
});