-
Notifications
You must be signed in to change notification settings - Fork 106
Expand file tree
/
Copy pathexample.php
More file actions
214 lines (186 loc) · 10.2 KB
/
example.php
File metadata and controls
214 lines (186 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebAuthn Passkey Example</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; line-height: 1.6; }
.box { border: 1px solid #ddd; padding: 20px; margin-bottom: 20px; border-radius: 8px; background: #fafafa; }
button { padding: 10px 15px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 1em; width: 100%; }
button:hover { background: #0056b3; }
input { padding: 10px; margin-bottom: 15px; width: 100%; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; font-size: 1em; }
#message { margin-top: 20px; padding: 15px; border-radius: 4px; display: none; font-weight: bold; text-align: center; }
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.hint { font-size: 0.85em; color: #666; margin-top: -10px; margin-bottom: 15px; display: block; }
</style>
</head>
<body>
<h2>Passkey Example (SQLite)</h2>
<p>This example demonstrates how to implement secure, modern Passkeys using the <code>lbuchs/WebAuthn</code> library.</p>
<div id="message"></div>
<div class="box">
<h3>1. Register</h3>
<input type="text" id="reg-username" placeholder="Choose a username (e.g., alice)">
<button onclick="register()">Register Passkey</button>
</div>
<div class="box">
<h3>2. Login</h3>
<input type="text" id="login-username" placeholder="Username (optional)">
<span class="hint">Leave blank for Usernameless Login (Discoverable Credentials).</span>
<button onclick="login()">Login with Passkey</button>
</div>
<script>
function showMessage(text, isError = false) {
const msgEl = document.getElementById('message');
msgEl.textContent = text;
msgEl.className = isError ? 'error' : 'success';
msgEl.style.display = 'block';
}
/**
* Recursively searches for WebAuthn binary markers and decodes Base64URL to ArrayBuffer
*/
function recursiveBase64StrToArrayBuffer(obj) {
let prefix = '=?BINARY?B?';
let suffix = '?=';
if (typeof obj === 'object') {
for (let key in obj) {
if (typeof obj[key] === 'string') {
let str = obj[key];
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
str = str.substring(prefix.length, str.length - suffix.length);
// Normalize Base64URL to standard Base64
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4 !== 0) str += '=';
let binary_string = window.atob(str);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
obj[key] = bytes.buffer;
}
} else {
recursiveBase64StrToArrayBuffer(obj[key]);
}
}
}
}
/**
* Encodes ArrayBuffer to Base64 safely
*/
function arrayBufferToBase64(buffer) {
if (!buffer) return null;
let binary = '';
let bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
try {
return window.btoa(binary);
} catch (e) {
return null;
}
}
let preloadedGetArgs = null;
// Preload generic challenge to bypass iOS Safari's strict user-gesture timeout
window.addEventListener('load', async () => {
if (window.fetch && navigator.credentials) {
try {
let rep = await fetch('server.php?fn=getGetArgs', { cache: 'no-cache' });
let repText = await rep.text();
preloadedGetArgs = JSON.parse(repText);
} catch(e) {}
}
});
async function register() {
try {
if (!window.isSecureContext) {
throw new Error("WebAuthn requires a secure context (HTTPS or localhost).");
}
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
throw new Error("Your browser or device does not support WebAuthn Passkeys.");
}
const username = document.getElementById('reg-username').value.trim();
if (!username) throw new Error("A username is required for registration.");
// 1. Get creation arguments from server
let rep = await fetch(`server.php?fn=getCreateArgs&username=${encodeURIComponent(username)}`, { cache: 'no-cache' });
let repText = await rep.text();
let createArgs;
try { createArgs = JSON.parse(repText); } catch(e) { throw new Error('Server error: ' + repText.substring(0, 100)); }
if (createArgs.success === false) throw new Error(createArgs.msg);
// 2. Ask the browser to create the credential
recursiveBase64StrToArrayBuffer(createArgs);
const cred = await navigator.credentials.create(createArgs);
const response = {
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
};
// 3. Send the new credential to the server to verify and store
rep = await fetch('server.php?fn=processCreate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(response),
cache: 'no-cache'
});
repText = await rep.text();
let result;
try { result = JSON.parse(repText); } catch(e) { throw new Error('Server error: ' + repText.substring(0, 100)); }
if (!result.success) throw new Error(result.msg);
showMessage(result.msg);
document.getElementById('reg-username').value = ''; // clear
} catch (e) {
showMessage(e.message, true);
}
}
async function login() {
try {
if (!window.isSecureContext) {
throw new Error("WebAuthn requires a secure context (HTTPS or localhost).");
}
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
throw new Error("Your browser or device does not support WebAuthn Passkeys.");
}
const username = document.getElementById('login-username').value.trim();
let getArgs;
if (username) {
// 1. Get assertion arguments from server
let url = 'server.php?fn=getGetArgs&username=' + encodeURIComponent(username);
let rep = await fetch(url, { cache: 'no-cache' });
let repText = await rep.text();
try { getArgs = JSON.parse(repText); } catch(e) { throw new Error('Server error: ' + repText.substring(0, 100)); }
} else if (preloadedGetArgs) {
// Instantly use preloaded args for zero latency (satisfies iOS user-gesture requirements)
getArgs = JSON.parse(JSON.stringify(preloadedGetArgs));
} else {
let rep = await fetch('server.php?fn=getGetArgs', { cache: 'no-cache' });
let repText = await rep.text();
try { getArgs = JSON.parse(repText); } catch(e) { throw new Error('Server error: ' + repText.substring(0, 100)); }
}
if (getArgs.success === false) throw new Error(getArgs.msg);
// 2. Ask the browser to prompt the user to authenticate
recursiveBase64StrToArrayBuffer(getArgs);
const cred = await navigator.credentials.get(getArgs);
const response = {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
signature: cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null,
userHandle: cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null
};
// 3. Send the signature to the server to verify
rep = await fetch('server.php?fn=processGet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(response),
cache: 'no-cache'
});
repText = await rep.text();
let result;
try { result = JSON.parse(repText); } catch(e) { throw new Error('Server error: ' + repText.substring(0, 100)); }
if (!result.success) throw new Error(result.msg);
showMessage(result.msg);
} catch (e) {
showMessage(e.message, true);
}
}
</script>
</body>
</html>