@atproto/oauth-client
Version:
OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).
149 lines • 6.32 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.dpopFetchWrapper = dpopFetchWrapper;
const fetch_1 = require("@atproto-labs/fetch");
const base64_1 = require("multiformats/bases/base64");
// "undefined" in non https environments or environments without crypto
const subtle = globalThis.crypto?.subtle;
const ReadableStream = globalThis.ReadableStream;
function dpopFetchWrapper({ key, iss, supportedAlgs, nonces, sha256 = typeof subtle !== 'undefined' ? subtleSha256 : undefined, isAuthServer, fetch = globalThis.fetch, }) {
if (!sha256) {
throw new TypeError(`crypto.subtle is not available in this environment. Please provide a sha256 function.`);
}
const alg = negotiateAlg(key, supportedAlgs);
return async function (input, init) {
if (!key.algorithms.includes(alg)) {
throw new TypeError(`Key does not support the algorithm ${alg}`);
}
const request = init == null && input instanceof Request
? input
: new Request(input, init);
const authorizationHeader = request.headers.get('Authorization');
const ath = authorizationHeader?.startsWith('DPoP ')
? await sha256(authorizationHeader.slice(5))
: undefined;
const { method, url } = request;
const { origin } = new URL(url);
let initNonce;
try {
initNonce = await nonces.get(origin);
}
catch {
// Ignore get errors, we will just not send a nonce
}
const initProof = await buildProof(key, alg, iss, method, url, initNonce, ath);
request.headers.set('DPoP', initProof);
const initResponse = await fetch.call(this, request);
// Make sure the response body is consumed. Either by the caller (when the
// response is returned), of if an error is thrown (catch block).
const nextNonce = initResponse.headers.get('DPoP-Nonce');
if (!nextNonce || nextNonce === initNonce) {
// No nonce was returned or it is the same as the one we sent. No need to
// update the nonce store, or retry the request.
return initResponse;
}
// Store the fresh nonce for future requests
try {
await nonces.set(origin, nextNonce);
}
catch {
// Ignore set errors
}
const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
if (!shouldRetry) {
// Not a "use_dpop_nonce" error, so there is no need to retry
return initResponse;
}
// If the input stream was already consumed, we cannot retry the request. A
// solution would be to clone() the request but that would bufferize the
// entire stream in memory which can lead to memory starvation. Instead, we
// will return the original response and let the calling code handle retries.
if (input === request) {
// The input request body was consumed. We cannot retry the request.
return initResponse;
}
if (ReadableStream && init?.body instanceof ReadableStream) {
// The init body was consumed. We cannot retry the request.
return initResponse;
}
// We will now retry the request with the fresh nonce.
// The initial response body must be consumed (see cancelBody's doc).
await (0, fetch_1.cancelBody)(initResponse, 'log');
const nextProof = await buildProof(key, alg, iss, method, url, nextNonce, ath);
const nextRequest = new Request(input, init);
nextRequest.headers.set('DPoP', nextProof);
return fetch.call(this, nextRequest);
};
}
async function buildProof(key, alg, iss, htm, htu, nonce, ath) {
if (!key.bareJwk) {
throw new Error('Only asymmetric keys can be used as DPoP proofs');
}
const now = Math.floor(Date.now() / 1e3);
return key.createJwt(
// https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
{
alg,
typ: 'dpop+jwt',
jwk: key.bareJwk,
}, {
iss,
iat: now,
// Any collision will cause the request to be rejected by the server. no biggie.
jti: Math.random().toString(36).slice(2),
htm,
htu,
nonce,
ath,
});
}
async function isUseDpopNonceError(response, isAuthServer) {
// https://datatracker.ietf.org/doc/html/rfc6750#section-3
// https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
if (isAuthServer === undefined || isAuthServer === false) {
if (response.status === 401) {
const wwwAuth = response.headers.get('WWW-Authenticate');
if (wwwAuth?.startsWith('DPoP')) {
return wwwAuth.includes('error="use_dpop_nonce"');
}
}
}
// https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
if (isAuthServer === undefined || isAuthServer === true) {
if (response.status === 400) {
try {
const json = await (0, fetch_1.peekJson)(response, 10 * 1024);
return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';
}
catch {
// Response too big (to be "use_dpop_nonce" error) or invalid JSON
return false;
}
}
}
return false;
}
function negotiateAlg(key, supportedAlgs) {
if (supportedAlgs) {
// Use order of supportedAlgs as preference
const alg = supportedAlgs.find((a) => key.algorithms.includes(a));
if (alg)
return alg;
}
else {
const [alg] = key.algorithms;
if (alg)
return alg;
}
throw new Error('Key does not match any alg supported by the server');
}
async function subtleSha256(input) {
if (subtle == null) {
throw new Error(`crypto.subtle is not available in this environment. Please provide a sha256 function.`);
}
const bytes = new TextEncoder().encode(input);
const digest = await subtle.digest('SHA-256', bytes);
const digestBytes = new Uint8Array(digest);
return base64_1.base64url.baseEncode(digestBytes);
}
//# sourceMappingURL=fetch-dpop.js.map