Account for node/browser discrepancy

This commit is contained in:
Manav Rathi 2024-05-24 10:51:19 +05:30
parent fec040e528
commit dc38a8bc9f
No known key found for this signature in database

View file

@ -64,11 +64,27 @@ export const codeFromURIString = (id: string, uriString: string): Code => {
const _codeFromURIString = (id: string, uriString: string): Code => { const _codeFromURIString = (id: string, uriString: string): Code => {
const url = new URL(uriString); const url = new URL(uriString);
// A URL like
//
// new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0")
//
// is parsed differently by the browser and Node depending on the scheme.
// When the scheme is http(s), then both of them consider "hotp" as the
// `host`. However, when the scheme is "otpauth", as is our case, the
// browser considers the entire thing as part of the pathname. so we get.
//
// host: ""
// pathname: "//hotp/Test"
//
// Since this code run on browsers only, we parse as per that behaviour.
const [type, path] = parsePathname(url);
return { return {
id, id,
type: parseType(url), type,
account: parseAccount(url), account: parseAccount(path),
issuer: parseIssuer(url), issuer: parseIssuer(url, path),
digits: parseDigits(url), digits: parseDigits(url),
period: parsePeriod(url), period: parsePeriod(url),
secret: parseSecret(url), secret: parseSecret(url),
@ -77,21 +93,22 @@ const _codeFromURIString = (id: string, uriString: string): Code => {
}; };
}; };
const parseType = (url: URL): Code["type"] => { const parsePathname = (url: URL): [type: Code["type"], path: string] => {
const t = url.host.toLowerCase(); const p = url.pathname.toLowerCase();
if (t == "totp" || t == "hotp") return t; if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)];
throw new Error(`Unsupported code with host "${t}"`); if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)];
throw new Error(`Unsupported code or unparseable path "${url.pathname}"`);
}; };
const parseAccount = (url: URL): string | undefined => { const parseAccount = (path: string): string | undefined => {
// "/ACME:user@example.org" => "user@example.org" // "/ACME:user@example.org" => "user@example.org"
let p = decodeURIComponent(url.pathname); let p = decodeURIComponent(path);
if (p.startsWith("/")) p = p.slice(1); if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":").slice(1).join(":"); if (p.includes(":")) p = p.split(":").slice(1).join(":");
return p; return p;
}; };
const parseIssuer = (url: URL): string => { const parseIssuer = (url: URL, path: string): string => {
// If there is a "issuer" search param, use that. // If there is a "issuer" search param, use that.
let issuer = url.searchParams.get("issuer"); let issuer = url.searchParams.get("issuer");
if (issuer) { if (issuer) {
@ -104,7 +121,7 @@ const parseIssuer = (url: URL): string => {
// Otherwise use the `prefix:` from the account as the issuer. // Otherwise use the `prefix:` from the account as the issuer.
// "/ACME:user@example.org" => "ACME" // "/ACME:user@example.org" => "ACME"
let p = decodeURIComponent(url.pathname); let p = decodeURIComponent(path);
if (p.startsWith("/")) p = p.slice(1); if (p.startsWith("/")) p = p.slice(1);
if (p.includes(":")) p = p.split(":")[0]; if (p.includes(":")) p = p.split(":")[0];