Skip to content

Instantly share code, notes, and snippets.

@andris9
Last active June 7, 2022 09:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andris9/41f6e47276a018ce06a96a02cb65f944 to your computer and use it in GitHub Desktop.
Save andris9/41f6e47276a018ce06a96a02cb65f944 to your computer and use it in GitHub Desktop.
Test client for WildDuck WebAuthn API endpoints

Test client for WildDuck WebAuthn API endpoints

<!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/bootstrap@4.6.1/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/jquery@3.5.1/dist/jquery.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/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