WebAuthn and Security APIs
1. Web Authentication (navigator.credentials.create, get)
| Method | Description | Browser Support |
|---|---|---|
| navigator.credentials.create(options) | Creates new credential (registration). Options include publicKey for WebAuthn. |
All Modern Browsers |
| navigator.credentials.get(options) | Gets credential for authentication. Options include publicKey for WebAuthn. |
All Modern Browsers |
| PublicKeyCredential Property | Type | Description |
|---|---|---|
| id | string |
Base64url-encoded credential ID. |
| rawId | ArrayBuffer |
Raw credential ID bytes. |
| type | string |
Always "public-key" for WebAuthn. |
| response | AuthenticatorResponse |
Authenticator response (attestation or assertion). |
Example: WebAuthn registration
// Registration (create new credential)
async function registerWebAuthn(username) {
try {
// Get challenge from server
const challengeResponse = await fetch("/auth/register/challenge", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({ "username": username })
});
const { "challenge": challenge, "userId": userId } = await challengeResponse.json();
// Create credential
const credential = await navigator.credentials.create({
"publicKey": {
"challenge": base64ToArrayBuffer(challenge),
"rp": {
"name": "My App",
"id": "example.com" // Domain without protocol/port
},
"user": {
"id": base64ToArrayBuffer(userId),
"name": username,
"displayName": username
},
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 }, // ES256
{ "type": "public-key", "alg": -257 } // RS256
],
"timeout": 60000, // 60 seconds
"attestation": "none", // "none", "indirect", or "direct"
"authenticatorSelection": {
"authenticatorAttachment": "platform", // "platform" (built-in) or "cross-platform" (USB key)
"requireResidentKey": false,
"userVerification": "preferred" // "required", "preferred", or "discouraged"
}
}
});
// Send credential to server for verification
const registrationResponse = await fetch("/auth/register/verify", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"id": credential.id,
"rawId": arrayBufferToBase64(credential.rawId),
"type": credential.type,
"response": {
"clientDataJSON": arrayBufferToBase64(credential.response.clientDataJSON),
"attestationObject": arrayBufferToBase64(credential.response.attestationObject)
}
})
});
const result = await registrationResponse.json();
console.log("Registration successful:", result);
return result;
} catch (error) {
console.error("WebAuthn registration failed:", error);
throw error;
}
}
// Helper functions
function base64ToArrayBuffer(base64) {
const binary = atob(base64.replace(/-/g, "+").replace(/_/g, "/"));
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
Example: WebAuthn authentication
// Authentication (get existing credential)
async function authenticateWebAuthn(username) {
try {
// Get challenge from server
const challengeResponse = await fetch("/auth/login/challenge", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({ "username": username })
});
const { "challenge": challenge, "allowCredentials": allowCredentials } = await challengeResponse.json();
// Get credential
const credential = await navigator.credentials.get({
"publicKey": {
"challenge": base64ToArrayBuffer(challenge),
"timeout": 60000,
"rpId": "example.com",
"allowCredentials": allowCredentials.map(cred => ({
"type": "public-key",
"id": base64ToArrayBuffer(cred.id)
})),
"userVerification": "preferred"
}
});
// Send assertion to server for verification
const authResponse = await fetch("/auth/login/verify", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"id": credential.id,
"rawId": arrayBufferToBase64(credential.rawId),
"type": credential.type,
"response": {
"clientDataJSON": arrayBufferToBase64(credential.response.clientDataJSON),
"authenticatorData": arrayBufferToBase64(credential.response.authenticatorData),
"signature": arrayBufferToBase64(credential.response.signature),
"userHandle": credential.response.userHandle ?
arrayBufferToBase64(credential.response.userHandle) : null
}
})
});
const result = await authResponse.json();
console.log("Authentication successful:", result);
return result;
} catch (error) {
console.error("WebAuthn authentication failed:", error);
throw error;
}
}
// Check browser support
if (window.PublicKeyCredential) {
console.log("WebAuthn supported");
// Check platform authenticator availability
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
.then(available => {
console.log("Platform authenticator available:", available);
});
} else {
console.log("WebAuthn not supported");
}
Note: WebAuthn enables passwordless authentication using biometrics or security keys. More secure than passwords - phishing resistant. Requires HTTPS. Server must validate attestation/assertion. Use platform authenticators (Face ID, Touch ID, Windows Hello) for best UX.
Warning: WebAuthn requires server-side implementation for challenge generation and verification. Always use proper challenge validation to prevent replay attacks. Test on multiple platforms - behavior varies. Fallback to password auth for browsers without support.
2. Credential Management (PasswordCredential, FederatedCredential)
| Method | Description | Browser Support |
|---|---|---|
| navigator.credentials.create(options) | Creates credential. Options: password for PasswordCredential. |
Chrome, Edge |
| navigator.credentials.get(options) | Gets stored credential. Options: password, mediation. |
Chrome, Edge |
| navigator.credentials.store(credential) | Stores credential in browser's password manager. | Chrome, Edge |
| navigator.credentials.preventSilentAccess() | Prevents automatic sign-in after sign-out. | Chrome, Edge |
| PasswordCredential Property | Type | Description |
|---|---|---|
| id | string |
Username/email. |
| password | string |
Password. |
| name | string |
Display name. |
| iconURL | string |
Icon URL. |
Example: Password credential management
// Auto sign-in on page load
async function autoSignIn() {
try {
// Get stored credential
const credential = await navigator.credentials.get({
"password": true,
"mediation": "optional" // "optional", "silent", or "required"
});
if (credential) {
console.log("Credential found:", credential.id);
// Use credential to sign in
const response = await fetch("/auth/login", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"username": credential.id,
"password": credential.password
})
});
if (response.ok) {
console.log("Auto sign-in successful");
return true;
}
} else {
console.log("No stored credentials");
}
} catch (error) {
console.error("Auto sign-in failed:", error);
}
return false;
}
// Store credential after successful registration/login
async function storeCredential(username, password, name) {
try {
// Create password credential
const credential = new PasswordCredential({
"id": username,
"password": password,
"name": name,
"iconURL": "/images/avatar.png"
});
// Store credential
await navigator.credentials.store(credential);
console.log("Credential stored");
return true;
} catch (error) {
console.error("Failed to store credential:", error);
return false;
}
}
// Alternative: create from form
async function storeCredentialFromForm() {
const form = document.querySelector("form");
try {
const credential = await navigator.credentials.create({
"password": form
});
await navigator.credentials.store(credential);
console.log("Credential from form stored");
} catch (error) {
console.error("Failed to store credential from form:", error);
}
}
// Prevent auto sign-in after logout
async function signOut() {
// Prevent silent access on next visit
await navigator.credentials.preventSilentAccess();
console.log("Signed out - auto sign-in disabled");
// Clear session
await fetch("/auth/logout", { "method": "POST" });
}
// Check support
if (window.PasswordCredential) {
console.log("Credential Management API supported");
// Try auto sign-in
autoSignIn().then(success => {
if (!success) {
// Show login form
document.querySelector(".login-form").style.display = "block";
}
});
} else {
console.log("Credential Management API not supported");
}
Note: Credential Management API has limited browser support (Chrome, Edge). Integrates with browser's password manager. Use
mediation: "silent" for automatic sign-in, "required" to always show account picker. Always call preventSilentAccess() on sign-out.
Warning: API doesn't store passwords itself - relies on browser's password manager. Not available in Safari/Firefox. Always provide fallback manual login. Don't rely on this for security - still validate on server.
3. Payment Request (new PaymentRequest, show)
| Method | Description | Browser Support |
|---|---|---|
| new PaymentRequest(methods, details, options) | Creates payment request. Methods: payment methods, details: items/total, options: shipping. | Chrome, Edge, Safari |
| request.show() | Shows payment UI. Returns Promise<PaymentResponse>. Requires user gesture. | Chrome, Edge, Safari |
| request.canMakePayment() | Checks if user can make payment. Returns Promise<boolean>. | Chrome, Edge, Safari |
| request.abort() | Aborts payment request. | Chrome, Edge, Safari |
| PaymentResponse Method | Description |
|---|---|
| response.complete(result) | Completes payment. Result: "success", "fail", or "unknown". |
| response.retry(errors) | Retries payment with error messages. |
Example: Payment Request API
// Process payment
async function processPayment() {
try {
// Check support
if (!window.PaymentRequest) {
console.log("Payment Request API not supported");
return;
}
// Define payment methods
const paymentMethods = [
{
"supportedMethods": "basic-card",
"data": {
"supportedNetworks": ["visa", "mastercard", "amex"],
"supportedTypes": ["debit", "credit"]
}
}
];
// Define payment details
const paymentDetails = {
"total": {
"label": "Total",
"amount": { "currency": "USD", "value": "99.99" }
},
"displayItems": [
{
"label": "Product",
"amount": { "currency": "USD", "value": "89.99" }
},
{
"label": "Shipping",
"amount": { "currency": "USD", "value": "10.00" }
}
]
};
// Optional: shipping options
const options = {
"requestShipping": true,
"requestPayerName": true,
"requestPayerEmail": true,
"requestPayerPhone": true,
"shippingOptions": [
{
"id": "standard",
"label": "Standard Shipping",
"amount": { "currency": "USD", "value": "10.00" },
"selected": true
},
{
"id": "express",
"label": "Express Shipping",
"amount": { "currency": "USD", "value": "20.00" }
}
]
};
// Create payment request
const request = new PaymentRequest(paymentMethods, paymentDetails, options);
// Check if user can make payment
const canMakePayment = await request.canMakePayment();
if (!canMakePayment) {
console.log("User cannot make payment");
return;
}
// Listen for shipping address change
request.addEventListener("shippingaddresschange", async (e) => {
// Update shipping options based on address
e.updateWith(new Promise((resolve) => {
// Calculate shipping based on address
const shippingAddress = request.shippingAddress;
console.log("Shipping to:", shippingAddress.country);
// Update details
resolve({
"total": {
"label": "Total",
"amount": { "currency": "USD", "value": "99.99" }
},
"shippingOptions": options.shippingOptions
});
}));
});
// Listen for shipping option change
request.addEventListener("shippingoptionchange", async (e) => {
const shippingOption = request.shippingOption;
console.log("Shipping option:", shippingOption);
// Update total
const shippingCost = shippingOption === "express" ? "20.00" : "10.00";
const total = (89.99 + parseFloat(shippingCost)).toFixed(2);
e.updateWith({
"total": {
"label": "Total",
"amount": { "currency": "USD", "value": total }
}
});
});
// Show payment UI
const response = await request.show();
// Get payment details
console.log("Card number:", response.details.cardNumber);
console.log("Payer name:", response.payerName);
console.log("Payer email:", response.payerEmail);
console.log("Shipping address:", response.shippingAddress);
// Process payment on server
const serverResponse = await fetch("/api/process-payment", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"paymentMethod": response.methodName,
"details": response.details,
"shippingAddress": response.shippingAddress,
"total": paymentDetails.total.amount.value
})
});
if (serverResponse.ok) {
// Complete payment successfully
await response.complete("success");
console.log("Payment successful");
} else {
// Payment failed
await response.complete("fail");
console.log("Payment failed");
}
} catch (error) {
console.error("Payment error:", error);
if (error.name === "AbortError") {
console.log("Payment cancelled by user");
}
}
}
// Trigger on button click (requires user gesture)
document.getElementById("pay-button").addEventListener("click", processPayment);
Note: Payment Request API provides native payment UI across browsers. Supports credit cards, digital wallets (Apple Pay, Google Pay). Must call
show() from user gesture. Always call complete() to close payment UI.
Warning: Payment Request API only handles UI - doesn't process payments. Must implement server-side payment processing. Never trust client-side data - validate on server. Test on mobile - UX varies by platform.
4. Permissions API (navigator.permissions.query)
| Method | Description | Browser Support |
|---|---|---|
| navigator.permissions.query(descriptor) | Queries permission state. Descriptor: { name: "camera" }. Returns Promise<PermissionStatus>. |
All Modern Browsers |
| Permission Name | Description | Support |
|---|---|---|
| camera | Camera access (getUserMedia). | Good |
| microphone | Microphone access (getUserMedia). | Good |
| geolocation | Location access. | Good |
| notifications | Notification permission. | Good |
| push | Push notification permission. | Modern |
| persistent-storage | Persistent storage quota. | Limited |
| PermissionStatus Property | Type | Description |
|---|---|---|
| state | string |
"granted", "denied", or "prompt". |
| onchange | EventHandler |
Fires when permission state changes. |
Example: Check and request permissions
// Check permission status
async function checkPermission(name) {
try {
const status = await navigator.permissions.query({ "name": name });
console.log(`${name} permission:`, status.state);
// Listen for changes
status.addEventListener("change", () => {
console.log(`${name} permission changed to:`, status.state);
});
return status.state;
} catch (error) {
console.error(`Failed to query ${name} permission:`, error);
return null;
}
}
// Camera permission
async function requestCameraAccess() {
const status = await checkPermission("camera");
if (status === "granted") {
console.log("Camera already granted");
return true;
} else if (status === "denied") {
console.log("Camera denied - show instructions to enable");
return false;
} else if (status === "prompt") {
console.log("Camera will prompt user");
// Request access (triggers permission prompt)
try {
const stream = await navigator.mediaDevices.getUserMedia({ "video": true });
stream.getTracks().forEach(track => track.stop());
return true;
} catch (error) {
console.error("Camera access denied:", error);
return false;
}
}
}
// Geolocation permission
async function requestLocationAccess() {
const status = await navigator.permissions.query({ "name": "geolocation" });
console.log("Geolocation permission:", status.state);
if (status.state === "granted") {
// Already granted - get location
navigator.geolocation.getCurrentPosition(
(position) => console.log("Location:", position.coords),
(error) => console.error("Location error:", error)
);
} else {
// Will prompt or is denied
navigator.geolocation.getCurrentPosition(
(position) => console.log("Location granted:", position.coords),
(error) => console.error("Location denied:", error)
);
}
}
// Notification permission
async function requestNotificationAccess() {
const status = await navigator.permissions.query({ "name": "notifications" });
console.log("Notification permission:", status.state);
if (status.state === "prompt") {
// Request permission
const result = await Notification.requestPermission();
console.log("Notification permission result:", result);
}
}
// Check multiple permissions
async function checkMultiplePermissions() {
const permissions = ["camera", "microphone", "geolocation", "notifications"];
const results = await Promise.all(
permissions.map(async (name) => {
try {
const status = await navigator.permissions.query({ "name": name });
return { "name": name, "state": status.state };
} catch (error) {
return { "name": name, "state": "unsupported" };
}
})
);
console.log("Permission states:");
results.forEach(({ "name": name, "state": state }) => {
console.log(` ${name}: ${state}`);
});
return results;
}
// Permission-aware UI
async function updateUIBasedOnPermissions() {
const cameraStatus = await checkPermission("camera");
const cameraButton = document.getElementById("camera-button");
if (cameraStatus === "granted") {
cameraButton.textContent = "Open Camera";
cameraButton.disabled = false;
} else if (cameraStatus === "denied") {
cameraButton.textContent = "Camera Denied";
cameraButton.disabled = true;
// Show instructions to enable in settings
} else {
cameraButton.textContent = "Allow Camera";
cameraButton.disabled = false;
}
}
Note: Permissions API only queries state - doesn't request permission. To request, use the actual API (getUserMedia, Notification.requestPermission, etc.). Use
onchange to update UI when user changes permission in browser settings.
Warning: Not all permissions queryable on all browsers. Some permissions always return "prompt" state. Don't rely solely on Permissions API - handle permission errors when using actual APIs. Browser support varies by permission type.
5. CSP Reporting (SecurityPolicyViolationEvent)
| Event | Description | Browser Support |
|---|---|---|
| securitypolicyviolation | Fires when CSP violation occurs. Event has violation details. | All Modern Browsers |
| SecurityPolicyViolationEvent Property | Type | Description |
|---|---|---|
| violatedDirective | string |
CSP directive that was violated (e.g., "script-src"). |
| effectiveDirective | string |
Effective directive that caused violation. |
| blockedURI | string |
URI that was blocked. |
| sourceFile | string |
File where violation occurred. |
| lineNumber | number |
Line number of violation. |
| columnNumber | number |
Column number of violation. |
| disposition | string |
"enforce" or "report" (report-only mode). |
Example: CSP violation monitoring
// Listen for CSP violations
document.addEventListener("securitypolicyviolation", (e) => {
console.warn("CSP Violation detected:");
console.log("Violated directive:", e.violatedDirective);
console.log("Effective directive:", e.effectiveDirective);
console.log("Blocked URI:", e.blockedURI);
console.log("Source file:", e.sourceFile);
console.log("Line:", e.lineNumber, "Column:", e.columnNumber);
console.log("Disposition:", e.disposition);
console.log("Original policy:", e.originalPolicy);
// Send violation to analytics
sendCSPViolation({
"directive": e.violatedDirective,
"blockedURI": e.blockedURI,
"sourceFile": e.sourceFile,
"lineNumber": e.lineNumber,
"disposition": e.disposition,
"timestamp": Date.now(),
"userAgent": navigator.userAgent
});
});
// Send CSP violations to server
function sendCSPViolation(violation) {
// Use sendBeacon for reliability
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/csp-violations", JSON.stringify(violation));
} else {
fetch("/api/csp-violations", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify(violation)
}).catch(error => console.error("Failed to send CSP violation:", error));
}
}
// CSP violation aggregator
class CSPMonitor {
constructor() {
this.violations = [];
this.listeners = [];
document.addEventListener("securitypolicyviolation", (e) => {
this.handleViolation(e);
});
}
handleViolation(event) {
const violation = {
"directive": event.violatedDirective,
"effectiveDirective": event.effectiveDirective,
"blockedURI": event.blockedURI,
"sourceFile": event.sourceFile,
"lineNumber": event.lineNumber,
"columnNumber": event.columnNumber,
"disposition": event.disposition,
"timestamp": Date.now()
};
this.violations.push(violation);
// Notify listeners
this.listeners.forEach(callback => callback(violation));
// Auto-report after 10 violations or 5 seconds
if (this.violations.length >= 10) {
this.report();
} else if (this.violations.length === 1) {
setTimeout(() => this.report(), 5000);
}
}
report() {
if (this.violations.length === 0) return;
const violations = this.violations.slice();
this.violations = [];
console.log(`Reporting ${violations.length} CSP violations`);
fetch("/api/csp-violations/batch", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({ "violations": violations })
}).catch(error => console.error("Failed to report CSP violations:", error));
}
onViolation(callback) {
this.listeners.push(callback);
}
getViolations() {
return this.violations.slice();
}
}
// Initialize monitor
const cspMonitor = new CSPMonitor();
cspMonitor.onViolation((violation) => {
console.warn("CSP violation:", violation);
// Show warning in dev mode
if (process.env.NODE_ENV === "development") {
console.warn("Fix CSP violation:", violation.blockedURI);
}
});
Note: CSP violations fire
securitypolicyviolation event. Use to monitor and fix CSP issues. Set CSP in report-only mode during development with Content-Security-Policy-Report-Only header. Aggregate violations before sending to reduce requests.
6. SubtleCrypto (crypto.subtle.encrypt, decrypt, sign)
| Method | Description | Browser Support |
|---|---|---|
| crypto.subtle.encrypt(algorithm, key, data) | Encrypts data. Returns Promise<ArrayBuffer>. | All Modern Browsers |
| crypto.subtle.decrypt(algorithm, key, data) | Decrypts data. Returns Promise<ArrayBuffer>. | All Modern Browsers |
| crypto.subtle.digest(algorithm, data) | Hashes data. Algorithm: "SHA-1", "SHA-256", "SHA-384", "SHA-512". | All Modern Browsers |
| crypto.subtle.generateKey(algorithm, extractable, keyUsages) | Generates key. Returns Promise<CryptoKey>. | All Modern Browsers |
| crypto.subtle.importKey(format, keyData, algorithm, extractable, keyUsages) | Imports key. Format: "raw", "pkcs8", "spki", "jwk". | All Modern Browsers |
| crypto.subtle.exportKey(format, key) | Exports key. Returns Promise<ArrayBuffer | JsonWebKey>. | All Modern Browsers |
| crypto.subtle.sign(algorithm, key, data) | Signs data. Returns Promise<ArrayBuffer>. | All Modern Browsers |
| crypto.subtle.verify(algorithm, key, signature, data) | Verifies signature. Returns Promise<boolean>. | All Modern Browsers |
Example: Hash data with SHA-256
// Hash string with SHA-256
async function hashString(message) {
// Encode string to bytes
const encoder = new TextEncoder();
const data = encoder.encode(message);
// Hash data
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
// Convert to hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
return hashHex;
}
// Usage
const hash = await hashString("Hello, World!");
console.log("SHA-256 hash:", hash);
Example: Encrypt/decrypt with AES-GCM
// Generate AES-GCM key
async function generateAESKey() {
const key = await crypto.subtle.generateKey(
{
"name": "AES-GCM",
"length": 256
},
true, // extractable
["encrypt", "decrypt"]
);
return key;
}
// Encrypt data
async function encryptData(key, data) {
const encoder = new TextEncoder();
const encoded = encoder.encode(data);
// Generate random IV (initialization vector)
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt
const encrypted = await crypto.subtle.encrypt(
{
"name": "AES-GCM",
"iv": iv
},
key,
encoded
);
// Return IV + encrypted data
return {
"iv": Array.from(iv),
"data": Array.from(new Uint8Array(encrypted))
};
}
// Decrypt data
async function decryptData(key, encryptedData) {
const decrypted = await crypto.subtle.decrypt(
{
"name": "AES-GCM",
"iv": new Uint8Array(encryptedData.iv)
},
key,
new Uint8Array(encryptedData.data)
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}
// Usage
const key = await generateAESKey();
const encrypted = await encryptData(key, "Secret message");
const decrypted = await decryptData(key, encrypted);
console.log("Decrypted:", decrypted);
Note: SubtleCrypto API is for cryptographic operations - hashing, encryption, signing. Only available in secure contexts (HTTPS). Use AES-GCM for encryption, SHA-256 for hashing, RSA/ECDSA for signing. All methods return Promises.
Warning: Crypto is complex - use established libraries when possible. Never implement your own crypto algorithms. Always use random IVs for encryption. Don't use for password storage - use server-side bcrypt/argon2. SubtleCrypto requires HTTPS.
Authentication and Security Best Practices
- Use WebAuthn for passwordless authentication - more secure than passwords
- Implement proper challenge validation for WebAuthn on server
- Credential Management API has limited support - provide fallback
- Payment Request API only handles UI - implement server-side processing
- Use Permissions API to check state before requesting - better UX
- Monitor CSP violations with
securitypolicyviolationevent - Use SubtleCrypto for client-side encryption - requires HTTPS
- Never implement custom crypto - use established algorithms
- Always validate and sanitize data on server - never trust client
- Use HTTPS for all security-sensitive operations
- Test security features across browsers - support varies
- Provide clear error messages when permissions denied