verifyServiceAuth

tcmts.atproto.verifyServiceAuth · #6od6r75b · function · runs on the server

uses Buffer

Check a service-auth token: signed by its issuer's repository key (#atproto, from their DID document), for one of audiences (this server, as serviceDid names it), for the method lxm, and not expired. The DID it's from; else it throws Refused, saying why.

import { isDid, Refused, SIGNING_KEYS, signingKey } from "#codebase/tcmts/atproto";

export async function verifyServiceAuth(
  jwt: string,
  o: { audiences: string[]; lxm: string },
): Promise<string> {
  const [h, p, sig, extra] = jwt.split(".");
  if (!h || !p || !sig || extra !== undefined) throw new Refused("that isn't a service-auth token");
  let header: { alg?: unknown };
  let claims: { iss?: unknown; aud?: unknown; lxm?: unknown; exp?: unknown };
  try {
    header = JSON.parse(Buffer.from(h, "base64url").toString("utf8"));
    claims = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
  } catch {
    throw new Refused("that isn't a service-auth token");
  }
  if (header.alg !== "ES256" && header.alg !== "ES256K")
    throw new Refused(`a service-auth token signed with ${String(header.alg)} isn't taken`);
  const iss = claims.iss;
  if (typeof iss !== "string" || !isDid(iss))
    throw new Refused("the token doesn't say whose it is");
  if (typeof claims.aud !== "string" || !o.audiences.includes(claims.aud))
    throw new Refused(
      `the token is for ${String(claims.aud)}, not this server (${o.audiences.join(", ") || "it has no URL: start it with --url"})`,
    );
  if (claims.lxm !== o.lxm)
    throw new Refused(`the token is for ${String(claims.lxm)}, not ${o.lxm}`);
  if (typeof claims.exp !== "number" || claims.exp * 1000 < Date.now())
    throw new Refused("the token has expired");
  const data = new Uint8Array(Buffer.from(`${h}.${p}`, "utf8"));
  const signature = new Uint8Array(Buffer.from(sig, "base64url"));
  const keyOf = async (fresh: boolean) => {
    const kept = SIGNING_KEYS.get(iss);
    if (kept && !fresh && Date.now() - kept.at < 5 * 60_000) return kept.key;
    const { key } = await signingKey(iss);
    SIGNING_KEYS.set(iss, { key, at: Date.now() });
    return key;
  };
  const checks = async (fresh: boolean) => (await keyOf(fresh)).verify(signature, data);
  if (!(await checks(false)) && !(await checks(true)))
    throw new Refused(`the token isn't signed by ${iss}'s key`);
  return iss;
}

uses

used by