即使在设置环境变量并运行模拟器之后,Firebase Auth也会攻击生产

问题描述

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

根据the documentation on how to connect to the authentication emulator,对于环境变量FIREBASE_AUTH_EMULATOR_HOST='localhost:9099'就足够了(假设用户尚未更改auth仿真器的默认主机,而我没有)。

按照报纸的原则,由于涉及的代码很多,我将从更广泛的信息转向更具体的信息。

首先,CF。这种注册是通过如下所示的 callable 云函数完成的:

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的配置对象来初始化admin。我已经对其进行了编辑,但是在所有情况下都使用相同的项目ID:原始的ID(我是说从Firebase控制台获得的,不是虚构的)。

// 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 protected]','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()))
}

为了遵循Clean Architecture指南,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
        }
    }
}

我有更多的测试文件,但是由于它们按摩卡字母顺序运行,因此这是第一个运行的文件。因此,我想对admin.initializeApp()的任何其他调用都不会干扰此测试的执行。不过,我利用这种顺序只调用了该文件中的admin.initializeApp()

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)