mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 23:47:50 +08:00
feat: add passkey (#384)
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@eslint/js": "8.56.0",
|
||||
"@simplewebauthn/types": "^10.0.0",
|
||||
"eslint": "8.56.0",
|
||||
"globals": "^15.8.0",
|
||||
"typescript-eslint": "^7.15.0",
|
||||
@@ -21,6 +22,7 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.609.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.609.0",
|
||||
"@simplewebauthn/server": "^10.0.1",
|
||||
"hono": "^4.4.12",
|
||||
"mimetext": "^3.0.24",
|
||||
"postal-mime": "^2.2.5",
|
||||
|
||||
Generated
+126
@@ -19,6 +19,9 @@ importers:
|
||||
'@aws-sdk/s3-request-presigner':
|
||||
specifier: ^3.609.0
|
||||
version: 3.609.0
|
||||
'@simplewebauthn/server':
|
||||
specifier: ^10.0.1
|
||||
version: 10.0.1
|
||||
hono:
|
||||
specifier: ^4.4.12
|
||||
version: 4.4.12
|
||||
@@ -41,6 +44,9 @@ importers:
|
||||
'@eslint/js':
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
'@simplewebauthn/types':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
eslint:
|
||||
specifier: 8.56.0
|
||||
version: 8.56.0
|
||||
@@ -444,6 +450,9 @@ packages:
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@hexagon/base64@1.1.28':
|
||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -471,6 +480,9 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@levischuck/tiny-cbor@0.2.2':
|
||||
resolution: {integrity: sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -486,6 +498,21 @@ packages:
|
||||
'@one-ini/wasm@0.1.1':
|
||||
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||
|
||||
'@peculiar/asn1-android@2.3.10':
|
||||
resolution: {integrity: sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==}
|
||||
|
||||
'@peculiar/asn1-ecc@2.3.8':
|
||||
resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==}
|
||||
|
||||
'@peculiar/asn1-rsa@2.3.8':
|
||||
resolution: {integrity: sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==}
|
||||
|
||||
'@peculiar/asn1-schema@2.3.8':
|
||||
resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==}
|
||||
|
||||
'@peculiar/asn1-x509@2.3.8':
|
||||
resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -497,6 +524,13 @@ packages:
|
||||
'@selderee/plugin-htmlparser2@0.11.0':
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
|
||||
'@simplewebauthn/server@10.0.1':
|
||||
resolution: {integrity: sha512-djNWcRn+H+6zvihBFJSpG3fzb0NQS9c/Mw5dYOtZ9H+oDw8qn9Htqxt4cpqRvSOAfwqP7rOvE9rwqVaoGGc3hg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@simplewebauthn/types@10.0.0':
|
||||
resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
|
||||
|
||||
'@smithy/abort-controller@3.1.1':
|
||||
resolution: {integrity: sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@@ -825,6 +859,10 @@ packages:
|
||||
as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
asn1js@3.0.5:
|
||||
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -900,6 +938,9 @@ packages:
|
||||
core-js-pure@3.37.1:
|
||||
resolution: {integrity: sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==}
|
||||
|
||||
cross-fetch@4.0.0:
|
||||
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1157,6 +1198,10 @@ packages:
|
||||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
ipaddr.js@2.2.0:
|
||||
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1418,6 +1463,13 @@ packages:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
pvtsutils@1.3.5:
|
||||
resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==}
|
||||
|
||||
pvutils@1.1.3:
|
||||
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -2344,6 +2396,8 @@ snapshots:
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
'@hexagon/base64@1.1.28': {}
|
||||
|
||||
'@humanwhocodes/config-array@0.11.14':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
@@ -2374,6 +2428,8 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
|
||||
'@levischuck/tiny-cbor@0.2.2': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -2388,6 +2444,40 @@ snapshots:
|
||||
|
||||
'@one-ini/wasm@0.1.1': {}
|
||||
|
||||
'@peculiar/asn1-android@2.3.10':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.8
|
||||
asn1js: 3.0.5
|
||||
tslib: 2.6.3
|
||||
|
||||
'@peculiar/asn1-ecc@2.3.8':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.8
|
||||
'@peculiar/asn1-x509': 2.3.8
|
||||
asn1js: 3.0.5
|
||||
tslib: 2.6.3
|
||||
|
||||
'@peculiar/asn1-rsa@2.3.8':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.8
|
||||
'@peculiar/asn1-x509': 2.3.8
|
||||
asn1js: 3.0.5
|
||||
tslib: 2.6.3
|
||||
|
||||
'@peculiar/asn1-schema@2.3.8':
|
||||
dependencies:
|
||||
asn1js: 3.0.5
|
||||
pvtsutils: 1.3.5
|
||||
tslib: 2.6.3
|
||||
|
||||
'@peculiar/asn1-x509@2.3.8':
|
||||
dependencies:
|
||||
'@peculiar/asn1-schema': 2.3.8
|
||||
asn1js: 3.0.5
|
||||
ipaddr.js: 2.2.0
|
||||
pvtsutils: 1.3.5
|
||||
tslib: 2.6.3
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
@@ -2404,6 +2494,22 @@ snapshots:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
|
||||
'@simplewebauthn/server@10.0.1':
|
||||
dependencies:
|
||||
'@hexagon/base64': 1.1.28
|
||||
'@levischuck/tiny-cbor': 0.2.2
|
||||
'@peculiar/asn1-android': 2.3.10
|
||||
'@peculiar/asn1-ecc': 2.3.8
|
||||
'@peculiar/asn1-rsa': 2.3.8
|
||||
'@peculiar/asn1-schema': 2.3.8
|
||||
'@peculiar/asn1-x509': 2.3.8
|
||||
'@simplewebauthn/types': 10.0.0
|
||||
cross-fetch: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
'@simplewebauthn/types@10.0.0': {}
|
||||
|
||||
'@smithy/abort-controller@3.1.1':
|
||||
dependencies:
|
||||
'@smithy/types': 3.3.0
|
||||
@@ -2871,6 +2977,12 @@ snapshots:
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
|
||||
asn1js@3.0.5:
|
||||
dependencies:
|
||||
pvtsutils: 1.3.5
|
||||
pvutils: 1.1.3
|
||||
tslib: 2.6.3
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
@@ -2948,6 +3060,12 @@ snapshots:
|
||||
|
||||
core-js-pure@3.37.1: {}
|
||||
|
||||
cross-fetch@4.0.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -3258,6 +3376,8 @@ snapshots:
|
||||
|
||||
ini@1.3.8: {}
|
||||
|
||||
ipaddr.js@2.2.0: {}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
dependencies:
|
||||
binary-extensions: 2.3.0
|
||||
@@ -3483,6 +3603,12 @@ snapshots:
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
pvtsutils@1.3.5:
|
||||
dependencies:
|
||||
tslib: 2.6.3
|
||||
|
||||
pvutils@1.1.3: {}
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
react-dom@18.3.1(react@18.3.1):
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
import type {
|
||||
AuthenticatorTransportFuture,
|
||||
CredentialDeviceType,
|
||||
Base64URLString,
|
||||
} from '@simplewebauthn/types';
|
||||
|
||||
export type Passkey = {
|
||||
id: Base64URLString;
|
||||
publicKey: string;
|
||||
counter: number;
|
||||
deviceType: CredentialDeviceType;
|
||||
backedUp: boolean;
|
||||
transports?: AuthenticatorTransportFuture[];
|
||||
};
|
||||
|
||||
export class AdminWebhookSettings {
|
||||
allowList: string[];
|
||||
|
||||
|
||||
@@ -4,15 +4,30 @@ import { HonoCustomType } from '../types';
|
||||
import settings from './settings';
|
||||
import user from './user';
|
||||
import bind_address from './bind_address';
|
||||
import passkey from './passkey';
|
||||
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
|
||||
// settings api
|
||||
api.get('/user_api/open_settings', settings.openSettings);
|
||||
api.get('/user_api/settings', settings.settings);
|
||||
|
||||
// user api
|
||||
api.post('/user_api/login', user.login);
|
||||
api.post('/user_api/verify_code', user.verifyCode);
|
||||
api.post('/user_api/register', user.register);
|
||||
|
||||
// bind address api
|
||||
api.get('/user_api/bind_address', bind_address.getBindedAddresses);
|
||||
api.post('/user_api/bind_address', bind_address.bind);
|
||||
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
|
||||
api.post('/user_api/unbind_address', bind_address.unbind);
|
||||
|
||||
// passkey api
|
||||
api.get('/user_api/passkey', passkey.getPassKeys);
|
||||
api.post('/user_api/passkey/rename', passkey.renamePassKey);
|
||||
api.delete('/user_api/passkey/:passkey_id', passkey.deletePassKey);
|
||||
api.post('/user_api/passkey/register_request', passkey.registerRequest);
|
||||
api.post('/user_api/passkey/register_response', passkey.registerResponse);
|
||||
api.post('/user_api/passkey/authenticate_request', passkey.authenticateRequest);
|
||||
api.post('/user_api/passkey/authenticate_response', passkey.authenticateResponse);
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
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
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -59,7 +59,6 @@ export const getBooleanValue = (
|
||||
if (typeof value === "string") {
|
||||
return value === "true";
|
||||
}
|
||||
console.error(`Failed to parse boolean value: ${value}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ app.use('/user_api/*', async (c, next) => {
|
||||
|| c.req.path.startsWith("/user_api/register")
|
||||
|| c.req.path.startsWith("/user_api/login")
|
||||
|| c.req.path.startsWith("/user_api/verify_code")
|
||||
|| c.req.path.startsWith("/user_api/passkey/authenticate_")
|
||||
) {
|
||||
await next();
|
||||
return;
|
||||
|
||||
@@ -54,11 +54,12 @@ ENABLE_AUTO_REPLY = false
|
||||
# DISABLE_SHOW_GITHUB = true
|
||||
# default send balance, if not set, it will be 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# NO_LIMIT_SEND_ROLE = "vip" # the role which can send emails without limit
|
||||
# Turnstile verification
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# telegram bot
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# TG_MAX_ADDRESS = 5
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user