Created
June 12, 2025 07:31
-
-
Save o-az/e8e841d2269b093bdb9779c13f93a63f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta name="color-scheme" content="light dark" /> | |
<title>FF Android Synced Passkeys</title> | |
<style> | |
body { | |
font-family: system-ui, -apple-system, sans-serif; | |
margin: 20px; | |
max-width: 800px; | |
} | |
section { | |
margin: 20px 0; | |
padding: 20px; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
} | |
input { | |
padding: 8px; | |
margin: 5px; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
} | |
button { | |
padding: 8px 16px; | |
margin: 5px; | |
background: #007bff; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
} | |
button:hover { | |
background: #0056b3; | |
} | |
pre { | |
background: #f5f5f5; | |
padding: 10px; | |
border-radius: 4px; | |
overflow-x: auto; | |
max-width: 100%; | |
} | |
.error { | |
color: #dc3545; | |
background: #f8d7da; | |
padding: 10px; | |
border-radius: 4px; | |
margin: 10px 0; | |
} | |
.result-box { | |
background-color: #f0f0f0; | |
overflow: scroll; | |
white-space: pre-wrap; | |
max-width: 75vw; | |
padding: 10px; | |
border-radius: 4px; | |
} | |
</style> | |
</head> | |
<body> | |
<main> | |
<h1>FF Android Synced Passkeys</h1> | |
<div id="capabilities"> | |
<pre id="webauthn-support"></pre> | |
<pre id="conditional-mediation"></pre> | |
<pre id="platform-authenticator"></pre> | |
</div> | |
<section> | |
<h3>Register</h3> | |
<input type="text" id="register-username" placeholder="Username" /> | |
<button type="button" onclick="signUp()">Register</button> | |
<h3>Login</h3> | |
<button type="button" onclick="userAssertion()" style="width: 150px">Login</button> | |
<pre id="assertion-result"></pre> | |
<h3>Login (with username)</h3> | |
<input type="text" id="login-username" placeholder="Username" /> | |
<button type="button" onclick="userAssertionWithUsername()">Login</button> | |
<pre id="assertion-username-result"></pre> | |
</section> | |
<section> | |
<h3>Credential</h3> | |
<div class="result-box"> | |
<pre id="client-data">No client data yet</pre> | |
</div> | |
<div id="error-container"></div> | |
</section> | |
<section> | |
<h4>Client Capabilities</h4> | |
<pre id="client-capabilities"></pre> | |
</section> | |
</main> | |
<script> | |
const RELAYING_PARTY = window.location.hostname; | |
const supportsWebAuthn = | |
typeof navigator !== "undefined" && | |
typeof navigator.credentials !== "undefined" && | |
typeof navigator.credentials.create !== "undefined" && | |
typeof navigator.credentials.get !== "undefined"; | |
const supportsConditionalMediation = () => | |
supportsWebAuthn && | |
typeof PublicKeyCredential.isConditionalMediationAvailable !== "undefined" && | |
PublicKeyCredential.isConditionalMediationAvailable(); | |
const randomStringFromServer = crypto | |
.getRandomValues(new Uint8Array(32)) | |
.toString(); | |
let currentCredential = null; | |
let lastError = null; | |
async function initializeCapabilities() { | |
document.getElementById('webauthn-support').textContent = | |
`[supports WebAuthn: ${supportsWebAuthn ? "yes" : "no"}]`; | |
try { | |
const conditionalSupport = await supportsConditionalMediation(); | |
document.getElementById('conditional-mediation').textContent = | |
`[supports Conditional Mediation: ${conditionalSupport ? "yes" : "no"}]`; | |
} catch (e) { | |
document.getElementById('conditional-mediation').textContent = | |
`[supports Conditional Mediation: no]`; | |
} | |
try { | |
const platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); | |
document.getElementById('platform-authenticator').textContent = | |
`[userVerifyingPlatformAuthenticatorAvailable: ${platformAvailable ? "yes" : "no"}]`; | |
} catch (e) { | |
document.getElementById('platform-authenticator').textContent = | |
`[userVerifyingPlatformAuthenticatorAvailable: unknown]`; | |
} | |
try { | |
if (PublicKeyCredential.getClientCapabilities) { | |
const capabilities = await PublicKeyCredential.getClientCapabilities(); | |
document.getElementById('client-capabilities').textContent = | |
JSON.stringify(capabilities, null, 2); | |
} | |
} catch (e) { | |
document.getElementById('client-capabilities').textContent = | |
'Client capabilities not available'; | |
} | |
} | |
function showError(error) { | |
const errorContainer = document.getElementById('error-container'); | |
const errorMessage = error instanceof Error ? error.message : String(error); | |
errorContainer.innerHTML = ` | |
<div class="error"> | |
<h3>Error</h3> | |
<pre>${errorMessage}</pre> | |
</div> | |
`; | |
} | |
function clearError() { | |
document.getElementById('error-container').innerHTML = ''; | |
} | |
function parseCredentials(credential) { | |
try { | |
const clientData = JSON.parse( | |
new TextDecoder("utf-8").decode(credential.response.clientDataJSON) | |
); | |
document.getElementById('client-data').textContent = | |
JSON.stringify(clientData, null, 2); | |
} catch (e) { | |
showError(e); | |
} | |
} | |
// Registration function | |
async function signUp() { | |
clearError(); | |
const username = document.getElementById('register-username').value; | |
if (!username) { | |
showError(new Error('Please enter a username')); | |
return; | |
} | |
try { | |
const credential = await navigator.credentials.create({ | |
publicKey: { | |
timeout: 60000, | |
attestation: "none", | |
challenge: Uint8Array.from(randomStringFromServer, c => c.charCodeAt(0)), | |
authenticatorSelection: { | |
requireResidentKey: true, | |
userVerification: "required", | |
authenticatorAttachment: "platform" | |
}, | |
user: { | |
name: username, | |
displayName: username, | |
id: Uint8Array.from(username, c => c.charCodeAt(0)) | |
}, | |
extensions: { | |
credProps: true | |
}, | |
pubKeyCredParams: [ | |
{ type: "public-key", alg: -7 }, | |
{ type: "public-key", alg: -257 }, | |
{ type: "public-key", alg: -8 } | |
], | |
rp: { | |
id: RELAYING_PARTY, | |
name: "FF Android Synced Passkeys" | |
} | |
} | |
}); | |
currentCredential = credential; | |
parseCredentials(credential); | |
} catch (error) { | |
showError(error); | |
} | |
} | |
async function userAssertion() { | |
clearError(); | |
try { | |
const assertion = await navigator.credentials.get({ | |
publicKey: { | |
challenge: Uint8Array.from(randomStringFromServer, c => c.charCodeAt(0)), | |
timeout: 60000, | |
userVerification: "preferred" | |
} | |
}); | |
document.getElementById('assertion-result').textContent = | |
JSON.stringify(assertion, null, 2); | |
} catch (error) { | |
showError(error); | |
document.getElementById('assertion-result').textContent = ''; | |
} | |
} | |
async function userAssertionWithUsername() { | |
clearError(); | |
const username = document.getElementById('login-username').value; | |
if (!username) { | |
showError(new Error('Please enter a username')); | |
return; | |
} | |
try { | |
const assertion = await navigator.credentials.get({ | |
publicKey: { | |
challenge: Uint8Array.from(randomStringFromServer, c => c.charCodeAt(0)), | |
timeout: 60000, | |
userVerification: "preferred" | |
} | |
}); | |
document.getElementById('assertion-username-result').textContent = | |
JSON.stringify(assertion, null, 2); | |
} catch (error) { | |
showError(error); | |
document.getElementById('assertion-username-result').textContent = ''; | |
} | |
} | |
window.addEventListener('DOMContentLoaded', initializeCapabilities); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment