Files
dticket.tootaio.com/server/utils/webauthn.ts
xiaomai 377a9617be feat: implement auth system, passkeys, and user management
Add PostgreSQL and Redis integration for users and sessions
Implement password and WebAuthn passkey login flows
Add Docker stack, super-admin seeding, and protected routes
2026-04-12 20:16:43 +08:00

105 lines
2.5 KiB
TypeScript

import type { H3Event } from 'h3'
import type { AuthenticatorTransportFuture, WebAuthnCredential } from '@simplewebauthn/server'
import { getRequestURL } from 'h3'
import { decodeBase64Url, randomToken } from './base64url'
import { getRedisClient } from './redis'
import type { PasskeyRecord } from './user-repository'
const CHALLENGE_TTL_SECONDS = 60 * 5
function getAppOrigin(event: H3Event) {
const config = useRuntimeConfig()
if (config.public.appUrl) {
return new URL(config.public.appUrl).origin
}
const url = getRequestURL(event)
return `${url.protocol}//${url.host}`
}
export function getWebAuthnConfig(event: H3Event) {
const config = useRuntimeConfig()
const origin = getAppOrigin(event)
const originUrl = new URL(origin)
return {
origin,
rpID: originUrl.hostname,
rpName: config.public.rpName || 'Dinner Ticket System'
}
}
function getRegistrationChallengeKey(userId: string) {
return `webauthn:register:${userId}`
}
function getLoginChallengeKey(token: string) {
return `webauthn:login:${token}`
}
export async function storeRegistrationChallenge(userId: string, challenge: string) {
const redis = await getRedisClient()
await redis.set(getRegistrationChallengeKey(userId), challenge, {
expiration: {
type: 'EX',
value: CHALLENGE_TTL_SECONDS
}
})
}
export async function consumeRegistrationChallenge(userId: string) {
const redis = await getRedisClient()
const key = getRegistrationChallengeKey(userId)
const challenge = await redis.get(key)
if (challenge) {
await redis.del(key)
}
return challenge
}
export async function createLoginChallenge(challenge: string) {
const token = randomToken(24)
const redis = await getRedisClient()
await redis.set(getLoginChallengeKey(token), challenge, {
expiration: {
type: 'EX',
value: CHALLENGE_TTL_SECONDS
}
})
return token
}
export async function consumeLoginChallenge(token: string) {
const redis = await getRedisClient()
const key = getLoginChallengeKey(token)
const challenge = await redis.get(key)
if (challenge) {
await redis.del(key)
}
return challenge
}
export function toWebAuthnCredential(passkey: PasskeyRecord): WebAuthnCredential {
return {
id: passkey.credentialId,
publicKey: decodeBase64Url(passkey.publicKey),
counter: passkey.counter,
transports: passkey.transports as AuthenticatorTransportFuture[]
}
}
export function buildPasskeyLabel() {
return `Passkey ${new Date().toISOString().slice(0, 16).replace('T', ' ')}`
}