UNPKG

@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
"use strict"; 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