首页 > 解决方案 > 即使在设置环境变量并运行模拟器后,Firebase Auth 也会攻击生产

问题描述

我有一个云函数作为端点,前端可以调用它来为我们的应用程序注册一个新用户。我正在编写单元测试以验证此 CF 确实按预期运行,但我无法连接到身份验证模拟器。我能够正确模拟 firestore 和所有其他服务,但是当涉及到 Auth 时,测试直接攻击生产环境,在那里注册用户。

根据有关如何连接到身份验证模拟器的文档,环境变量就足够了FIREBASE_AUTH_EMULATOR_HOST='localhost:9099'(假设用户没有更改身份验证模拟器的默认主机,我没有更改)。

遵循报纸原则,我将从更广泛的信息到更具体的信息,因为涉及到相当多的代码。

首先,CF。此类注册是通过如下所示的可调用云函数完成的:

export const signUpUser_v1 = functions.https.onCall(async (data: SignUpUserRequestModel, context) => {
    return await executeIfUserIsNotLoggedIn(async () => {
        const controller = getUsersControllerInstance()
        return await controller.signUpUser(data)
    }, context)
})

executeIfUserIsNotLoggedIn我认为,这与我们的问题无关,但由于它非常简单,我将把它留在这里,以防我错了,可以帮助解决问题。该函数executeIfUserIsNotLoggedIn用作一个中间件,通过检查上下文是否不包含身份验证凭据来确保没有登录的人执行此端点:

export async function executeIfUserIsNotLoggedIn(cb: () => any, context: functions.https.CallableContext) {
    if (context.auth?.uid) throw new functions.https.HttpsError('permission-denied', 'Logout first')

    return await cb()
}

现在,测试文件。在这个文件中,我清楚地定义了所需的变量,并通过将配置对象传递给它来初始化管理员projectId。我已经对其进行了编辑,但在所有情况下都使用相同的项目 ID:原始项目 ID(来自 Firebase 控制台的项目 ID,我的意思是,不是编造的)。

// tslint:disable: no-implicit-dependencies
// tslint:disable: no-import-side-effect
import { assert } from 'chai'
import admin = require('firebase-admin')
import * as tests from 'firebase-functions-test'
import 'mocha'
import * as path from 'path'
import { ChildrenSituationEnum } from '../../core/entities/enums/ChildrenSituationEnum'
import { GenderEnum } from '../../core/entities/enums/GenderEnum'
import { UserTraitsEnum } from '../../core/entities/enums/UserTraitsEnum'
import { SignUpUserRequestModel } from '../../core/requestModels/SignUpUserRequestModel'
import { SignUpUserResponseModel } from '../../core/responseModels/SignUpUserResponseModel'
import { FirestoreCollectionsEnum } from '../../services/firestore/FirestoreCollectionsEnum'
import { signUpUser_v1 } from '../../usersManagementFunctions'

process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080'
process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099'
process.env.GCLOUD_PROJECT = 'tribbum-ffe98'

const test = tests(
    {
        projectId: '[REDACTED]',
        databaseURL: 'https://[REDACTED].firebaseio.com',
        storageBucket: '[REDACTED].appspot.com',
    },
    path.join(__dirname, '[REDACTED]')
)

describe('Cloud Functions', () => {
    admin.initializeApp({ projectId: '[REDACTED]' })

    before(async () => {
        await deleteAllUsersFromUsersCollection()
    })

    beforeEach(async () => {
        await deleteAllUsersFromUsersCollection()
    })

    after(async () => {
        await deleteAllUsersFromUsersCollection()
    })

    describe('signupUser_v1', () => {
        const baseRequest = new SignUpUserRequestModel(
            'Name',
            'Surname',
            45,
            GenderEnum.FEMALE,
            'photoUrl',
            'email@email.com',
            '123456',
            ChildrenSituationEnum.DOESNT_HAVE_KIDS,
            [UserTraitsEnum.EMPLOYED],
            'description',
            10,
            100
        )

        it('When request is correct, returns a user in the response', async () => {
            const cf = test.wrap(signUpUser_v1)

            const response = <SignUpUserResponseModel>await cf(baseRequest)

            assert.equal(response.user.name, 'Name')
        })
    })
})

async function deleteAllUsersFromUsersCollection() {
    const query = await admin.firestore().collection(FirestoreCollectionsEnum.USERS).get()
    await Promise.all(query.docs.map((doc) => doc.ref.delete()))
}

为了遵循清洁架构指南,CF 接收一个 RequestModel 作为第一个参数,并将其传递给期望它的控制器方法。这是控制器:

import { IUserEntity } from '../entities/IUserEntity'
import { User } from '../entities/User'
import { IEntityGateway } from '../gateways/IEntityGateway'
import { IIdentityGateway } from '../gateways/IIdentityGateway'
import { IRequestsValidationGateway } from '../gateways/IRequestsValidationGateway'
import { IUserInteractor } from '../interactors/IUsersInteractor'
import { SignUpUserRequestModel } from '../requestModels/SignUpUserRequestModel'
import { UpdateUserEmailAddressRequestModel } from '../requestModels/UpdateUserEmailAddressRequestModel'
import { UpdateUserProfileInformationRequestModel } from '../requestModels/UpdateUserProfileInformationRequestModel'
import { SignUpUserResponseModel } from '../responseModels/SignUpUserResponseModel'
import { UpdateUserEmailAddressResponseModel } from '../responseModels/UpdateUserEmailAddressResponseModel'
import { UpdateUserProfileInformationResponseModel } from '../responseModels/UpdateUserProfileInformationResponseModel'
import { IUuid } from '../tools/uuid/IUuid'

export class UsersController implements IUserInteractor {
    private _uuid: IUuid
    private _persistence: IEntityGateway
    private _identity: IIdentityGateway
    private _validation: IRequestsValidationGateway

    constructor(
        uuid: IUuid,
        persistence: IEntityGateway,
        identity: IIdentityGateway,
        validation: IRequestsValidationGateway
    ) {
        this._uuid = uuid
        this._persistence = persistence
        this._identity = identity
        this._validation = validation
    }

    async signUpUser(request: SignUpUserRequestModel): Promise<SignUpUserResponseModel> {
        this._validation.validate(request)

        if (await this._identity.emailIsAlreadyInUse(request.email)) throw new Error(`Email already in use`)

        const user: IUserEntity = this.buildUserFromRequestInformation(request)

        const identityPromise = this._identity.signUpNewUser(user, request.password)
        const persistencePromise = this._persistence.createUser(user)

        await Promise.all([identityPromise, persistencePromise])

        const response: SignUpUserResponseModel = {
            user,
        }

        return response
    }

    private buildUserFromRequestInformation(request: SignUpUserRequestModel) {
        const user: IUserEntity = new User(
            this._uuid.generateUuidV4(),
            request.name,
            request.surname,
            request.age,
            request.gender,
            request.photoUrl,
            request.email,
            request.childrenSituation,
            request.traitsArray,
            request.description,
            request.budgetMin,
            request.budgetMax
        )
        return user
    }
}

控制器有更多方法,但这是唯一被调用的方法。如您所见,控制器使用依赖注入来接收它们将使用的服务。影响身份验证的服务是IIdentityGateway,其实现使用 Firebase Auth。让我们来看看它:

界面,很简单。

import { IUserEntity } from '../entities/IUserEntity'

export interface IIdentityGateway {
    updateUserEmail(id: string, newEmail: string): Promise<void>
    signUpNewUser(user: IUserEntity, password: string): Promise<void>
    emailIsAlreadyInUse(email: string): Promise<boolean>
}

以及使用 Auth 的接口的实现:

import admin = require('firebase-admin')
import { IUserEntity } from '../../../core/entities/IUserEntity'
import { IIdentityGateway } from '../../../core/gateways/IIdentityGateway'

export class FirebaseAuthIdentityGateway implements IIdentityGateway {
    private _auth: admin.auth.Auth
    private _admin: typeof admin

    constructor() {
        this._admin = require('firebase-admin')
        // I have tried both this:
        this._auth = this._admin.auth()
        // And this:
        // this._auth = admin.auth()
    }

    async updateUserEmail(id: string, newEmail: string): Promise<void> {
        await this._auth.updateUser(id, {
            email: newEmail,
        })
    }

    async signUpNewUser(user: IUserEntity, password: string): Promise<void> {
        await this._auth.createUser({
            uid: user.id,
            password,
            email: user.email,
        })
    }

    async emailIsAlreadyInUse(email: string): Promise<boolean> {
        try {
            await this._auth.getUserByEmail(email)
            return true
        } catch (err) {
            if (err.code === 'auth/user-not-found') {
                return false
            }

            throw err
        }
    }
}

我有更多的测试文件,但是因为它们按 mocha 的字母顺序运行,所以这是第一个运行的。所以我想任何其他的调用admin.initializeApp()都不会干扰这个测试的执行。不过,我利用这个顺序只调用admin.initializeApp()这个文件。

标签: typescriptfirebasefirebase-authenticationfirebase-tools

解决方案


更新项目 firebase 的依赖项已解决此问题。


推荐阅读