码桶
发现社区成员的开源项目
login-options.js1.8 KB
// POST /api/passkey/login-options — generate WebAuthn authentication options
// No authentication required. User provides username to get their allowed credentials.
import { generateAuthenticationOptions } from '@simplewebauthn/server';
export async function onRequestPost(context) {
const { request, env } = context;
const kv = env.CF_DNS_KV;
if (!kv) {
return new Response(JSON.stringify({ error: 'KV storage not configured.' }), {
status: 500, headers: { 'Content-Type': 'application/json' }
});
}
const body = await request.json();
const username = (body.username || '').trim().toLowerCase();
if (!username) {
return new Response(JSON.stringify({ error: 'Username is required.' }), {
status: 400, headers: { 'Content-Type': 'application/json' }
});
}
const url = new URL(request.url);
const rpID = url.hostname;
// Load user's credentials
const credsJson = await kv.get(`PASSKEY_CREDS:${username}`);
const creds = credsJson ? JSON.parse(credsJson) : [];
if (creds.length === 0) {
return new Response(JSON.stringify({ error: 'No passkeys registered for this user.' }), {
status: 404, headers: { 'Content-Type': 'application/json' }
});
}
const options = await generateAuthenticationOptions({
rpID,
allowCredentials: creds.map(c => ({
id: c.credentialId,
transports: c.transports || [],
})),
userVerification: 'preferred',
});
// Store challenge with 5-min TTL
await kv.put(`PASSKEY_CHALLENGE:${options.challenge}`, JSON.stringify({
username,
type: 'authentication',
}), { expirationTtl: 300 });
return new Response(JSON.stringify(options), {
headers: { 'Content-Type': 'application/json' }
});
}