Test client for WildDuck WebAuthn API endpoints
Last active
June 7, 2022 09:07
-
-
Save andris9/41f6e47276a018ce06a96a02cb65f944 to your computer and use it in GitHub Desktop.
Test client for WildDuck WebAuthn API endpoints
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> | |
<!-- Required meta tags --> | |
<meta charset="utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> | |
<!-- Bootstrap CSS --> | |
<link | |
rel="stylesheet" | |
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" | |
integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" | |
crossorigin="anonymous" | |
/> | |
<title>Hello, world!</title> | |
</head> | |
<body> | |
<main role="main"> | |
<div class="container"> | |
<h1 class="mt-5">WebAuthn API test</h1> | |
<div> | |
<div class="form-group row"> | |
<label for="userId" class="col-sm-2 col-form-label">User ID</label> | |
<div class="col-sm-10"><input type="text" class="form-control" id="userId" value="61bd131e81131cf059aa466e" /></div> | |
</div> | |
<div class="form-group row"> | |
<label for="apiUrl" class="col-sm-2 col-form-label">API URL</label> | |
<div class="col-sm-10"> | |
<input type="url" class="form-control" id="apiUrl" value="https://localapi.kreata.ee" /> | |
</div> | |
</div> | |
<div class="form-group row"> | |
<label for="rpId" class="col-sm-2 col-form-label">Relaying party ID</label> | |
<div class="col-sm-10"><input type="text" class="form-control" id="rpId" value="localdev.kreata.ee" /></div> | |
</div> | |
</div> | |
<div class="btn-group" role="group"> | |
<button class="btn btn-primary" id="btn-refresh">Refresh list</button> | |
<button class="btn btn-primary" id="btn-reg">Register platform key</button> | |
<button class="btn btn-primary" id="btn-reg2">Register cross-platform key</button> | |
<button class="btn btn-primary" id="btn-auth">Authenticate using platform key</button> | |
<button class="btn btn-primary" id="btn-auth2">Authenticate using cross-platform key</button> | |
</div> | |
<ul id="creds-list" class="list-group mt-3"></ul> | |
</div> | |
</main> | |
<!-- Optional JavaScript; choose one of the two! --> | |
<!-- Option 1: jQuery and Bootstrap Bundle (includes Popper) --> | |
<script | |
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.slim.min.js" | |
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" | |
crossorigin="anonymous" | |
></script> | |
<script | |
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" | |
integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" | |
crossorigin="anonymous" | |
></script> | |
<script> | |
document.addEventListener('DOMContentLoaded', () => { | |
// cache user id and API url in local storage | |
let getUserId = () => { | |
const USER_ID = document.getElementById('userId').value.trim(); | |
const API_URL = new URL(document.getElementById('apiUrl').value.trim()).origin; | |
const RP_ID = document.getElementById('rpId').value.trim(); | |
if (USER_ID && localStorage.getItem('webauthn_user_id') !== USER_ID) { | |
localStorage.setItem('webauthn_user_id', USER_ID); | |
} | |
if (API_URL && localStorage.getItem('webauthn_api_url') !== API_URL) { | |
localStorage.setItem('webauthn_api_url', API_URL); | |
} | |
if (RP_ID && localStorage.getItem('webauthn_rp_id') !== RP_ID) { | |
localStorage.setItem('webauthn_rp_id', RP_ID); | |
} | |
return { USER_ID, API_URL, RP_ID }; | |
}; | |
// redraw key listing | |
async function repaintList() { | |
const { USER_ID, API_URL } = getUserId(); | |
let listResponse = await fetch(`${API_URL}/users/${USER_ID}/2fa/webauthn/credentials`, { | |
headers: {} | |
}); | |
const listElm = document.getElementById('creds-list'); | |
listElm.innerHTML = ''; | |
const listData = await listResponse.json(); | |
if (!listData.success) { | |
throw new Error('Failed to load credentials'); | |
} | |
for (let credential of listData.credentials) { | |
let rowElm = document.createElement('li'); | |
rowElm.classList.add('list-group-item'); | |
let textElm = document.createElement('span'); | |
textElm.textContent = credential.description; | |
let btnElm = document.createElement('button'); | |
btnElm.textContent = 'Delete'; | |
btnElm.classList.add('btn', 'btn-sm', 'btn-danger', 'float-right'); | |
btnElm.addEventListener('click', e => { | |
if (!confirm('Are you sure?')) { | |
return; | |
} | |
let deleteRes = fetch(`${API_URL}/users/${USER_ID}/2fa/webauthn/credentials/${credential.id}`, { | |
method: 'delete' | |
}) | |
.then(() => repaintList()) | |
.catch(err => console.log(err)); | |
}); | |
rowElm.appendChild(textElm); | |
rowElm.appendChild(btnElm); | |
listElm.appendChild(rowElm); | |
} | |
} | |
// converts hex values from server to ArrayBuffer values | |
function decodeChallengeOptions(registrationOptions) { | |
let fromHex = str => new Uint8Array(str.match(/../g).map(h => parseInt(h, 16))).buffer; | |
let decoded = Object.assign({}, registrationOptions, { | |
challenge: fromHex(registrationOptions.challenge) | |
}); | |
if (decoded.user) { | |
decoded.user = Object.assign({}, decoded.user, { | |
id: Uint8Array.from(registrationOptions.user.id, c => c.charCodeAt(0)).buffer | |
}); | |
} | |
// excludeCredentials from hex to Uint8Array | |
for (let key of ['excludeCredentials', 'allowCredentials']) { | |
if (registrationOptions[key] && registrationOptions[key].length) { | |
decoded[key] = registrationOptions[key].map(entry => ({ | |
id: fromHex(entry.rawId), | |
type: entry.type, | |
transports: entry.transports | |
})); | |
} | |
} | |
return decoded; | |
} | |
// converts ArrayBuffer values to hex strings | |
function encodeChallengeResponse(challenge, attestationResult) { | |
let toHex = arrayBuffer => new Uint8Array(arrayBuffer).reduce((a, b) => a + b.toString(16).padStart(2, '0'), ''); | |
let res = { | |
rawId: toHex(attestationResult.rawId), | |
challenge | |
}; | |
for (let key of ['clientDataJSON', 'attestationObject', 'signature', 'authenticatorData']) { | |
if (attestationResult.response[key]) { | |
res[key] = toHex(attestationResult.response[key]); | |
} | |
} | |
return res; | |
} | |
async function registerCredentials(authenticatorAttachment) { | |
const { USER_ID, API_URL, RP_ID } = getUserId(); | |
let challengeRes = await fetch(`${API_URL}/users/${USER_ID}/2fa/webauthn/registration-challenge`, { | |
method: 'post', | |
body: JSON.stringify({ | |
authenticatorAttachment, | |
origin: window.origin, | |
description: `test ${Date.now()} – ${authenticatorAttachment}`, | |
rpId: RP_ID | |
}), | |
headers: { 'Content-Type': 'application/json' } | |
}); | |
const challengeData = await challengeRes.json(); | |
if (!challengeData.success) { | |
throw new Error('Failed to fetch registration data from server'); | |
} | |
let attestationResult; | |
try { | |
attestationResult = await navigator.credentials.create({ | |
publicKey: decodeChallengeOptions(challengeData.registrationOptions) | |
}); | |
} catch (err) { | |
throw err; | |
} | |
// process response | |
let attestationRequestPayload = encodeChallengeResponse(challengeData.registrationOptions.challenge, attestationResult); | |
let attestationRes = await fetch(`${API_URL}/users/${USER_ID}/2fa/webauthn/registration-attestation`, { | |
method: 'post', | |
body: JSON.stringify(Object.assign(attestationRequestPayload, { rpId: RP_ID })), | |
headers: { 'Content-Type': 'application/json' } | |
}); | |
const attestationData = await attestationRes.json(); | |
if (!attestationData.success) { | |
throw new Error('Failed to validate attestation response'); | |
} | |
await repaintList(); | |
} | |
async function authenticateCredentials(authenticatorAttachment) { | |
const { USER_ID, API_URL, RP_ID } = getUserId(); | |
let challengeRes = await fetch(`${API_URL}/users/${USER_ID}/2fa/webauthn/authentication-challenge`, { | |
method: 'post', | |
body: JSON.stringify({ | |
origin: window.origin, | |
authenticatorAttachment, | |
rpId: RP_ID | |
}), | |
headers: { 'Content-Type': 'application/json' } | |
}); | |
const challengeData = await challengeRes.json(); | |
if (!challengeData.success) { | |
throw new Error('Failed to fetch registration data from server'); | |
} | |
let assertionResult = await navigator.credentials.get({ | |
publicKey: decodeChallengeOptions(challengeData.authenticationOptions) | |
}); | |
// process response | |
let assertionRequestPayload = encodeChallengeResponse(challengeData.authenticationOptions.challenge, assertionResult); | |
let assertionResponse = await fetch(`${API_URL}/users/${USER_ID}/2fa/webauthn/authentication-assertion`, { | |
method: 'post', | |
body: JSON.stringify(Object.assign(assertionRequestPayload, { rpId: RP_ID })), | |
headers: { 'Content-Type': 'application/json' } | |
}); | |
const assertionData = await assertionResponse.json(); | |
if (!assertionData.success) { | |
throw new Error('Failed to validate assertion response'); | |
} | |
alert(`Authenticated using credentials: "${assertionData.response.credential}"`); | |
} | |
document.getElementById('btn-reg').addEventListener('click', () => { | |
registerCredentials('platform').catch(err => { | |
console.error(err); | |
}); | |
}); | |
document.getElementById('btn-reg2').addEventListener('click', () => { | |
registerCredentials('cross-platform').catch(err => { | |
console.error(err); | |
}); | |
}); | |
document.getElementById('btn-auth').addEventListener('click', () => { | |
authenticateCredentials('platform').catch(err => { | |
console.error(err); | |
}); | |
}); | |
document.getElementById('btn-auth2').addEventListener('click', () => { | |
authenticateCredentials('cross-platform').catch(err => { | |
console.error(err); | |
}); | |
}); | |
document.getElementById('btn-refresh').addEventListener('click', () => { | |
repaintList().catch(err => { | |
console.error(err); | |
}); | |
}); | |
if (localStorage.getItem('webauthn_user_id')) { | |
document.getElementById('userId').value = localStorage.getItem('webauthn_user_id'); | |
} | |
if (localStorage.getItem('webauthn_api_url')) { | |
document.getElementById('apiUrl').value = localStorage.getItem('webauthn_api_url'); | |
} | |
repaintList().catch(err => { | |
console.error(err); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment