summary refs log tree commit diff
path: root/src/api/routes/users/@me/mfa
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-12-17 11:00:51 +0100
committerRory& <root@rory.gay>2025-12-17 11:01:27 +0100
commit491c0de845a886954262e3ddb24959ba37a014b6 (patch)
treec0251f3779a5cd2842320e1278232b48fbea0390 /src/api/routes/users/@me/mfa
parentPre-commit: use spaces for formatting, regenerate schemas if changed (diff)
downloadserver-ts-491c0de845a886954262e3ddb24959ba37a014b6.tar.xz
lintstagedrc: regenerate schemas/openapi if schemas changed
Diffstat (limited to 'src/api/routes/users/@me/mfa')
-rw-r--r--src/api/routes/users/@me/mfa/codes-verification.ts82
-rw-r--r--src/api/routes/users/@me/mfa/codes.ts100
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts80
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts82
-rw-r--r--src/api/routes/users/@me/mfa/webauthn/credentials/#key_id/index.ts38
-rw-r--r--src/api/routes/users/@me/mfa/webauthn/credentials/index.ts208
6 files changed, 295 insertions, 295 deletions
diff --git a/src/api/routes/users/@me/mfa/codes-verification.ts b/src/api/routes/users/@me/mfa/codes-verification.ts

index c8995d83..4cc4c072 100644 --- a/src/api/routes/users/@me/mfa/codes-verification.ts +++ b/src/api/routes/users/@me/mfa/codes-verification.ts
@@ -24,52 +24,52 @@ import { CodesVerificationSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); router.post( - "/", - route({ - requestBody: "CodesVerificationSchema", - responses: { - 200: { - body: "APIBackupCodeArray", - }, - 400: { - body: "APIErrorResponse", - }, - 404: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - // const { key, nonce, regenerate } = req.body as CodesVerificationSchema; - const { regenerate } = req.body as CodesVerificationSchema; + "/", + route({ + requestBody: "CodesVerificationSchema", + responses: { + 200: { + body: "APIBackupCodeArray", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + // const { key, nonce, regenerate } = req.body as CodesVerificationSchema; + const { regenerate } = req.body as CodesVerificationSchema; - // TODO: We don't have email/etc etc, so can't send a verification code. - // Once that's done, this route can verify `key` + // TODO: We don't have email/etc etc, so can't send a verification code. + // Once that's done, this route can verify `key` - // const user = await User.findOneOrFail({ where: { id: req.user_id } }); - if ((await User.count({ where: { id: req.user_id } })) === 0) throw DiscordApiErrors.UNKNOWN_USER; + // const user = await User.findOneOrFail({ where: { id: req.user_id } }); + if ((await User.count({ where: { id: req.user_id } })) === 0) throw DiscordApiErrors.UNKNOWN_USER; - let codes: BackupCode[]; - if (regenerate) { - await BackupCode.update({ user: { id: req.user_id } }, { expired: true }); + let codes: BackupCode[]; + if (regenerate) { + await BackupCode.update({ user: { id: req.user_id } }, { expired: true }); - codes = generateMfaBackupCodes(req.user_id); - await Promise.all(codes.map((x) => x.save())); - } else { - codes = await BackupCode.find({ - where: { - user: { - id: req.user_id, - }, - expired: false, - }, - }); - } + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map((x) => x.save())); + } else { + codes = await BackupCode.find({ + where: { + user: { + id: req.user_id, + }, + expired: false, + }, + }); + } - return res.json({ - backup_codes: codes.map((x) => ({ ...x, expired: undefined })), - }); - }, + return res.json({ + backup_codes: codes.map((x) => ({ ...x, expired: undefined })), + }); + }, ); export default router; diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts
index 579a106e..2eb1a3e3 100644 --- a/src/api/routes/users/@me/mfa/codes.ts +++ b/src/api/routes/users/@me/mfa/codes.ts
@@ -27,61 +27,61 @@ const router = Router({ mergeParams: true }); // TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients router.post( - "/", - route({ - requestBody: "MfaCodesSchema", - deprecated: true, - description: "This route is replaced with users/@me/mfa/codes-verification in newer clients", - responses: { - 200: { - body: "APIBackupCodeArray", - }, - 400: { - body: "APIErrorResponse", - }, - 404: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - const { password, regenerate } = req.body as MfaCodesSchema; + "/", + route({ + requestBody: "MfaCodesSchema", + deprecated: true, + description: "This route is replaced with users/@me/mfa/codes-verification in newer clients", + responses: { + 200: { + body: "APIBackupCodeArray", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const { password, regenerate } = req.body as MfaCodesSchema; - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["data"], - }); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data"], + }); - if (!(await bcrypt.compare(password, user.data.hash || ""))) { - throw FieldErrors({ - password: { - message: req.t("auth:login.INVALID_PASSWORD"), - code: "INVALID_PASSWORD", - }, - }); - } + if (!(await bcrypt.compare(password, user.data.hash || ""))) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } - let codes: BackupCode[]; - if (regenerate) { - await BackupCode.update({ user: { id: req.user_id } }, { expired: true }); + let codes: BackupCode[]; + if (regenerate) { + await BackupCode.update({ user: { id: req.user_id } }, { expired: true }); - codes = generateMfaBackupCodes(req.user_id); - await Promise.all(codes.map((x) => x.save())); - } else { - codes = await BackupCode.find({ - where: { - user: { - id: req.user_id, - }, - expired: false, - }, - }); - } + codes = generateMfaBackupCodes(req.user_id); + await Promise.all(codes.map((x) => x.save())); + } else { + codes = await BackupCode.find({ + where: { + user: { + id: req.user_id, + }, + expired: false, + }, + }); + } - return res.json({ - backup_codes: codes.map((x) => ({ ...x, expired: undefined })), - }); - }, + return res.json({ + backup_codes: codes.map((x) => ({ ...x, expired: undefined })), + }); + }, ); export default router; diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts
index 003dcb74..11c927e4 100644 --- a/src/api/routes/users/@me/mfa/totp/disable.ts +++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -26,51 +26,51 @@ import { TotpDisableSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); router.post( - "/", - route({ - requestBody: "TotpDisableSchema", - responses: { - 200: { - body: "TokenOnlyResponse", - }, - 400: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - const body = req.body as TotpDisableSchema; + "/", + route({ + requestBody: "TotpDisableSchema", + responses: { + 200: { + body: "TokenOnlyResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const body = req.body as TotpDisableSchema; - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["totp_secret"], - }); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["totp_secret"], + }); - const backup = await BackupCode.findOne({ where: { code: body.code } }); - if (!backup) { - const ret = verifyToken(user.totp_secret || "", body.code); - if (!ret || ret.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - } + const backup = await BackupCode.findOne({ where: { code: body.code } }); + if (!backup) { + const ret = verifyToken(user.totp_secret || "", body.code); + if (!ret || ret.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + } - await User.update( - { id: req.user_id }, - { - mfa_enabled: false, - totp_secret: "", - }, - ); + await User.update( + { id: req.user_id }, + { + mfa_enabled: false, + totp_secret: "", + }, + ); - await BackupCode.update( - { user: { id: req.user_id } }, - { - expired: true, - }, - ); + await BackupCode.update( + { user: { id: req.user_id } }, + { + expired: true, + }, + ); - return res.json({ - token: await generateToken(user.id), - }); - }, + return res.json({ + token: await generateToken(user.id), + }); + }, ); export default router; diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts
index f0563e30..a400c628 100644 --- a/src/api/routes/users/@me/mfa/totp/enable.ts +++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -27,54 +27,54 @@ import { TotpEnableSchema } from "@spacebar/schemas"; const router = Router({ mergeParams: true }); router.post( - "/", - route({ - requestBody: "TotpEnableSchema", - responses: { - 200: { - body: "TokenWithBackupCodesResponse", - }, - 400: { - body: "APIErrorResponse", - }, - 404: { - body: "APIErrorResponse", - }, - }, - }), - async (req: Request, res: Response) => { - const body = req.body as TotpEnableSchema; + "/", + route({ + requestBody: "TotpEnableSchema", + responses: { + 200: { + body: "TokenWithBackupCodesResponse", + }, + 400: { + body: "APIErrorResponse", + }, + 404: { + body: "APIErrorResponse", + }, + }, + }), + async (req: Request, res: Response) => { + const body = req.body as TotpEnableSchema; - const user = await User.findOneOrFail({ - where: { id: req.user_id }, - select: ["data", "email"], - }); + const user = await User.findOneOrFail({ + where: { id: req.user_id }, + select: ["data", "email"], + }); - // TODO: Are guests allowed to enable 2fa? - if (user.data.hash) { - if (!(await bcrypt.compare(body.password, user.data.hash))) { - throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); - } - } + // TODO: Are guests allowed to enable 2fa? + if (user.data.hash) { + if (!(await bcrypt.compare(body.password, user.data.hash))) { + throw new HTTPError(req.t("auth:login.INVALID_PASSWORD")); + } + } - if (!body.secret) throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); + if (!body.secret) throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005); - if (!body.code) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + if (!body.code) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - if (verifyToken(body.secret, body.code)?.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); + if (verifyToken(body.secret, body.code)?.delta != 0) throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008); - const backup_codes = generateMfaBackupCodes(req.user_id); - await Promise.all(backup_codes.map((x) => x.save())); - await User.update({ id: req.user_id }, { mfa_enabled: true, totp_secret: body.secret }); + const backup_codes = generateMfaBackupCodes(req.user_id); + await Promise.all(backup_codes.map((x) => x.save())); + await User.update({ id: req.user_id }, { mfa_enabled: true, totp_secret: body.secret }); - res.send({ - token: await generateToken(user.id), - backup_codes: backup_codes.map((x) => ({ - ...x, - expired: undefined, - })), - }); - }, + res.send({ + token: await generateToken(user.id), + backup_codes: backup_codes.map((x) => ({ + ...x, + expired: undefined, + })), + }); + }, ); export default router; 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
index 6dc06563..77180cac 100644 --- 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
@@ -22,29 +22,29 @@ import { Request, Response, Router } from "express"; const router = Router({ mergeParams: true }); router.delete( - "/", - route({ - responses: { - 204: {}, - }, - }), - async (req: Request, res: Response) => { - const { key_id } = req.params; + "/", + route({ + responses: { + 204: {}, + }, + }), + async (req: Request, res: Response) => { + const { key_id } = req.params; - await SecurityKey.delete({ - id: key_id, - user_id: req.user_id, - }); + await SecurityKey.delete({ + id: key_id, + user_id: req.user_id, + }); - const keys = await SecurityKey.count({ - where: { user_id: req.user_id }, - }); + const keys = await SecurityKey.count({ + where: { user_id: req.user_id }, + }); - // disable webauthn if there are no keys left - if (keys === 0) await User.update({ id: req.user_id }, { webauthn_enabled: false }); + // disable webauthn if there are no keys left + if (keys === 0) await User.update({ id: req.user_id }, { webauthn_enabled: false }); - res.sendStatus(204); - }, + 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
index 0c4b733d..7bd29a57 100644 --- a/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts +++ b/src/api/routes/users/@me/mfa/webauthn/credentials/index.ts
@@ -26,138 +26,138 @@ import { CreateWebAuthnCredentialSchema, GenerateWebAuthnCredentialsSchema, WebA const router = Router({ mergeParams: true }); const isGenerateSchema = (body: WebAuthnPostSchema): body is GenerateWebAuthnCredentialsSchema => { - return "password" in body; + return "password" in body; }; const isCreateSchema = (body: WebAuthnPostSchema): body is CreateWebAuthnCredentialSchema => { - return "credential" in body; + 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; + 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, - }, - }); + const securityKeys = await SecurityKey.find({ + where: { + user_id: req.user_id, + }, + }); - return res.json( - securityKeys.map((key) => ({ - id: key.id, - name: key.name, - })), - ); + return res.json( + securityKeys.map((key) => ({ + id: key.id, + name: key.name, + })), + ); }); router.post( - "/", - route({ - requestBody: "WebAuthnPostSchema", - responses: { - 200: { - body: "WebAuthnCreateResponse", - }, - 400: { - body: "APIErrorResponse", - }, - }, - }), - 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"); - } + "/", + route({ + requestBody: "WebAuthnPostSchema", + responses: { + 200: { + body: "WebAuthnCreateResponse", + }, + 400: { + body: "APIErrorResponse", + }, + }, + }), + 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", "totp_secret", "mfa_enabled", "username"], - relations: ["settings"], - }); + const user = await User.findOneOrFail({ + where: { + id: req.user_id, + }, + select: ["data", "id", "disabled", "deleted", "totp_secret", "mfa_enabled", "username"], + relations: ["settings"], + }); - 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", - }, - }); - } + 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 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); + const ticket = await generateWebAuthnTicket(challenge); - return res.json({ - ticket: ticket, - challenge, - }); - } else if (isCreateSchema(req.body)) { - const { credential, name, ticket } = req.body; + 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 verified = await verifyWebAuthnToken(ticket); + if (!verified) throw new HTTPError("Invalid ticket", 400); - const clientAttestationResponse = JSON.parse(credential); + const clientAttestationResponse = JSON.parse(credential); - if (!clientAttestationResponse.rawId) throw new HTTPError("Missing rawId", 400); + if (!clientAttestationResponse.rawId) throw new HTTPError("Missing rawId", 400); - const rawIdBuffer = Buffer.from(clientAttestationResponse.rawId, "base64"); - clientAttestationResponse.rawId = toArrayBuffer(rawIdBuffer); + const rawIdBuffer = Buffer.from(clientAttestationResponse.rawId, "base64"); + clientAttestationResponse.rawId = toArrayBuffer(rawIdBuffer); - const attestationExpectations: ExpectedAttestationResult = JSON.parse(Buffer.from(clientAttestationResponse.response.clientDataJSON, "base64").toString()); + const attestationExpectations: ExpectedAttestationResult = JSON.parse(Buffer.from(clientAttestationResponse.response.clientDataJSON, "base64").toString()); - const regResult = await WebAuthn.fido2.attestationResult(clientAttestationResponse, { - ...attestationExpectations, - factor: "second", - }); + 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 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, - }); + const securityKey = SecurityKey.create({ + name, + counter, + public_key: publicKey, + user_id: req.user_id, + key_id: keyId, + }); - await Promise.all([securityKey.save(), User.update({ id: req.user_id }, { webauthn_enabled: true })]); + await Promise.all([securityKey.save(), User.update({ id: req.user_id }, { webauthn_enabled: true })]); - return res.json({ - name, - id: securityKey.id, - }); - } else { - throw DiscordApiErrors.INVALID_AUTHENTICATION_TOKEN; - } - }, + return res.json({ + name, + id: securityKey.id, + }); + } else { + throw DiscordApiErrors.INVALID_AUTHENTICATION_TOKEN; + } + }, ); export default router;