Advertisement
Guest User

Untitled

a guest
Apr 23rd, 2025
85
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import * as functions from "firebase-functions/v1";
  2. import * as admin from "firebase-admin";
  3. import * as twilio from "./tools/twilio";
  4. import { FirebaseFunctionsRateLimiter } from "@omgovich/firebase-functions-rate-limiter";
  5. import { Database } from "firebase-admin/database";
  6. import { Twilio } from "twilio";
  7. import { VerificationCheckListInstanceCreateOptions } from "twilio/lib/rest/verify/v2/service/verificationCheck";
  8. import { UserRecord } from "firebase-functions/v1/auth";
  9.  
  10. const endpointApiKey = "API_KEY";
  11.  
  12. interface RateLimiterConfig {
  13.   name: string;
  14.   maxCalls: number;
  15.   periodSeconds: number;
  16. }
  17.  
  18. const createLimiter = (
  19.   config: RateLimiterConfig,
  20.   database: Database
  21. ): FirebaseFunctionsRateLimiter => {
  22.   return FirebaseFunctionsRateLimiter.withRealtimeDbBackend(config, database);
  23. };
  24.  
  25. const verifyPhoneNumberIpRateLimiterConfig: RateLimiterConfig = {
  26.   name: "verifyPhoneNumberIpRateLimiter",
  27.   maxCalls: 10,
  28.   periodSeconds: 15 * 60,
  29. };
  30.  
  31. const verifyPhoneNumberGlobalRateLimiterConfig: RateLimiterConfig = {
  32.   name: "verifyPhoneNumberGlobalRateLimiter",
  33.   maxCalls: 30,
  34.   periodSeconds: 15 * 60,
  35. };
  36.  
  37. /**
  38.  * This function returns whether the request is rate limited or not.
  39.  * Each call will increment the ratelimit counters.
  40.  * @param {functions.https.Request} req The request to check
  41.  * @param {string} phoneNumber The phone number from the request
  42.  */
  43. async function isRateLimited(
  44.   req: functions.https.Request,
  45.   phoneNumber: string | undefined
  46. ): Promise<{ error: string } | false> {
  47.   const database = admin.database();
  48.  
  49.   const verifyPhoneNumberIpRateLimiter = createLimiter(
  50.     verifyPhoneNumberIpRateLimiterConfig,
  51.     database
  52.   );
  53.   const isIpQuotaExceeded =
  54.     await verifyPhoneNumberIpRateLimiter.isQuotaExceededOrRecordUsage(
  55.       phoneNumber
  56.     );
  57.   if (isIpQuotaExceeded) {
  58.     // We must not increment the global counter, otherwise a dos from one
  59.     // ip would be more likely to block all other users
  60.     // Incrementing past the limit is not needed
  61.     return { error: "rate-limit/phone-number-limit-reached" };
  62.   }
  63.  
  64.   const verifyPhoneNumberGlobalRateLimiter = createLimiter(
  65.     verifyPhoneNumberGlobalRateLimiterConfig,
  66.     database
  67.   );
  68.   const isGlobalQuotaExceeded =
  69.     await verifyPhoneNumberGlobalRateLimiter.isQuotaExceededOrRecordUsage(
  70.       phoneNumber
  71.     );
  72.   if (isGlobalQuotaExceeded) {
  73.     return { error: "rate-limit/global-limit-reached" };
  74.   }
  75.  
  76.   return false;
  77. }
  78. /**
  79.  * This function checks whether the phone number is safe to handle
  80.  * Goal is not to check for validity as this will be done by twilio
  81.  * @param {string} phoneString The phone number to check
  82.  * @return {boolean} Whether the phone number is safe to handle
  83.  */
  84. function phoneNumberIsSanitized(phoneString: string): boolean {
  85.   const regex = /^\+\d{1,100}$/;
  86.   return regex.test(phoneString);
  87. }
  88.  
  89. export const sendVerificationCode = functions
  90.   .runWith({
  91.     secrets: [
  92.       "TWILIO_PROJECT_ID",
  93.       "TWILIO_ACCOUNT_SID",
  94.       "TWILIO_AUTH_TOKEN",
  95.       "TWILIO_TEST_NUMBERS",
  96.     ],
  97.   })
  98.   .region("europe-west1")
  99.   .https.onRequest(async (req, res) => {
  100.     const apiKey =
  101.       req.query?.apiKey?.toString() || req.body.data?.apiKey?.toString();
  102.     if (apiKey !== endpointApiKey) {
  103.       res.status(401).send({ error: "Unauthorized" });
  104.       return;
  105.     }
  106.  
  107.     if (req.method !== "POST") {
  108.       res.status(405).send({ error: "Method not allowed" });
  109.       return;
  110.     }
  111.  
  112.     const phoneNumber =
  113.       req.query?.phoneNumber?.toString() ||
  114.       req.body.data?.phoneNumber?.toString();
  115.  
  116.     if (!phoneNumber || !phoneNumberIsSanitized(phoneNumber)) {
  117.       // Don't tell what's wrong, we don't want to give hints to attackers
  118.       res.status(400).send({ error: "Bad request" });
  119.       return;
  120.     }
  121.  
  122.     const appHash =
  123.       req.query?.appHash?.toString() || req.body.data?.appHash?.toString();
  124.  
  125.     const rateLimitResult = await isRateLimited(req, phoneNumber);
  126.     if (rateLimitResult) {
  127.       res.status(200).send({ data: rateLimitResult });
  128.       return;
  129.     }
  130.  
  131.     const loginResult = await sendVerificationCode(phoneNumber, appHash);
  132.  
  133.     res.status(200).send({ data: loginResult });
  134.   });
  135.  
  136. export const checkVerificationCode = functions
  137.   .runWith({
  138.     secrets: [
  139.       "TWILIO_PROJECT_ID",
  140.       "TWILIO_ACCOUNT_SID",
  141.       "TWILIO_AUTH_TOKEN",
  142.       "TWILIO_TEST_NUMBERS",
  143.     ],
  144.   })
  145.   .region("europe-west1")
  146.   .https.onRequest(async (req, res) => {
  147.     const apiKey =
  148.       req.query?.apiKey?.toString() || req.body.data?.apiKey?.toString();
  149.     if (apiKey !== endpointApiKey) {
  150.       res.status(401).send({ error: "Unauthorized" });
  151.       return;
  152.     }
  153.  
  154.     const phoneNumber =
  155.       req.query.phoneNumber?.toString() ||
  156.       req.body.data.phoneNumber?.toString();
  157.  
  158.     const code = req.query.code?.toString() || req.body.data.code?.toString();
  159.     if (!phoneNumber || !code) {
  160.       res.status(400).send({ error: "Missing parameters" });
  161.       return;
  162.     }
  163.  
  164.     const codeSendResult = await attemptLogin(phoneNumber, code);
  165.  
  166.     res.status(200).send({ data: codeSendResult });
  167.   });
  168.  
  169. const getTwilioError = (error) => {
  170.   // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  171.   // @ts-ignore
  172.   const errorCode = error.code;
  173.  
  174.   if (!errorCode) {
  175.     return { error: "verification/something-went-wrong" };
  176.   }
  177.  
  178.   switch (errorCode) {
  179.     case 20404:
  180.       return { error: "verification/not-found" };
  181.     case 60200:
  182.       return { error: "verification/invalid-phone-number" };
  183.     case 60202:
  184.       return { error: "verification/too-many-verify-requests" };
  185.     case 60203:
  186.       return { error: "verification/too-many-send-requests" };
  187.     case 62009: // Our account sid was not found.
  188.       return { error: "verification/internal-error" };
  189.     case 60207: // Our service is rate limited.
  190.       return { error: "verification/internal-error" };
  191.     default:
  192.       return { error: "verification/something-went-wrong" };
  193.   }
  194. };
  195.  
  196. const sendVerificationCode = async (
  197.   phoneNumber,
  198.   appHash: string | undefined
  199. ) => {
  200.   if (await isPhoneNumberDisabled(phoneNumber)) {
  201.     return { error: "firebase-auth/user-disabled" };
  202.   }
  203.  
  204.   const twilioProjectId = process.env.TWILIO_PROJECT_ID ?? "";
  205.   const twilioAccountSid = process.env.TWILIO_ACCOUNT_SID ?? "";
  206.   const twilioAuthToken = process.env.TWILIO_AUTH_TOKEN ?? "";
  207.  
  208.   const client = new Twilio(twilioAccountSid, twilioAuthToken);
  209.  
  210.   const channel = "sms";
  211.  
  212.   try {
  213.     const result = await client.verify.v2
  214.       .services(twilioProjectId)
  215.       .verifications.create({
  216.         to: phoneNumber,
  217.         channel: channel,
  218.         appHash: appHash,
  219.       });
  220.  
  221.     return { status: `verification/${result.status}` };
  222.   } catch (e) {
  223.     return getTwilioError(e);
  224.   }
  225. };
  226.  
  227. const attemptLogin = async (
  228.   phoneNumber,
  229.   code,
  230.   createUserIfNotExist: boolean
  231. ) => {
  232.   const twilioProjectId = process.env.TWILIO_PROJECT_ID ?? "";
  233.   const twilioAccountSid = process.env.TWILIO_ACCOUNT_SID ?? "";
  234.   const twilioAuthToken = process.env.TWILIO_AUTH_TOKEN ?? "";
  235.  
  236.   const client = new Twilio(twilioAccountSid, twilioAuthToken);
  237.  
  238.   try {
  239.     const result = await client.verify.v2
  240.       .services(twilioProjectId)
  241.       .verificationChecks.create({
  242.         to: phoneNumber,
  243.         code: code,
  244.       } as VerificationCheckListInstanceCreateOptions);
  245.  
  246.     if (result.status == "approved") {
  247.       return await phoneNumberLogin(phoneNumber, createUserIfNotExist);
  248.     } else {
  249.       return { error: "verification/wrong-code" };
  250.     }
  251.   } catch (e) {
  252.     return getTwilioError(e);
  253.   }
  254. };
  255.  
  256. export const isPhoneNumberDisabled = async (phoneNumber) => {
  257.   try {
  258.     const user = await admin.auth().getUserByPhoneNumber(phoneNumber);
  259.     return user.disabled;
  260.   } catch (e) {
  261.     return false;
  262.   }
  263. };
  264.  
  265. export const phoneNumberLogin = async (
  266.   phoneNumber: string,
  267.   createUser: boolean
  268. ) => {
  269.   let user: UserRecord | null = null;
  270.  
  271.   // Get user if it exists for the phone number
  272.   try {
  273.     user = await admin.auth().getUserByPhoneNumber(phoneNumber);
  274.     // eslint-disable-next-line @typescript-eslint/no-explicit-any
  275.   } catch (e: any) {
  276.     if (e.code != "auth/user-not-found") {
  277.       return { error: e.code };
  278.     }
  279.   }
  280.  
  281.   if (user) {
  282.     // There seems to be a user with this phone number
  283.     return await createTokenForUser(user);
  284.   }
  285.  
  286.   if (createUser) {
  287.     const user = await admin.auth().createUser({ phoneNumber: phoneNumber });
  288.     return createLoginToken(user.uid);
  289.   } else {
  290.     // No user creation and no anonymous user given
  291.     return { error: "firebase-auth/no-phone-number-found" };
  292.   }
  293. };
  294.  
  295. export const createTokenForUser = async (user: UserRecord) => {
  296.   // Check if user is disabled
  297.   if (user.disabled) {
  298.     return { error: "firebase-auth/user-disabled" };
  299.   }
  300.  
  301.   return createLoginToken(user.uid);
  302. };
  303.  
  304. /**
  305.  * Generates a sign in token for a given uid
  306.  *
  307.  * @async
  308.  * @param {string} uid
  309.  */
  310. async function createLoginToken(uid: string) {
  311.   try {
  312.     // For this to work the SA running the function needs Service Account Token Creator role
  313.     // This usually is {project-name}@appspot.gserviceaccount.com unless another sa is defined
  314.     const customToken = await admin.auth().createCustomToken(uid);
  315.     return { token: customToken, uid: uid };
  316.   } catch (e) {
  317.     // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  318.     // @ts-ignore
  319.     const errorCode = e.code;
  320.     return { error: errorCode };
  321.   }
  322. }
  323.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement