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
This commit is contained in:
104
server/utils/webauthn.ts
Normal file
104
server/utils/webauthn.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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', ' ')}`
|
||||
}
|
||||
Reference in New Issue
Block a user