import { verifyAuthentication, decodeUserHandle } from '../../../utils/webauthnHelper' import { consumeChallenge } from '../../../utils/webauthnChallengeStore' import { readCredentials, writeCredentials, fetchUserName } from '../../../utils/adUserCredentials' import { decryptPassword } from '../../../utils/passwordCipher' export default defineEventHandler(async (event) => { const body = await readBody(event) const response = body?.response if (!response?.response?.clientDataJSON) { throw createError({ statusCode: 400, statusMessage: 'Missing authentication response' }) } // 1. Decode + consume challenge (signed cookie issued by login-options) const clientDataStr = Buffer.from(response.response.clientDataJSON, 'base64url').toString('utf8') const clientData = JSON.parse(clientDataStr) const challenge: string = clientData.challenge const entry = consumeChallenge(event, 'login', challenge) if (!entry) { throw createError({ statusCode: 400, statusMessage: 'Challenge expired or invalid' }) } // 2. Resolve user from userHandle (we encoded ad_user.id as the userHandle // during registration, so discoverable-credential responses give it back) const userId = decodeUserHandle(response.response?.userHandle) if (!userId) { throw createError({ statusCode: 400, statusMessage: 'No user handle in response' }) } // 3. Load credentials for this user (uses the iDempiere service token) const stored = await readCredentials(event, userId).catch(() => ({ keys: [], password: null })) const matching = stored.keys.find(k => k.credentialId === response.id) if (!matching) { throw createError({ statusCode: 400, statusMessage: 'Credential not registered' }) } if (!stored.password) { throw createError({ statusCode: 400, statusMessage: 'No stored credentials — please re-register your security key' }) } // 4. Verify the assertion const verification = await verifyAuthentication({ response, expectedChallenge: challenge, credential: { id: matching.credentialId, publicKey: new Uint8Array(Buffer.from(matching.publicKey, 'base64')), counter: matching.counter, transports: matching.transports as any, }, }).catch((e: any) => { throw createError({ statusCode: 400, statusMessage: 'Verification failed: ' + (e?.message ?? 'unknown error') }) }) if (!verification.verified) { throw createError({ statusCode: 400, statusMessage: 'Verification failed' }) } // 5. Update counter matching.counter = verification.authenticationInfo.newCounter await writeCredentials(event, userId, stored).catch(() => null) // 6. Decrypt the iDempiere password let password: string try { password = decryptPassword(stored.password) } catch (e: any) { throw createError({ statusCode: 500, statusMessage: 'Could not decrypt stored credentials: ' + (e?.message ?? 'unknown') }) } // 7. Resolve username (the login id iDempiere expects) const userName = await fetchUserName(event, userId) if (!userName) { throw createError({ statusCode: 500, statusMessage: 'Could not resolve username for user ' + userId }) } // 8. Delegate to the EXISTING /api/idempiere-auth/login endpoint via internal // $fetch.raw. login.post.ts is left 100% untouched — it does the auth // call, sets all cookies, runs auto-finalize. We forward its Set-Cookie // headers to our outer response so the browser sees them. const isPwa = !!body?.pwa const isMobileWorker = !!body?.mobileWorker || getCookie(event, 'logship_mw') === '1' const loginRes = await $fetch.raw('/api/idempiere-auth/login', { method: 'POST', headers: { // forward existing cookies so client_id etc. may be available cookie: getHeader(event, 'cookie') ?? '', }, body: { userName, password, remember: false, selectRole: false, mobileWorker: isMobileWorker, pwa: isPwa, }, }).catch((e: any) => { throw createError({ statusCode: e?.statusCode ?? e?.status ?? 500, statusMessage: e?.statusMessage ?? e?.message ?? 'Login failed', }) }) // Forward Set-Cookie headers from the internal login response to our caller. // Without this, the browser never sees logship_it / logship_user / etc. const setCookies: string[] = (loginRes.headers as any).getSetCookie?.() ?? (loginRes.headers as any).raw?.()['set-cookie'] ?? [] for (const c of setCookies) { appendResponseHeader(event, 'set-cookie', c) } return loginRes._data })