/* Spacebar: A FOSS re-implementation and extension of the Discord.com backend. Copyright (C) 2023 Spacebar and Spacebar Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { route } from "@spacebar/api"; import { generateToken, SecurityKey, User, verifyWebAuthnToken, WebAuthn } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { ExpectedAssertionResult } from "fido2-lib"; import { HTTPError } from "lambert-server"; import { WebAuthnTotpSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); function toArrayBuffer(buf: Buffer) { const ab = new ArrayBuffer(buf.length); const view = new Uint8Array(ab); for (let i = 0; i < buf.length; ++i) { view[i] = buf[i]; } return ab; } router.post( "/", route({ requestBody: "WebAuthnTotpSchema", responses: { 200: { body: "TokenResponse" }, 400: { body: "APIErrorResponse" }, }, spacebarOnly: false, // not part of public openapi }), async (req: Request, res: Response) => { if (!WebAuthn.fido2) { // TODO: I did this for typescript and I can't use ! throw new Error("WebAuthn not enabled"); } const { code, ticket } = req.body as WebAuthnTotpSchema; const user = await User.findOneOrFail({ where: { totp_last_ticket: ticket, }, select: { id: true }, relations: { settings: true }, }); const ret = await verifyWebAuthnToken(ticket); if (!ret) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); await User.update({ id: user.id }, { totp_last_ticket: "" }); const clientAttestationResponse = JSON.parse(code); if (!clientAttestationResponse.rawId) throw new HTTPError("Missing rawId", 400); clientAttestationResponse.rawId = toArrayBuffer(Buffer.from(clientAttestationResponse.rawId, "base64url")); const securityKey = await SecurityKey.findOneOrFail({ where: { key_id: Buffer.from(clientAttestationResponse.rawId, "base64url").toString("base64"), }, }); const assertionExpectations: ExpectedAssertionResult = JSON.parse(Buffer.from(clientAttestationResponse.response.clientDataJSON, "base64").toString()); const authnResult = await WebAuthn.fido2.assertionResult(clientAttestationResponse, { ...assertionExpectations, factor: "second", publicKey: securityKey.public_key, prevCounter: securityKey.counter, userHandle: securityKey.key_id, }); const counter = authnResult.authnrData.get("counter"); securityKey.counter = counter; await securityKey.save(); return res.json({ token: await generateToken(user.id), user_settings: user.settings, }); }, ); export default router;