diff options
author | Puyodead1 <puyodead@proton.me> | 2023-01-29 21:30:42 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-30 13:30:42 +1100 |
commit | 709dc7280e8b4aab2b173c3897b418f6e1759ae7 (patch) | |
tree | 5a8ed3e144a8032649d1c2f7c72f9c0c01e7742c /src/api/routes/users/@me | |
parent | Merge branch 'master' of github.com:fosscord/fosscord-server (diff) | |
download | server-709dc7280e8b4aab2b173c3897b418f6e1759ae7.tar.xz |
Implement WebAuthn (#967)
* implement webauthn * code review --------- Co-authored-by: Madeline <46743919+MaddyUnderStars@users.noreply.github.com>
Diffstat (limited to 'src/api/routes/users/@me')
-rw-r--r-- | src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts | 35 | ||||
-rw-r--r-- | src/api/routes/users/@me/mfa/webauthn/credentials/index.ts | 196 |
2 files changed, 231 insertions, 0 deletions
diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts new file mode 100644 index 00000000..c451e357 --- /dev/null +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts @@ -0,0 +1,35 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord 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 <https://www.gnu.org/licenses/>. +*/ + +import { route } from "@fosscord/api"; +import { SecurityKey } from "@fosscord/util"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.delete("/", route({}), async (req: Request, res: Response) => { + const { key_id } = req.params; + + await SecurityKey.delete({ + id: key_id, + user_id: req.user_id, + }); + + res.sendStatus(204); +}); + +export default router; diff --git a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts new file mode 100644 index 00000000..581950b8 --- /dev/null +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts @@ -0,0 +1,196 @@ +/* + Fosscord: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Fosscord and Fosscord 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 <https://www.gnu.org/licenses/>. +*/ + +import { route } from "@fosscord/api"; +import { + CreateWebAuthnCredentialSchema, + DiscordApiErrors, + FieldErrors, + GenerateWebAuthnCredentialsSchema, + generateWebAuthnTicket, + SecurityKey, + User, + verifyWebAuthnToken, + WebAuthn, + WebAuthnPostSchema, +} from "@fosscord/util"; +import bcrypt from "bcrypt"; +import { Request, Response, Router } from "express"; +import { ExpectedAttestationResult } from "fido2-lib"; +import { HTTPError } from "lambert-server"; +const router = Router(); + +const isGenerateSchema = ( + body: WebAuthnPostSchema, +): body is GenerateWebAuthnCredentialsSchema => { + return "password" in body; +}; + +const isCreateSchema = ( + body: WebAuthnPostSchema, +): body is CreateWebAuthnCredentialSchema => { + return "credential" in body; +}; + +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.get("/", route({}), async (req: Request, res: Response) => { + const securityKeys = await SecurityKey.find({ + where: { + user_id: req.user_id, + }, + }); + + return res.json( + securityKeys.map((key) => ({ + id: key.id, + name: key.name, + })), + ); +}); + +router.post( + "/", + route({ body: "WebAuthnPostSchema" }), + 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 user = await User.findOneOrFail({ + where: { + id: req.user_id, + }, + select: [ + "data", + "id", + "disabled", + "deleted", + "settings", + "totp_secret", + "mfa_enabled", + "username", + ], + }); + + if (isGenerateSchema(req.body)) { + const { password } = req.body; + const same_password = await bcrypt.compare( + password, + user.data.hash || "", + ); + if (!same_password) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } + + const registrationOptions = + await WebAuthn.fido2.attestationOptions(); + const challenge = JSON.stringify({ + publicKey: { + ...registrationOptions, + challenge: Buffer.from( + registrationOptions.challenge, + ).toString("base64"), + user: { + id: user.id, + name: user.username, + displayName: user.username, + }, + }, + }); + + const ticket = await generateWebAuthnTicket(challenge); + + return res.json({ + ticket: ticket, + challenge, + }); + } else if (isCreateSchema(req.body)) { + const { credential, name, ticket } = req.body; + + const verified = await verifyWebAuthnToken(ticket); + if (!verified) throw new HTTPError("Invalid ticket", 400); + + const clientAttestationResponse = JSON.parse(credential); + + if (!clientAttestationResponse.rawId) + throw new HTTPError("Missing rawId", 400); + + const rawIdBuffer = Buffer.from( + clientAttestationResponse.rawId, + "base64", + ); + clientAttestationResponse.rawId = toArrayBuffer(rawIdBuffer); + + const attestationExpectations: ExpectedAttestationResult = + JSON.parse( + Buffer.from( + clientAttestationResponse.response.clientDataJSON, + "base64", + ).toString(), + ); + + const regResult = await WebAuthn.fido2.attestationResult( + clientAttestationResponse, + { + ...attestationExpectations, + factor: "second", + }, + ); + + const authnrData = regResult.authnrData; + const keyId = Buffer.from(authnrData.get("credId")).toString( + "base64", + ); + const counter = authnrData.get("counter"); + const publicKey = authnrData.get("credentialPublicKeyPem"); + + const securityKey = SecurityKey.create({ + name, + counter, + public_key: publicKey, + user_id: req.user_id, + key_id: keyId, + }); + + await securityKey.save(); + + return res.json({ + name, + id: securityKey.id, + }); + } else { + throw DiscordApiErrors.INVALID_AUTHENTICATION_TOKEN; + } + }, +); + +export default router; |