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
105 lines
2.5 KiB
TypeScript
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', ' ')}`
|
|
}
|