mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-14 09:07:35 +08:00
205 lines
8.4 KiB
TypeScript
205 lines
8.4 KiB
TypeScript
import { Context } from 'hono';
|
|
import { Jwt } from 'hono/utils/jwt'
|
|
import {
|
|
generateRegistrationOptions,
|
|
verifyRegistrationResponse,
|
|
generateAuthenticationOptions,
|
|
verifyAuthenticationResponse
|
|
} from '@simplewebauthn/server';
|
|
|
|
import { HonoCustomType } from '../types';
|
|
import { Passkey } from '../models';
|
|
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
|
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
|
|
export default {
|
|
getPassKeys: async (c: Context<HonoCustomType>) => {
|
|
const user = c.get("userPayload");
|
|
const { results } = await c.env.DB.prepare(
|
|
`SELECT passkey_name, passkey_id, created_at, updated_at FROM user_passkeys WHERE user_id = ?`
|
|
).bind(user.user_id).all<Record<string, string>>();
|
|
return c.json(results);
|
|
},
|
|
renamePassKey: async (c: Context<HonoCustomType>) => {
|
|
const user = c.get("userPayload");
|
|
const { passkey_id, passkey_name } = await c.req.json();
|
|
if (!passkey_name || passkey_name.length > 255) {
|
|
return c.text("Invalid passkey name", 400);
|
|
}
|
|
const { success } = await c.env.DB.prepare(
|
|
`UPDATE user_passkeys SET passkey_name = ? WHERE user_id = ? AND passkey_id = ?`
|
|
).bind(passkey_name, user.user_id, passkey_id).run();
|
|
return c.json({ success });
|
|
},
|
|
deletePassKey: async (c: Context<HonoCustomType>) => {
|
|
const user = c.get("userPayload");
|
|
const { passkey_id } = c.req.param();
|
|
const { success } = await c.env.DB.prepare(
|
|
`DELETE FROM user_passkeys WHERE user_id = ? AND passkey_id = ?`
|
|
).bind(user.user_id, passkey_id).run();
|
|
return c.json({ success });
|
|
},
|
|
registerRequest: async (c: Context<HonoCustomType>) => {
|
|
const user = c.get("userPayload");
|
|
const { domain } = await c.req.json();
|
|
const { results } = await c.env.DB.prepare(
|
|
`SELECT passkey FROM user_passkeys WHERE user_id = ?`
|
|
).bind(user.user_id).all<Record<string, string>>();
|
|
const excludeCredentials = results
|
|
.map((record: any) => JSON.parse(record.passkey) as Passkey)
|
|
.map((passkey: Passkey) => ({
|
|
id: passkey.id,
|
|
transports: passkey.transports,
|
|
}));
|
|
// create challenge with 1 hour expiration
|
|
const challenge = await Jwt.sign({
|
|
user_email: user.user_email,
|
|
user_id: user.user_id,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
}, c.env.JWT_SECRET, "HS256")
|
|
// Use SimpleWebAuthn's handy function to create registration options.
|
|
const options = await generateRegistrationOptions({
|
|
rpName: c.env.TITLE || "Temp Mail",
|
|
rpID: domain,
|
|
userID: new TextEncoder().encode(user.user_id.toString()),
|
|
userName: user.user_email,
|
|
userDisplayName: user.user_email,
|
|
attestationType: 'none',
|
|
excludeCredentials: excludeCredentials,
|
|
challenge: challenge,
|
|
});
|
|
|
|
return c.json(options);
|
|
},
|
|
registerResponse: async (c: Context<HonoCustomType>) => {
|
|
const user = c.get("userPayload");
|
|
const { credential, origin, passkey_name } = await c.req.json();
|
|
// Verify the registration response
|
|
const verification = await verifyRegistrationResponse({
|
|
response: credential,
|
|
expectedChallenge: async (challenge: string) => {
|
|
const payload = await Jwt.verify(atob(challenge), c.env.JWT_SECRET, "HS256");
|
|
if (!payload || !payload.iat) return false;
|
|
// check iad is not older than 5 minutes
|
|
if (Math.floor(Date.now() / 1000) - payload.iat > 300) return false;
|
|
if (payload.user_id !== user.user_id) return false;
|
|
return true;
|
|
},
|
|
expectedOrigin: origin,
|
|
requireUserVerification: true,
|
|
});
|
|
const { verified, registrationInfo } = verification;
|
|
|
|
if (!verified || !registrationInfo) {
|
|
return c.text("Registration failed", 400);
|
|
}
|
|
|
|
const {
|
|
credentialID, credentialPublicKey,
|
|
counter, credentialDeviceType, credentialBackedUp,
|
|
} = registrationInfo;
|
|
|
|
// Base64URL encode ArrayBuffers.
|
|
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
|
|
|
|
const newPasskey: Passkey = {
|
|
id: credentialID,
|
|
publicKey: base64PublicKey,
|
|
counter,
|
|
deviceType: credentialDeviceType,
|
|
backedUp: credentialBackedUp,
|
|
transports: credential?.response?.transports,
|
|
};
|
|
|
|
// Store the credential ID in the database
|
|
const { success } = await c.env.DB.prepare(
|
|
`INSERT INTO user_passkeys (user_id, passkey_name, passkey_id, passkey, counter) VALUES (?, ?, ?, ?, ?)`
|
|
).bind(user.user_id, passkey_name, credentialID, JSON.stringify(newPasskey), counter).run();
|
|
|
|
return c.json({ success });
|
|
},
|
|
authenticateRequest: async (c: Context<HonoCustomType>) => {
|
|
const { domain } = await c.req.json();
|
|
const challenge = await Jwt.sign({
|
|
domain,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
}, c.env.JWT_SECRET, "HS256")
|
|
const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions({
|
|
rpID: domain,
|
|
challenge: challenge,
|
|
allowCredentials: [],
|
|
});
|
|
return c.json(options);
|
|
},
|
|
authenticateResponse: async (c: Context<HonoCustomType>) => {
|
|
const { domain, credential, origin } = await c.req.json();
|
|
const passkey_id = credential?.id;
|
|
if (!passkey_id) {
|
|
return c.text("Invalid request", 400);
|
|
}
|
|
const { user_id, counter, passkey } = await c.env.DB.prepare(
|
|
`SELECT user_id, counter, passkey FROM user_passkeys WHERE passkey_id = ?`
|
|
).bind(passkey_id).first<{
|
|
counter: number; passkey: string; user_id: number;
|
|
}>() || {};
|
|
if (!passkey) {
|
|
return c.text("Passkey not found", 404);
|
|
}
|
|
const passkeyData = JSON.parse(passkey) as Passkey;
|
|
// Verify the registration response
|
|
const verification = await verifyAuthenticationResponse({
|
|
response: credential,
|
|
expectedChallenge: async (challenge: string) => {
|
|
const payload = await Jwt.verify(atob(challenge), c.env.JWT_SECRET, "HS256");
|
|
if (!payload || !payload.iat) return false;
|
|
// check iad is not older than 5 minutes
|
|
if (Math.floor(Date.now() / 1000) - payload.iat > 300) return false;
|
|
return true;
|
|
},
|
|
expectedOrigin: origin,
|
|
expectedRPID: domain,
|
|
authenticator: {
|
|
credentialID: passkeyData.id,
|
|
credentialPublicKey: isoBase64URL.toBuffer(passkeyData.publicKey),
|
|
counter: counter || passkeyData.counter,
|
|
transports: passkeyData.transports,
|
|
},
|
|
});
|
|
const { verified, authenticationInfo } = verification;
|
|
if (!verified) {
|
|
return c.text("Authentication failed", 400);
|
|
}
|
|
|
|
if (authenticationInfo) {
|
|
const { newCounter } = authenticationInfo;
|
|
// Update the counter in the database
|
|
await c.env.DB.prepare(
|
|
`UPDATE user_passkeys SET counter = ? WHERE passkey_id = ?`
|
|
).bind(newCounter, passkey_id).run();
|
|
}
|
|
// update passkey updated_at
|
|
await c.env.DB.prepare(
|
|
`UPDATE user_passkeys SET updated_at = datetime('now') WHERE passkey_id = ?`
|
|
).bind(passkey_id).run();
|
|
|
|
// return jwt
|
|
const { user_email } = await c.env.DB.prepare(
|
|
`SELECT user_email FROM users WHERE id = ?`
|
|
).bind(user_id).first<{ user_email: string }>() || {};
|
|
if (!user_email) {
|
|
return c.text("User not found", 404);
|
|
}
|
|
// create jwt
|
|
const jwt = await Jwt.sign({
|
|
user_email: user_email,
|
|
user_id: user_id,
|
|
// 30 days expire in seconds
|
|
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
|
iat: Math.floor(Date.now() / 1000),
|
|
}, c.env.JWT_SECRET, "HS256")
|
|
return c.json({
|
|
jwt: jwt
|
|
})
|
|
},
|
|
}
|