A non-functional draft, saving to allow reverts.
This commit is contained in:
55
static/app.js
Normal file
55
static/app.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const { startRegistration, startAuthentication } = SimpleWebAuthnBrowser
|
||||
|
||||
async function register(username) {
|
||||
// Registration chat
|
||||
const ws = await aWebSocket('/ws/register')
|
||||
ws.send(username)
|
||||
const optionsJSON = JSON.parse(await ws.recv())
|
||||
if (optionsJSON.error) throw new Error(optionsJSON.error)
|
||||
ws.send(JSON.stringify(await startRegistration({optionsJSON})))
|
||||
const result = JSON.parse(await ws.recv())
|
||||
if (result.error) throw new Error(`Server: ${result.error}`)
|
||||
}
|
||||
|
||||
async function authenticate() {
|
||||
// Authentication chat
|
||||
const ws = await aWebSocket('/ws/authenticate')
|
||||
ws.send('') // Send empty string to trigger authentication
|
||||
const optionsJSON = JSON.parse(await ws.recv())
|
||||
if (optionsJSON.error) throw new Error(optionsJSON.error)
|
||||
ws.send(JSON.stringify(await startAuthentication({optionsJSON})))
|
||||
const result = JSON.parse(await ws.recv())
|
||||
if (result.error) throw new Error(`Server: ${result.error}`)
|
||||
return result
|
||||
}
|
||||
|
||||
(function() {
|
||||
const regForm = document.getElementById('registrationForm')
|
||||
const regSubmitBtn = regForm.querySelector('button[type="submit"]')
|
||||
regForm.addEventListener('submit', ev => {
|
||||
ev.preventDefault()
|
||||
regSubmitBtn.disabled = true
|
||||
const username = (new FormData(regForm)).get('username')
|
||||
register(username).then(() => {
|
||||
alert(`Registration successful for ${username}!`)
|
||||
}).catch(err => {
|
||||
alert(`Registration failed: ${err.message}`)
|
||||
}).finally(() => {
|
||||
regSubmitBtn.disabled = false
|
||||
})
|
||||
})
|
||||
|
||||
const authForm = document.getElementById('authenticationForm')
|
||||
const authSubmitBtn = authForm.querySelector('button[type="submit"]')
|
||||
authForm.addEventListener('submit', ev => {
|
||||
ev.preventDefault()
|
||||
authSubmitBtn.disabled = true
|
||||
authenticate().then(result => {
|
||||
alert(`Authentication successful! Welcome ${result.username}`)
|
||||
}).catch(err => {
|
||||
alert(`Authentication failed: ${err.message}`)
|
||||
}).finally(() => {
|
||||
authSubmitBtn.disabled = false
|
||||
})
|
||||
})
|
||||
})()
|
||||
43
static/awaitable-websocket.js
Normal file
43
static/awaitable-websocket.js
Normal file
@@ -0,0 +1,43 @@
|
||||
class AwaitableWebSocket extends WebSocket {
|
||||
#received = []
|
||||
#waiting = []
|
||||
#err = null
|
||||
#opened = false
|
||||
|
||||
constructor(resolve, reject, url, protocols) {
|
||||
super(url, protocols)
|
||||
this.onopen = () => {
|
||||
this.#opened = true
|
||||
resolve(this)
|
||||
}
|
||||
this.onmessage = e => {
|
||||
if (this.#waiting.length) this.#waiting.shift().resolve(e.data)
|
||||
else this.#received.push(e.data)
|
||||
}
|
||||
this.onclose = e => {
|
||||
if (!this.#opened) {
|
||||
reject(new Error(`WebSocket ${this.url} failed to connect, code ${e.code}`))
|
||||
return
|
||||
}
|
||||
this.#err = e.wasClean
|
||||
? new Error(`Websocket ${this.url} closed ${e.code}`)
|
||||
: new Error(`WebSocket ${this.url} closed with error ${e.code}`)
|
||||
this.#waiting.splice(0).forEach(p => p.reject(this.#err))
|
||||
}
|
||||
}
|
||||
|
||||
recv() {
|
||||
// If we have a message already received, return it immediately
|
||||
if (this.#received.length) return Promise.resolve(this.#received.shift())
|
||||
// Wait for incoming messages, if we have an error, reject immediately
|
||||
if (this.#err) return Promise.reject(this.#err)
|
||||
return new Promise((resolve, reject) => this.#waiting.push({ resolve, reject }))
|
||||
}
|
||||
}
|
||||
|
||||
// Construct an async WebSocket with await aWebSocket(url)
|
||||
function aWebSocket(url, protocols) {
|
||||
return new Promise((resolve, reject) => {
|
||||
new AwaitableWebSocket(resolve, reject, url, protocols)
|
||||
})
|
||||
}
|
||||
68
static/index.html
Normal file
68
static/index.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebAuthn Registration Demo</title>
|
||||
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
|
||||
<script src="/static/awaitable-websocket.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
input[type="text"] {
|
||||
padding: 10px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
margin: 10px;
|
||||
width: 250px;
|
||||
}
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
margin: 10px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>WebAuthn Demo</h1>
|
||||
|
||||
<div style="margin-bottom: 30px;">
|
||||
<h2>Register</h2>
|
||||
<form id="registrationForm">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
<br>
|
||||
<button type="submit">Register Passkey</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Authenticate</h2>
|
||||
<form id="authenticationForm">
|
||||
<button type="submit">Authenticate with Passkey</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user