mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
57b6e9aca4
closes #6930
245 lines
8.2 KiB
TypeScript
245 lines
8.2 KiB
TypeScript
/**
|
|
* Utility functions for working with passkeys and WebAuthn.
|
|
*/
|
|
|
|
interface ParsedUserAgent {
|
|
browser?: string;
|
|
os?: string;
|
|
device?: string;
|
|
}
|
|
|
|
export enum PasskeyBrand {
|
|
AliasVault = "a11a5faa-9f32-4b8c-8c5d-2f7d13e8c942",
|
|
GooglePasswordManager = "ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4",
|
|
ChromeOnMac = "adce0002-35bc-c60a-648b-0b25f1f05503",
|
|
WindowsHello1 = "08987058-cadc-4b81-b6e1-30de50dcbe96",
|
|
WindowsHello2 = "9ddd1817-af5a-4672-a2b9-3e3dd95000a9",
|
|
WindowsHello3 = "6028b017-b1d4-4c02-b4b3-afcdafc96bb2",
|
|
ICloudKeychainManaged = "dd4ec289-e01d-41c9-bb89-70fa845d4bf2",
|
|
Dashlane = "531126d6-e717-415c-9320-3d9aa6981239",
|
|
OnePassword = "bada5566-a7aa-401f-bd96-45619a55120d",
|
|
NordPass = "b84e4048-15dc-4dd0-8640-f4f60813c8af",
|
|
Keeper = "0ea242b4-43c4-4a1b-8b17-dd6d0b6baec6",
|
|
Sesame = "891494da-2c90-4d31-a9cd-4eab0aed1309",
|
|
Enpass = "f3809540-7f14-49c1-a8b3-8f813b225541",
|
|
ChromiumBrowser = "b5397666-4885-aa6b-cebf-e52262a439a2",
|
|
EdgeOnMac = "771b48fd-d3d4-4f74-9232-fc157ab0507a",
|
|
IDmelon = "39a5647e-1853-446c-a1f6-a79bae9f5bc7",
|
|
Bitwarden = "d548826e-79b4-db40-a3d8-11116f7e8349",
|
|
ApplePasswords = "fbfc3007-154e-4ecc-8c0b-6e020557d7bd",
|
|
SamsungPass = "53414d53-554e-4700-0000-000000000000",
|
|
ThalesBioIOS = "66a0ccb3-bd6a-191f-ee06-e375c50b9846",
|
|
ThalesBioAndroid = "8836336a-f590-0921-301d-46427531eee6",
|
|
ThalesPINAndroid = "cd69adb5-3c7a-deb9-3177-6800ea6cb72a",
|
|
ThalesPINIOS = "17290f1e-c212-34d0-1423-365d729f09d9",
|
|
ProtonPass = "50726f74-6f6e-5061-7373-50726f746f6e",
|
|
KeePassXC = "fdb141b2-5d84-443e-8a35-4698c205a502",
|
|
KeePassDX = "eaecdef2-1c31-5634-8639-f1cbd9c00a08",
|
|
ToothPicPasskeyProvider = "cc45f64e-52a2-451b-831a-4edd8022a202",
|
|
IPasswords = "bfc748bb-3429-4faa-b9f9-7cfa9f3b76d0",
|
|
ZohoVault = "b35a26b2-8f6e-4697-ab1d-d44db4da28c6",
|
|
LastPass = "b78a0a55-6ef8-d246-a042-ba0f6d55050c",
|
|
Devolutions = "de503f9c-21a4-4f76-b4b7-558eb55c6f89",
|
|
LogMeOnce = "22248c4c-7a12-46e2-9a41-44291b373a4d",
|
|
KasperskyPasswordManager = "a10c6dd9-465e-4226-8198-c7c44b91c555",
|
|
PwSafe = "d350af52-0351-4ba2-acd3-dfeeadc3f764",
|
|
MicrosoftPasswordManager = "d3452668-01fd-4c12-926c-83a4204853aa",
|
|
Initial = "6d212b28-a2c1-4638-b375-5932070f62e9",
|
|
HeimlaneVault = "d49b2120-b865-4191-8cea-be84a52b0485",
|
|
}
|
|
|
|
export const PasskeyBrandNames: Record<PasskeyBrand, string> = {
|
|
[PasskeyBrand.AliasVault]: "AliasVault",
|
|
[PasskeyBrand.GooglePasswordManager]: "Google Password Manager",
|
|
[PasskeyBrand.ChromeOnMac]: "Chrome on Mac",
|
|
[PasskeyBrand.WindowsHello1]: "Windows Hello",
|
|
[PasskeyBrand.WindowsHello2]: "Windows Hello",
|
|
[PasskeyBrand.WindowsHello3]: "Windows Hello",
|
|
[PasskeyBrand.ICloudKeychainManaged]: "iCloud Keychain (Managed)",
|
|
[PasskeyBrand.Dashlane]: "Dashlane",
|
|
[PasskeyBrand.OnePassword]: "1Password",
|
|
[PasskeyBrand.NordPass]: "NordPass",
|
|
[PasskeyBrand.Keeper]: "Keeper",
|
|
[PasskeyBrand.Sesame]: "Sésame",
|
|
[PasskeyBrand.Enpass]: "Enpass",
|
|
[PasskeyBrand.ChromiumBrowser]: "Chromium Browser",
|
|
[PasskeyBrand.EdgeOnMac]: "Edge on Mac",
|
|
[PasskeyBrand.IDmelon]: "IDmelon",
|
|
[PasskeyBrand.Bitwarden]: "Bitwarden",
|
|
[PasskeyBrand.ApplePasswords]: "Apple Passwords",
|
|
[PasskeyBrand.SamsungPass]: "Samsung Pass",
|
|
[PasskeyBrand.ThalesBioIOS]: "Thales Bio iOS SDK",
|
|
[PasskeyBrand.ThalesBioAndroid]: "Thales Bio Android SDK",
|
|
[PasskeyBrand.ThalesPINAndroid]: "Thales PIN Android SDK",
|
|
[PasskeyBrand.ThalesPINIOS]: "Thales PIN iOS SDK",
|
|
[PasskeyBrand.ProtonPass]: "Proton Pass",
|
|
[PasskeyBrand.KeePassXC]: "KeePassXC",
|
|
[PasskeyBrand.KeePassDX]: "KeePassDX",
|
|
[PasskeyBrand.ToothPicPasskeyProvider]: "ToothPic Passkey Provider",
|
|
[PasskeyBrand.IPasswords]: "iPasswords",
|
|
[PasskeyBrand.ZohoVault]: "Zoho Vault",
|
|
[PasskeyBrand.LastPass]: "LastPass",
|
|
[PasskeyBrand.Devolutions]: "Devolutions",
|
|
[PasskeyBrand.LogMeOnce]: "LogMeOnce",
|
|
[PasskeyBrand.KasperskyPasswordManager]: "Kaspersky Password Manager",
|
|
[PasskeyBrand.PwSafe]: "pwSafe",
|
|
[PasskeyBrand.MicrosoftPasswordManager]: "Microsoft Password Manager",
|
|
[PasskeyBrand.Initial]: "initial",
|
|
[PasskeyBrand.HeimlaneVault]: "Heimlane Vault",
|
|
};
|
|
|
|
/**
|
|
* Parses a user agent string to extract browser, OS, and device information.
|
|
*
|
|
* @param userAgent the user agent string to parse.
|
|
* @returns parsed information about the browser, OS, and device.
|
|
*/
|
|
function parseUserAgent(userAgent: string): ParsedUserAgent {
|
|
if (!userAgent) {
|
|
return {};
|
|
}
|
|
|
|
const ua = userAgent.toLowerCase();
|
|
const result: ParsedUserAgent = {};
|
|
|
|
// Detect browser
|
|
if (ua.includes("edg/") || ua.includes("edga/") || ua.includes("edgios/")) {
|
|
result.browser = "Edge";
|
|
} else if (ua.includes("opr/") || ua.includes("opera")) {
|
|
result.browser = "Opera";
|
|
} else if (ua.includes("chrome") && !ua.includes("edg")) {
|
|
result.browser = "Chrome";
|
|
} else if (ua.includes("safari") && !ua.includes("chrome")) {
|
|
result.browser = "Safari";
|
|
} else if (ua.includes("firefox")) {
|
|
result.browser = "Firefox";
|
|
}
|
|
|
|
// Detect OS (check iPhone/iPad before macOS since they contain "Mac OS X")
|
|
if (ua.includes("windows")) {
|
|
result.os = "Windows";
|
|
} else if (ua.includes("iphone")) {
|
|
result.os = "iOS";
|
|
result.device = "iPhone";
|
|
} else if (ua.includes("ipad")) {
|
|
result.os = "iOS";
|
|
result.device = "iPad";
|
|
} else if (ua.includes("mac os x") || ua.includes("macos")) {
|
|
result.os = "macOS";
|
|
} else if (ua.includes("android")) {
|
|
result.os = "Android";
|
|
if (ua.includes("mobile")) {
|
|
result.device = "Android Phone";
|
|
} else {
|
|
result.device = "Android Tablet";
|
|
}
|
|
} else if (ua.includes("linux")) {
|
|
result.os = "Linux";
|
|
} else if (ua.includes("cros")) {
|
|
result.os = "Chrome OS";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Determines the type of authenticator based on transports.
|
|
*
|
|
* @param transports array of authenticator transports.
|
|
* @returns a human-readable authenticator type or undefined.
|
|
*/
|
|
function getAuthenticatorType(transports?: string[]): string | undefined {
|
|
if (!transports || transports.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
// Platform authenticators typically use "internal"
|
|
if (transports.includes("internal")) {
|
|
return undefined;
|
|
}
|
|
|
|
// Security keys typically use USB, NFC, or BLE
|
|
if (
|
|
transports.includes("usb") ||
|
|
transports.includes("nfc") ||
|
|
transports.includes("ble")
|
|
) {
|
|
return "Security Key";
|
|
}
|
|
|
|
// Hybrid authenticators use hybrid transport (phone as authenticator)
|
|
if (transports.includes("hybrid")) {
|
|
return "Phone";
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Generates a default passkey name when user agent parsing fails.
|
|
*
|
|
* @param transports optional array of authenticator transports.
|
|
* @returns a default passkey name.
|
|
*/
|
|
function generateDefaultName(transports?: string[]): string {
|
|
const authenticatorType = getAuthenticatorType(transports);
|
|
|
|
if (authenticatorType) {
|
|
return authenticatorType;
|
|
}
|
|
|
|
return "Passkey";
|
|
}
|
|
|
|
/**
|
|
* Generates a friendly name for a passkey based on AAGUID, user agent, and transport information.
|
|
*
|
|
* @param aaguid the authenticator AAGUID from the registration response.
|
|
* @param userAgent the user agent string from the registration request.
|
|
* @param transports optional array of authenticator transports.
|
|
* @returns a human-readable name for the passkey.
|
|
*/
|
|
export function generatePasskeyName(
|
|
aaguid?: string | PasskeyBrand,
|
|
userAgent?: string,
|
|
transports?: string[]
|
|
): string {
|
|
// Prefer known brand from AAGUID over user agent sniffing
|
|
if (aaguid && PasskeyBrandNames[aaguid as PasskeyBrand]) {
|
|
return PasskeyBrandNames[aaguid as PasskeyBrand];
|
|
}
|
|
|
|
// Fall back to user agent parsing if AAGUID is not recognized
|
|
if (!userAgent) {
|
|
return generateDefaultName(transports);
|
|
}
|
|
|
|
const parsed = parseUserAgent(userAgent);
|
|
const parts: string[] = [];
|
|
|
|
// Prioritize device name if available (e.g., "iPhone", "iPad")
|
|
if (parsed.device) {
|
|
parts.push(parsed.device);
|
|
} else {
|
|
// Otherwise use browser and OS
|
|
if (parsed.browser) {
|
|
parts.push(parsed.browser);
|
|
}
|
|
if (parsed.os) {
|
|
parts.push(`on ${parsed.os}`);
|
|
}
|
|
}
|
|
|
|
// Add authenticator type hint if available
|
|
const authenticatorType = getAuthenticatorType(transports);
|
|
if (authenticatorType) {
|
|
parts.push(`(${authenticatorType})`);
|
|
}
|
|
|
|
if (parts.length === 0) {
|
|
return generateDefaultName(transports);
|
|
}
|
|
|
|
return parts.join(" ");
|
|
}
|