import { verifyRegistration } from '../../../utils/webauthnHelper' import { consumeChallenge } from '../../../utils/webauthnChallengeStore' import { readCredentials, writeCredentials, type WebAuthnKeyEntry } from '../../../utils/adUserCredentials' import { encryptPassword } from '../../../utils/passwordCipher' import fetchHelper from '../../../utils/fetchHelper' export default defineEventHandler(async (event) => { const userIdRaw = getCookie(event, 'logship_user_id') const token = getCookie(event, 'logship_it') const userId = Number(userIdRaw) if (!userId || !token) { throw createError({ statusCode: 401, statusMessage: 'Not authenticated' }) } const body = await readBody(event) const response = body?.response const nickname = String(body?.nickname ?? 'Security Key').slice(0, 64) || 'Security Key' if (!response?.response?.clientDataJSON) { throw createError({ statusCode: 400, statusMessage: 'Missing registration response' }) } // Decode challenge from clientDataJSON const clientDataStr = Buffer.from(response.response.clientDataJSON, 'base64url').toString('utf8') const clientData = JSON.parse(clientDataStr) const challenge: string = clientData.challenge const entry = consumeChallenge(event, 'register', challenge) if (!entry || entry.userId !== userId) { throw createError({ statusCode: 400, statusMessage: 'Challenge expired or invalid' }) } const verification = await verifyRegistration({ response, expectedChallenge: challenge }) if (!verification.verified || !verification.registrationInfo) { throw createError({ statusCode: 400, statusMessage: 'Registration verification failed' }) } const info = verification.registrationInfo const cred = info.credential const newKey: WebAuthnKeyEntry = { credentialId: cred.id, publicKey: Buffer.from(cred.publicKey).toString('base64'), counter: cred.counter, transports: cred.transports as string[] | undefined, nickname, createdAt: Date.now(), deviceType: info.credentialDeviceType, backedUp: info.credentialBackedUp, } const existing = await readCredentials(event, userId, token) // Capture the iDempiere password from ad_user via REST (using the user's own // token, which is authorized to read their own record). The Password column // is returned in plaintext by iDempiere REST, which is exactly what we need // to feed back into auth/tokens during a future passkey login. let password = existing.password if (!password) { try { const rec: any = await fetchHelper(event, 'models/ad_user/' + userId + '?$select=password', 'GET', token, null) const plain: string | undefined = rec?.Password ?? rec?.password if (plain) password = encryptPassword(plain) } catch { // leave password null; registration will fail loudly below } } if (!password) { throw createError({ statusCode: 400, statusMessage: 'Could not capture password from ad_user — passkey not enrolled', }) } const payload = { keys: [...existing.keys.filter(k => k.credentialId !== newKey.credentialId), newKey], password, } await writeCredentials(event, userId, payload, token) return { status: 200, credentialId: newKey.credentialId, nickname: newKey.nickname, passwordCaptured: !!password, } })