Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import * as functions from "firebase-functions/v1";
- import * as admin from "firebase-admin";
- import * as twilio from "./tools/twilio";
- import { FirebaseFunctionsRateLimiter } from "@omgovich/firebase-functions-rate-limiter";
- import { Database } from "firebase-admin/database";
- import { Twilio } from "twilio";
- import { VerificationCheckListInstanceCreateOptions } from "twilio/lib/rest/verify/v2/service/verificationCheck";
- import { UserRecord } from "firebase-functions/v1/auth";
- const endpointApiKey = "API_KEY";
- interface RateLimiterConfig {
- name: string;
- maxCalls: number;
- periodSeconds: number;
- }
- const createLimiter = (
- config: RateLimiterConfig,
- database: Database
- ): FirebaseFunctionsRateLimiter => {
- return FirebaseFunctionsRateLimiter.withRealtimeDbBackend(config, database);
- };
- const verifyPhoneNumberIpRateLimiterConfig: RateLimiterConfig = {
- name: "verifyPhoneNumberIpRateLimiter",
- maxCalls: 10,
- periodSeconds: 15 * 60,
- };
- const verifyPhoneNumberGlobalRateLimiterConfig: RateLimiterConfig = {
- name: "verifyPhoneNumberGlobalRateLimiter",
- maxCalls: 30,
- periodSeconds: 15 * 60,
- };
- /**
- * This function returns whether the request is rate limited or not.
- * Each call will increment the ratelimit counters.
- * @param {functions.https.Request} req The request to check
- * @param {string} phoneNumber The phone number from the request
- */
- async function isRateLimited(
- req: functions.https.Request,
- phoneNumber: string | undefined
- ): Promise<{ error: string } | false> {
- const database = admin.database();
- const verifyPhoneNumberIpRateLimiter = createLimiter(
- verifyPhoneNumberIpRateLimiterConfig,
- database
- );
- const isIpQuotaExceeded =
- await verifyPhoneNumberIpRateLimiter.isQuotaExceededOrRecordUsage(
- phoneNumber
- );
- if (isIpQuotaExceeded) {
- // We must not increment the global counter, otherwise a dos from one
- // ip would be more likely to block all other users
- // Incrementing past the limit is not needed
- return { error: "rate-limit/phone-number-limit-reached" };
- }
- const verifyPhoneNumberGlobalRateLimiter = createLimiter(
- verifyPhoneNumberGlobalRateLimiterConfig,
- database
- );
- const isGlobalQuotaExceeded =
- await verifyPhoneNumberGlobalRateLimiter.isQuotaExceededOrRecordUsage(
- phoneNumber
- );
- if (isGlobalQuotaExceeded) {
- return { error: "rate-limit/global-limit-reached" };
- }
- return false;
- }
- /**
- * This function checks whether the phone number is safe to handle
- * Goal is not to check for validity as this will be done by twilio
- * @param {string} phoneString The phone number to check
- * @return {boolean} Whether the phone number is safe to handle
- */
- function phoneNumberIsSanitized(phoneString: string): boolean {
- const regex = /^\+\d{1,100}$/;
- return regex.test(phoneString);
- }
- export const sendVerificationCode = functions
- .runWith({
- secrets: [
- "TWILIO_PROJECT_ID",
- "TWILIO_ACCOUNT_SID",
- "TWILIO_AUTH_TOKEN",
- "TWILIO_TEST_NUMBERS",
- ],
- })
- .region("europe-west1")
- .https.onRequest(async (req, res) => {
- const apiKey =
- req.query?.apiKey?.toString() || req.body.data?.apiKey?.toString();
- if (apiKey !== endpointApiKey) {
- res.status(401).send({ error: "Unauthorized" });
- return;
- }
- if (req.method !== "POST") {
- res.status(405).send({ error: "Method not allowed" });
- return;
- }
- const phoneNumber =
- req.query?.phoneNumber?.toString() ||
- req.body.data?.phoneNumber?.toString();
- if (!phoneNumber || !phoneNumberIsSanitized(phoneNumber)) {
- // Don't tell what's wrong, we don't want to give hints to attackers
- res.status(400).send({ error: "Bad request" });
- return;
- }
- const appHash =
- req.query?.appHash?.toString() || req.body.data?.appHash?.toString();
- const rateLimitResult = await isRateLimited(req, phoneNumber);
- if (rateLimitResult) {
- res.status(200).send({ data: rateLimitResult });
- return;
- }
- const loginResult = await sendVerificationCode(phoneNumber, appHash);
- res.status(200).send({ data: loginResult });
- });
- export const checkVerificationCode = functions
- .runWith({
- secrets: [
- "TWILIO_PROJECT_ID",
- "TWILIO_ACCOUNT_SID",
- "TWILIO_AUTH_TOKEN",
- "TWILIO_TEST_NUMBERS",
- ],
- })
- .region("europe-west1")
- .https.onRequest(async (req, res) => {
- const apiKey =
- req.query?.apiKey?.toString() || req.body.data?.apiKey?.toString();
- if (apiKey !== endpointApiKey) {
- res.status(401).send({ error: "Unauthorized" });
- return;
- }
- const phoneNumber =
- req.query.phoneNumber?.toString() ||
- req.body.data.phoneNumber?.toString();
- const code = req.query.code?.toString() || req.body.data.code?.toString();
- if (!phoneNumber || !code) {
- res.status(400).send({ error: "Missing parameters" });
- return;
- }
- const codeSendResult = await attemptLogin(phoneNumber, code);
- res.status(200).send({ data: codeSendResult });
- });
- const getTwilioError = (error) => {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- const errorCode = error.code;
- if (!errorCode) {
- return { error: "verification/something-went-wrong" };
- }
- switch (errorCode) {
- case 20404:
- return { error: "verification/not-found" };
- case 60200:
- return { error: "verification/invalid-phone-number" };
- case 60202:
- return { error: "verification/too-many-verify-requests" };
- case 60203:
- return { error: "verification/too-many-send-requests" };
- case 62009: // Our account sid was not found.
- return { error: "verification/internal-error" };
- case 60207: // Our service is rate limited.
- return { error: "verification/internal-error" };
- default:
- return { error: "verification/something-went-wrong" };
- }
- };
- const sendVerificationCode = async (
- phoneNumber,
- appHash: string | undefined
- ) => {
- if (await isPhoneNumberDisabled(phoneNumber)) {
- return { error: "firebase-auth/user-disabled" };
- }
- const twilioProjectId = process.env.TWILIO_PROJECT_ID ?? "";
- const twilioAccountSid = process.env.TWILIO_ACCOUNT_SID ?? "";
- const twilioAuthToken = process.env.TWILIO_AUTH_TOKEN ?? "";
- const client = new Twilio(twilioAccountSid, twilioAuthToken);
- const channel = "sms";
- try {
- const result = await client.verify.v2
- .services(twilioProjectId)
- .verifications.create({
- to: phoneNumber,
- channel: channel,
- appHash: appHash,
- });
- return { status: `verification/${result.status}` };
- } catch (e) {
- return getTwilioError(e);
- }
- };
- const attemptLogin = async (
- phoneNumber,
- code,
- createUserIfNotExist: boolean
- ) => {
- const twilioProjectId = process.env.TWILIO_PROJECT_ID ?? "";
- const twilioAccountSid = process.env.TWILIO_ACCOUNT_SID ?? "";
- const twilioAuthToken = process.env.TWILIO_AUTH_TOKEN ?? "";
- const client = new Twilio(twilioAccountSid, twilioAuthToken);
- try {
- const result = await client.verify.v2
- .services(twilioProjectId)
- .verificationChecks.create({
- to: phoneNumber,
- code: code,
- } as VerificationCheckListInstanceCreateOptions);
- if (result.status == "approved") {
- return await phoneNumberLogin(phoneNumber, createUserIfNotExist);
- } else {
- return { error: "verification/wrong-code" };
- }
- } catch (e) {
- return getTwilioError(e);
- }
- };
- export const isPhoneNumberDisabled = async (phoneNumber) => {
- try {
- const user = await admin.auth().getUserByPhoneNumber(phoneNumber);
- return user.disabled;
- } catch (e) {
- return false;
- }
- };
- export const phoneNumberLogin = async (
- phoneNumber: string,
- createUser: boolean
- ) => {
- let user: UserRecord | null = null;
- // Get user if it exists for the phone number
- try {
- user = await admin.auth().getUserByPhoneNumber(phoneNumber);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- } catch (e: any) {
- if (e.code != "auth/user-not-found") {
- return { error: e.code };
- }
- }
- if (user) {
- // There seems to be a user with this phone number
- return await createTokenForUser(user);
- }
- if (createUser) {
- const user = await admin.auth().createUser({ phoneNumber: phoneNumber });
- return createLoginToken(user.uid);
- } else {
- // No user creation and no anonymous user given
- return { error: "firebase-auth/no-phone-number-found" };
- }
- };
- export const createTokenForUser = async (user: UserRecord) => {
- // Check if user is disabled
- if (user.disabled) {
- return { error: "firebase-auth/user-disabled" };
- }
- return createLoginToken(user.uid);
- };
- /**
- * Generates a sign in token for a given uid
- *
- * @async
- * @param {string} uid
- */
- async function createLoginToken(uid: string) {
- try {
- // For this to work the SA running the function needs Service Account Token Creator role
- // This usually is {project-name}@appspot.gserviceaccount.com unless another sa is defined
- const customToken = await admin.auth().createCustomToken(uid);
- return { token: customToken, uid: uid };
- } catch (e) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- const errorCode = e.code;
- return { error: errorCode };
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement