Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145import { randomBytes } from "node:crypto";import { httpLogger } from "@atproto/pds";
export const PREAUTHORIZE_PATH = "/_preauthorize";export const MAX_ACCOUNT_AGE_MS = 5 * 60 * 1000;const MAX_BODY_BYTES = 8 * 1024;
class Refusal extends Error { constructor(status, error, message) { super(message); this.status = status; this.error = error; }}
const refuse = (message) => new Refusal(403, "PreauthorizeRefused", message);
export const clientsFromEnv = (raw = process.env.TANGLED_PREAUTHORIZE_CLIENTS) => (raw ?? "") .split(",") .map((client) => client.trim()) .filter(Boolean);
// everything the route calls. a pds that dropped one leaves the route off and logs itconst requiredApi = (pds) => { const provider = pds.ctx?.oauthProvider; return { "app.post": pds.app?.post, "ctx.authVerifier.adminToken": pds.ctx?.authVerifier?.adminToken, "ctx.accountManager.getAccount": pds.ctx?.accountManager?.getAccount, "oauthProvider.clientManager.getClient": provider?.clientManager?.getClient, "oauthProvider.accountManager.getAccount": provider?.accountManager?.getAccount, "oauthProvider.accountManager.setAuthorizedClient": provider?.accountManager?.setAuthorizedClient, "oauthProvider.requestManager.peekClientId": provider?.requestManager?.peekClientId, "oauthProvider.requestManager.get": provider?.requestManager?.get, "oauthProvider.requestManager.setAuthorized": provider?.requestManager?.setAuthorized, "oauthProvider.requestManager.delete": provider?.requestManager?.delete, "oauthProvider.tokenManager.listAccountTokens": provider?.tokenManager?.listAccountTokens };};
export const missingApi = (pds) => { const missing = Object.entries(requiredApi(pds)) .filter(([, value]) => typeof value !== "function") .map(([name]) => name); if (typeof pds.ctx?.oauthProvider?.issuer !== "string") missing.push("oauthProvider.issuer"); return missing;};
const readJson = async (req) => { let size = 0; const chunks = []; for await (const chunk of req) { size += chunk.length; if (size > MAX_BODY_BYTES) throw new Refusal(413, "PayloadTooLarge", "body too large"); chunks.push(chunk); } try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { throw new Refusal(400, "InvalidRequest", "body is not json"); }};
export const mountPreauthorize = (pds, clients = clientsFromEnv(), now = Date.now) => { const missing = missingApi(pds); if (missing.length > 0) { httpLogger.error({ missing }, "tangled preauthorize is off, the pds lacks what it needs"); return; } const { authVerifier, accountManager: actors, oauthProvider: provider } = pds.ctx;
const authorize = async (req) => { const { did, requestUri } = (await readJson(req)) ?? {}; if (typeof did !== "string" || typeof requestUri !== "string") { throw new Refusal(400, "InvalidRequest", "did and requestUri are required"); }
const clientId = await provider.requestManager.peekClientId(requestUri); if (!clientId) throw refuse("unknown request"); if (!clients.includes(clientId)) throw refuse("client is not allowed");
const actor = await actors.getAccount(did); if (!actor) throw refuse("unknown account"); if (now() - Date.parse(actor.createdAt) > MAX_ACCOUNT_AGE_MS) throw refuse("account is not new");
const { account, authorizedClients } = await provider.accountManager.getAccount(did); if (authorizedClients.size > 0) throw refuse("account already authorized a client"); if ((await provider.tokenManager.listAccountTokens(did)).length > 0) { throw refuse("account already has oauth tokens"); }
const client = await provider.clientManager.getClient(clientId); const deviceId = `dev-${randomBytes(16).toString("hex")}`; const { parameters } = await provider.requestManager.get(requestUri, deviceId, clientId); const hint = parameters.login_hint; if (hint && hint !== account.did && hint !== account.handle) { await provider.requestManager.delete(requestUri); throw refuse("request is for another account"); }
const metadata = { userAgent: req.headers["user-agent"] ?? null, ipAddress: req.ip ?? req.socket.remoteAddress ?? "", lastSeenAt: new Date(now()) }; const code = await provider.requestManager.setAuthorized( requestUri, client, account, deviceId, metadata ); await provider.accountManager.setAuthorizedClient(account, client, { authorizedScopes: parameters.scope?.split(" ") ?? [] }); return { code, state: parameters.state, iss: provider.issuer }; };
pds.app.post(PREAUTHORIZE_PATH, async (req, res) => { try { await authVerifier.adminToken({ req, res }); } catch { return res .status(401) .json({ error: "AuthenticationRequired", message: "admin auth required" }); } try { res.json(await authorize(req)); } catch (err) { if (err instanceof Refusal) return res.status(err.status).json({ error: err.error, message: err.message }); if (err?.status < 500 && typeof err.error === "string") { return res .status(403) .json({ error: "PreauthorizeRefused", message: err.error_description ?? err.error }); } httpLogger.error({ err }, "tangled preauthorize failed"); res.status(500).json({ error: "InternalServerError", message: "Internal Server Error" }); } });};