expo-passkey
Version:
Passkey authentication for Expo apps with Better Auth integration
169 lines • 5.43 kB
JavaScript
/**
* @file Web-specific utilities for WebAuthn. Runs only in web environments.
* @module expo-passkey/client/utils/web
*/
// Static import - safe since this file only runs in web environments
import * as webAuthnBrowser from "@simplewebauthn/browser";
/**
* Get WebAuthn browser module (now statically imported)
*/
export function getWebAuthnBrowser() {
return webAuthnBrowser;
}
/**
* Check if a string is already base64url encoded
* Uses a conservative approach to avoid false positives with human-readable strings
*/
function isBase64URLEncoded(str) {
// Must only contain valid base64url characters
if (!/^[A-Za-z0-9_-]+$/.test(str)) {
return false;
}
// Require minimum length but not too restrictive
if (str.length < 4) {
return false;
}
const hasUpperAndLower = /[a-z]/.test(str) && /[A-Z]/.test(str);
const hasBase64UrlChars = /_/.test(str) || (str.match(/-/g) || []).length > 1;
const looksHuman = /^[a-z]+-[0-9]+$/i.test(str) ||
/^[a-z]+[0-9]+$/i.test(str) ||
/^[0-9]+-[a-z]+$/i.test(str);
// If it looks like a human-readable pattern, don't treat as base64url
if (looksHuman) {
return false;
}
// If it has characteristics of base64url, validate by decoding
if (hasUpperAndLower || hasBase64UrlChars) {
try {
let base64 = str;
// Convert base64url to base64
base64 = base64.replace(/-/g, "+").replace(/_/g, "/");
// Add padding if needed
const remainder = base64.length % 4;
if (remainder > 0) {
base64 = base64.padEnd(base64.length + 4 - remainder, "=");
}
// Try to decode (works in both browser and Node.js for testing)
if (typeof atob !== "undefined") {
atob(base64); // Browser environment
}
else {
Buffer.from(base64, "base64"); // Node.js environment (testing)
}
return true;
}
catch {
return false;
}
}
return false;
}
/**
* Convert a string to Base64URL format (for SimpleWebAuthn compatibility)
*/
function toBase64URLString(str) {
// If it's already base64url encoded, return as is
if (isBase64URLEncoded(str)) {
return str;
}
// Convert to base64url
let base64;
if (typeof btoa !== "undefined") {
// Browser environment
base64 = btoa(str);
}
else {
// Node.js environment (for testing)
base64 = Buffer.from(str, "utf8").toString("base64");
}
return base64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
/**
* Create registration options for web (SimpleWebAuthn JSON format)
*/
export function createWebRegistrationOptions(challenge, userId, userName, displayName, rpId, rpName, options) {
return {
rp: {
id: rpId,
name: rpName,
},
user: {
id: toBase64URLString(userId),
name: userName,
displayName: displayName,
},
challenge: toBase64URLString(challenge),
pubKeyCredParams: [
{
type: "public-key",
alg: -7, // ES256
},
{
type: "public-key",
alg: -257, // RS256
},
],
timeout: options?.timeout ?? 60000,
authenticatorSelection: options?.authenticatorSelection ?? {
authenticatorAttachment: "platform",
userVerification: "preferred",
residentKey: "preferred",
},
attestation: options?.attestation ?? "none",
excludeCredentials: options?.excludeCredentials?.map((cred) => ({
id: toBase64URLString(cred.id),
type: cred.type,
transports: ["internal"],
})) ?? [],
};
}
/**
* Create authentication options for web (SimpleWebAuthn JSON format)
*/
export function createWebAuthenticationOptions(challenge, rpId, options) {
return {
challenge: toBase64URLString(challenge),
rpId,
timeout: options?.timeout ?? 60000,
userVerification: options?.userVerification ?? "preferred",
allowCredentials: options?.allowCredentials?.map((cred) => ({
id: toBase64URLString(cred.id),
type: cred.type,
transports: ["internal", "hybrid"],
})) ?? [],
};
}
/**
* Check if WebAuthn is supported in the current browser
*/
export function isWebAuthnSupportedInBrowser() {
return !!(typeof window !== "undefined" &&
window?.PublicKeyCredential &&
typeof window.PublicKeyCredential === "function");
}
/**
* Check if platform authenticator is available in the browser
*/
export async function isPlatformAuthenticatorAvailable() {
if (!isWebAuthnSupportedInBrowser()) {
return false;
}
try {
return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}
catch {
return false;
}
}
/**
* Browser-specific error messages
*/
export const WEB_ERROR_MESSAGES = {
NOT_SECURE_CONTEXT: "WebAuthn requires a secure context (HTTPS)",
BROWSER_NOT_SUPPORTED: "Your browser does not support WebAuthn",
PLATFORM_AUTHENTICATOR_NOT_AVAILABLE: "No platform authenticator available",
};
//# sourceMappingURL=web.js.map