Skip to content

Instantly share code, notes, and snippets.

@o-az
Created June 12, 2025 07:31
Show Gist options
  • Save o-az/e8e841d2269b093bdb9779c13f93a63f to your computer and use it in GitHub Desktop.
Save o-az/e8e841d2269b093bdb9779c13f93a63f to your computer and use it in GitHub Desktop.
<!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