-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 2fa backup codes #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: enhance-two-factor-security-foundation
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import React from "react"; | ||
| import { useFormContext } from "react-hook-form"; | ||
|
|
||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import { Label, TextField } from "@calcom/ui"; | ||
|
|
||
| export default function TwoFactor({ center = true }) { | ||
| const { t } = useLocale(); | ||
| const methods = useFormContext(); | ||
|
|
||
| return ( | ||
| <div className={center ? "mx-auto !mt-0 max-w-sm" : "!mt-0 max-w-sm"}> | ||
| <Label className="mt-4">{t("backup_code")}</Label> | ||
|
|
||
| <p className="text-subtle mb-4 text-sm">{t("backup_code_instructions")}</p> | ||
|
|
||
| <TextField | ||
| id="backup-code" | ||
| label="" | ||
| defaultValue="" | ||
| placeholder="XXXXX-XXXXX" | ||
| minLength={10} // without dash | ||
| maxLength={11} // with dash | ||
| required | ||
| {...methods.register("backupCode")} | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -43,8 +43,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||||
| return res.status(400).json({ error: ErrorCode.IncorrectPassword }); | ||||||
| } | ||||||
| } | ||||||
| // if user has 2fa | ||||||
| if (user.twoFactorEnabled) { | ||||||
|
|
||||||
| // if user has 2fa and using backup code | ||||||
| if (user.twoFactorEnabled && req.body.backupCode) { | ||||||
| if (!process.env.CALENDSO_ENCRYPTION_KEY) { | ||||||
| console.error("Missing encryption key; cannot proceed with backup code login."); | ||||||
| throw new Error(ErrorCode.InternalServerError); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔷 Medium: This API mixes returned JSON errors with thrown errors; throwing here can produce an unstructured 500 response and bypass the established error body shape. Use a consistent JSON response (e.g., res.status(500).json({ error })) and avoid throwing to keep clients predictable. Apply the same approach to other throws in this handler (missing secret/encryption key/decrypt length).
Suggested change
|
||||||
| } | ||||||
|
|
||||||
| if (!user.backupCodes) { | ||||||
| return res.status(400).json({ error: ErrorCode.MissingBackupCodes }); | ||||||
| } | ||||||
|
|
||||||
| const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, process.env.CALENDSO_ENCRYPTION_KEY)); | ||||||
|
|
||||||
| // check if user-supplied code matches one | ||||||
| const index = backupCodes.indexOf(req.body.backupCode.replaceAll("-", "")); | ||||||
| if (index === -1) { | ||||||
| return res.status(400).json({ error: ErrorCode.IncorrectBackupCode }); | ||||||
| } | ||||||
|
|
||||||
| // we delete all stored backup codes at the end, no need to do this here | ||||||
|
|
||||||
| // if user has 2fa and NOT using backup code, try totp | ||||||
| } else if (user.twoFactorEnabled) { | ||||||
| if (!req.body.code) { | ||||||
| return res.status(400).json({ error: ErrorCode.SecondFactorRequired }); | ||||||
| // throw new Error(ErrorCode.SecondFactorRequired); | ||||||
|
|
@@ -82,6 +104,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) | |||||
| id: session.user.id, | ||||||
| }, | ||||||
| data: { | ||||||
| backupCodes: null, | ||||||
| twoFactorEnabled: false, | ||||||
| twoFactorSecret: null, | ||||||
| }, | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Low: Blob URLs created for backup code download are revoked only when replaced, not when closing the modal. Revoke on reset to prevent object URL leaks across open/close cycles.