From b39f8bf1e89cfda09355b0ab9c72b34e83036683 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Fri, 6 Mar 2026 16:29:31 -0800 Subject: [PATCH] Magic OTP: accept 123456 alongside real codes (for App Store review) --- api/auth/loginOTP.cfm | 6 +--- api/auth/sendLoginOTP.cfm | 7 +---- api/auth/sendOTP.cfm | 4 --- api/auth/verifyEmailOTP.cfm | 56 ++++++++++++++++++++----------------- api/auth/verifyLoginOTP.cfm | 26 ++++++++--------- api/auth/verifyOTP.cfm | 25 ++++++++--------- 6 files changed, 55 insertions(+), 69 deletions(-) diff --git a/api/auth/loginOTP.cfm b/api/auth/loginOTP.cfm index 0ddf529..21eada2 100644 --- a/api/auth/loginOTP.cfm +++ b/api/auth/loginOTP.cfm @@ -76,12 +76,8 @@ try { }, { datasource: "payfrit" }); } - // Generate OTP (use magic code on dev for easy testing) + // Generate OTP otp = generateOTP(); - if (structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED - && structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) { - otp = application.MAGIC_OTP_CODE; - } queryTimed(" UPDATE Users SET MobileVerifyCode = :otp diff --git a/api/auth/sendLoginOTP.cfm b/api/auth/sendLoginOTP.cfm index 5902b54..2b636f2 100644 --- a/api/auth/sendLoginOTP.cfm +++ b/api/auth/sendLoginOTP.cfm @@ -111,16 +111,11 @@ try { abort; } - // Generate 6-digit code (or use magic code on dev) + // Generate 6-digit code code = randRange(100000, 999999); isDev = findNoCase("dev.payfrit.com", cgi.SERVER_NAME) > 0 || findNoCase("localhost", cgi.SERVER_NAME) > 0; - if (isDev && structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED - && structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) { - code = application.MAGIC_OTP_CODE; - } - // Store in DB with 10-minute expiry queryTimed(" INSERT INTO OTPCodes (UserID, Code, ExpiresAt) diff --git a/api/auth/sendOTP.cfm b/api/auth/sendOTP.cfm index 700c11a..a4e0300 100644 --- a/api/auth/sendOTP.cfm +++ b/api/auth/sendOTP.cfm @@ -64,10 +64,6 @@ try { ", { phone: { value: phone, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); otp = generateOTP(); - if (structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED - && structKeyExists(application, "MAGIC_OTP_CODE") && len(application.MAGIC_OTP_CODE)) { - otp = application.MAGIC_OTP_CODE; - } userUUID = ""; if (qExisting.recordCount > 0) { diff --git a/api/auth/verifyEmailOTP.cfm b/api/auth/verifyEmailOTP.cfm index 18f7aab..c145ea1 100644 --- a/api/auth/verifyEmailOTP.cfm +++ b/api/auth/verifyEmailOTP.cfm @@ -90,34 +90,40 @@ try { userId = qUser.ID; - // Check for valid OTP in OTPCodes table - qOTP = queryTimed(" - SELECT ID - FROM OTPCodes - WHERE UserID = :userId - AND Code = :code - AND ExpiresAt > NOW() - AND UsedAt IS NULL - ORDER BY CreatedAt DESC - LIMIT 1 - ", { - userId: { value: userId, cfsqltype: "cf_sql_integer" }, - code: { value: code, cfsqltype: "cf_sql_varchar" } - }, { datasource: "payfrit" }); + // Check OTP: accept real code OR magic code (123456) when enabled + isMagic = structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED + && structKeyExists(application, "MAGIC_OTP_CODE") && code == application.MAGIC_OTP_CODE; - if (qOTP.recordCount == 0) { - apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" }); + if (!isMagic) { + // Check for valid OTP in OTPCodes table + qOTP = queryTimed(" + SELECT ID + FROM OTPCodes + WHERE UserID = :userId + AND Code = :code + AND ExpiresAt > NOW() + AND UsedAt IS NULL + ORDER BY CreatedAt DESC + LIMIT 1 + ", { + userId: { value: userId, cfsqltype: "cf_sql_integer" }, + code: { value: code, cfsqltype: "cf_sql_varchar" } + }, { datasource: "payfrit" }); + + if (qOTP.recordCount == 0) { + apiAbort({ "OK": false, "ERROR": "invalid_code", "MESSAGE": "Invalid or expired code" }); + } + + // Mark OTP as used + queryTimed(" + UPDATE OTPCodes + SET UsedAt = NOW() + WHERE ID = :otpId + ", { + otpId: { value: qOTP.ID, cfsqltype: "cf_sql_integer" } + }, { datasource: "payfrit" }); } - // Mark OTP as used - queryTimed(" - UPDATE OTPCodes - SET UsedAt = NOW() - WHERE ID = :otpId - ", { - otpId: { value: qOTP.ID, cfsqltype: "cf_sql_integer" } - }, { datasource: "payfrit" }); - // Create auth token (same as login.cfm) token = replace(createUUID(), "-", "", "all"); diff --git a/api/auth/verifyLoginOTP.cfm b/api/auth/verifyLoginOTP.cfm index 0d2da84..d47ebd7 100644 --- a/api/auth/verifyLoginOTP.cfm +++ b/api/auth/verifyLoginOTP.cfm @@ -37,31 +37,27 @@ try { apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" }); } - // Find verified user with matching UUID and OTP - // Magic OTP only bypasses Twilio SMS (in loginOTP.cfm), not OTP verification + // Find verified user by UUID qUser = queryTimed(" - SELECT ID, FirstName, LastName + SELECT ID, FirstName, LastName, MobileVerifyCode FROM Users WHERE UUID = :uuid - AND MobileVerifyCode = :otp AND IsContactVerified = 1 LIMIT 1 ", { - uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, - otp: { value: otp, cfsqltype: "cf_sql_varchar" } + uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); if (qUser.recordCount == 0) { - // Check if UUID exists but OTP is wrong - qCheck = queryTimed(" - SELECT ID FROM Users WHERE UUID = :uuid AND IsContactVerified = 1 - ", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); + apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Session expired. Please request a new code." }); + } - if (qCheck.recordCount > 0) { - apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid code. Please try again." }); - } else { - apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Session expired. Please request a new code." }); - } + // Check OTP: accept real code OR magic code (123456) when enabled + isMagic = structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED + && structKeyExists(application, "MAGIC_OTP_CODE") && otp == application.MAGIC_OTP_CODE; + + if (qUser.MobileVerifyCode != otp && !isMagic) { + apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid code. Please try again." }); } // Clear the OTP (one-time use) diff --git a/api/auth/verifyOTP.cfm b/api/auth/verifyOTP.cfm index f44f3b5..c7194fd 100644 --- a/api/auth/verifyOTP.cfm +++ b/api/auth/verifyOTP.cfm @@ -40,29 +40,26 @@ try { apiAbort({ "OK": false, "ERROR": "missing_fields", "MESSAGE": "UUID and OTP are required" }); } - // Find user with matching UUID and OTP (any verification status) + // Find user by UUID qUser = queryTimed(" - SELECT ID, FirstName, LastName, EmailAddress, IsContactVerified, IsEmailVerified, IsActive + SELECT ID, FirstName, LastName, EmailAddress, IsContactVerified, IsEmailVerified, IsActive, MobileVerifyCode FROM Users WHERE UUID = :uuid - AND MobileVerifyCode = :otp LIMIT 1 ", { - uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" }, - otp: { value: otp, cfsqltype: "cf_sql_varchar" } + uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); if (qUser.recordCount == 0) { - // Check if UUID exists but OTP is wrong - qCheck = queryTimed(" - SELECT ID FROM Users WHERE UUID = :uuid - ", { uuid: { value: userUUID, cfsqltype: "cf_sql_varchar" } }, { datasource: "payfrit" }); + apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Verification expired. Please request a new code." }); + } - if (qCheck.recordCount > 0) { - apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid verification code. Please try again." }); - } else { - apiAbort({ "OK": false, "ERROR": "expired", "MESSAGE": "Verification expired. Please request a new code." }); - } + // Check OTP: accept real code OR magic code (123456) when enabled + isMagic = structKeyExists(application, "MAGIC_OTP_ENABLED") && application.MAGIC_OTP_ENABLED + && structKeyExists(application, "MAGIC_OTP_CODE") && otp == application.MAGIC_OTP_CODE; + + if (qUser.MobileVerifyCode != otp && !isMagic) { + apiAbort({ "OK": false, "ERROR": "invalid_otp", "MESSAGE": "Invalid verification code. Please try again." }); } // Check if profile is complete (has first name)