WWW-Authenticate Header Parser

Parse WWW-Authenticate challenges and quoted auth parameters, including multiple challenges in one header.

TypescriptData Fetching

Code

snippet
export interface AuthChallenge {
  scheme: string;
  params: Record<string, string>;
}

function skipWhitespace(header: string, index: number): number {
  while (index < header.length && /\s/.test(header[index]!)) index += 1;
  return index;
}

function readToken(header: string, index: number): { value: string; end: number } {
  const match = /^[^\s,="]+/.exec(header.slice(index));
  if (!match) throw new SyntaxError("expected token at position " + index);
  return { value: match[0], end: index + match[0].length };
}

function readQuoted(header: string, index: number): { value: string; end: number } {
  let value = "";
  index += 1;
  while (index < header.length) {
    const character = header[index]!;
    if (character === "\\" && index + 1 < header.length) {
      value += header[index + 1];
      index += 2;
      continue;
    }
    if (character === '"') return { value, end: index + 1 };
    value += character;
    index += 1;
  }
  throw new SyntaxError("unterminated quoted string");
}

function readValue(header: string, index: number): { value: string; end: number } {
  return header[index] === '"' ? readQuoted(header, index) : readToken(header, index);
}

export function parseWwwAuthenticate(header: string): AuthChallenge[] {
  if (typeof header !== "string" || header.trim() === "") {
    throw new TypeError("header must be a non-empty string");
  }
  const challenges: AuthChallenge[] = [];
  let index = 0;
  while (index < header.length) {
    index = skipWhitespace(header, index);
    if (header[index] === ",") {
      index = skipWhitespace(header, index + 1);
      continue;
    }
    if (index >= header.length) break;
    const schemeToken = readToken(header, index);
    const scheme = schemeToken.value;
    index = skipWhitespace(header, schemeToken.end);
    const params: Record<string, string> = {};
    if (index < header.length && header[index] !== ",") {
      const name = readToken(header, index);
      index = skipWhitespace(header, name.end);
      if (header[index] !== "=") {
        throw new SyntaxError("expected = after parameter " + name.value);
      }
      index = skipWhitespace(header, index + 1);
      const value = readValue(header, index);
      params[name.value.toLowerCase()] = value.value;
      index = value.end;
    }
    while (true) {
      index = skipWhitespace(header, index);
      if (header[index] !== ",") break;
      const afterComma = skipWhitespace(header, index + 1);
      if (afterComma >= header.length) {
        index = afterComma;
        break;
      }
      const lookahead = readToken(header, afterComma);
      const afterName = skipWhitespace(header, lookahead.end);
      if (header[afterName] !== "=") break;
      index = skipWhitespace(header, afterName + 1);
      const value = readValue(header, index);
      params[lookahead.value.toLowerCase()] = value.value;
      index = value.end;
    }
    challenges.push({ scheme, params });
  }
  return challenges;
}