diff --git a/spec/common/providers/identity.spec.ts b/spec/common/providers/identity.spec.ts index 308845172..73fb208d3 100644 --- a/spec/common/providers/identity.spec.ts +++ b/spec/common/providers/identity.spec.ts @@ -26,6 +26,9 @@ import * as identity from "../../../src/common/providers/identity"; const EVENT = "EVENT_TYPE"; const now = new Date(); +const TEST_NAME = "John Doe"; +const ALLOW = "ALLOW"; +const BLOCK = "BLOCK"; describe("identity", () => { describe("userRecordConstructor", () => { @@ -232,14 +235,14 @@ describe("identity", () => { describe("parseProviderData", () => { const decodedUserInfo = { provider_id: "google.com", - display_name: "John Doe", + display_name: TEST_NAME, photo_url: "https://lh3.googleusercontent.com/1234567890/photo.jpg", uid: "1234567890", email: "user@gmail.com", }; const userInfo = { providerId: "google.com", - displayName: "John Doe", + displayName: TEST_NAME, photoURL: "https://lh3.googleusercontent.com/1234567890/photo.jpg", uid: "1234567890", email: "user@gmail.com", @@ -340,12 +343,12 @@ describe("identity", () => { uid: "abcdefghijklmnopqrstuvwxyz", email: "user@gmail.com", email_verified: true, - display_name: "John Doe", + display_name: TEST_NAME, phone_number: "+11234567890", provider_data: [ { provider_id: "google.com", - display_name: "John Doe", + display_name: TEST_NAME, photo_url: "https://lh3.googleusercontent.com/1234567890/photo.jpg", email: "user@gmail.com", uid: "1234567890", @@ -366,7 +369,7 @@ describe("identity", () => { provider_id: "password", email: "user@gmail.com", uid: "user@gmail.com", - display_name: "John Doe", + display_name: TEST_NAME, }, ], password_hash: "passwordHash", @@ -407,11 +410,11 @@ describe("identity", () => { phoneNumber: "+11234567890", emailVerified: true, disabled: false, - displayName: "John Doe", + displayName: TEST_NAME, providerData: [ { providerId: "google.com", - displayName: "John Doe", + displayName: TEST_NAME, photoURL: "https://lh3.googleusercontent.com/1234567890/photo.jpg", email: "user@gmail.com", uid: "1234567890", @@ -435,7 +438,7 @@ describe("identity", () => { }, { providerId: "password", - displayName: "John Doe", + displayName: TEST_NAME, photoURL: undefined, email: "user@gmail.com", uid: "user@gmail.com", @@ -489,8 +492,9 @@ describe("identity", () => { }); describe("parseAuthEventContext", () => { + const TEST_RECAPTCHA_SCORE = 0.9; const rawUserInfo = { - name: "John Doe", + name: TEST_NAME, granted_scopes: "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile", id: "123456789", @@ -516,6 +520,7 @@ describe("identity", () => { user_agent: "USER_AGENT", locale: "en", raw_user_info: JSON.stringify(rawUserInfo), + recaptcha_score: TEST_RECAPTCHA_SCORE, }; const context = { locale: "en", @@ -523,6 +528,7 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: EVENT, + emailType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -534,6 +540,8 @@ describe("identity", () => { profile: rawUserInfo, username: undefined, isNewUser: false, + recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, }, credential: null, params: {}, @@ -563,6 +571,7 @@ describe("identity", () => { oauth_refresh_token: "REFRESH_TOKEN", oauth_token_secret: "OAUTH_TOKEN_SECRET", oauth_expires_in: 3600, + recaptcha_score: TEST_RECAPTCHA_SCORE, }; const context = { locale: "en", @@ -570,6 +579,7 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeSignIn:password", + emailType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -581,6 +591,8 @@ describe("identity", () => { profile: rawUserInfo, username: undefined, isNewUser: false, + recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, }, credential: { claims: undefined, @@ -619,14 +631,14 @@ describe("identity", () => { uid: "abcdefghijklmnopqrstuvwxyz", email: "user@gmail.com", email_verified: true, - display_name: "John Doe", + display_name: TEST_NAME, phone_number: "+11234567890", provider_data: [ { provider_id: "oidc.provider", email: "user@gmail.com", uid: "user@gmail.com", - display_name: "John Doe", + display_name: TEST_NAME, }, ], photo_url: "https://lh3.googleusercontent.com/1234567890/photo.jpg", @@ -647,6 +659,7 @@ describe("identity", () => { oauth_token_secret: "OAUTH_TOKEN_SECRET", oauth_expires_in: 3600, raw_user_info: JSON.stringify(rawUserInfo), + recaptcha_score: TEST_RECAPTCHA_SCORE, }; const context = { locale: "en", @@ -654,6 +667,7 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeCreate:oidc.provider", + emailType: undefined, authType: "USER", resource: { service: "identitytoolkit.googleapis.com", @@ -665,6 +679,8 @@ describe("identity", () => { providerId: "oidc.provider", profile: rawUserInfo, isNewUser: true, + recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, }, credential: { claims: undefined, @@ -681,6 +697,50 @@ describe("identity", () => { expect(identity.parseAuthEventContext(decodedJwt, "project-id", time)).to.deep.equal(context); }); + + it("should parse a beforeSendEmail event", () => { + const time = now.getTime(); + const decodedJwt = { + iss: "https://securetoken.google.com/project_id", + aud: "https://us-east1-project_id.cloudfunctions.net/function-1", + iat: 1, + exp: 60 * 60 + 1, + event_id: "EVENT_ID", + event_type: "beforeSendEmail", + user_agent: "USER_AGENT", + ip_address: "1.2.3.4", + locale: "en", + recaptcha_score: TEST_RECAPTCHA_SCORE, + email_type: "RESET_PASSWORD", + email: "johndoe@gmail.com", + }; + const context = { + locale: "en", + ipAddress: "1.2.3.4", + userAgent: "USER_AGENT", + eventId: "EVENT_ID", + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + emailType: "RESET_PASSWORD", + authType: "UNAUTHENTICATED", + resource: { + service: "identitytoolkit.googleapis.com", + name: "projects/project-id", + }, + timestamp: new Date(1000).toUTCString(), + additionalUserInfo: { + isNewUser: false, + profile: undefined, + providerId: undefined, + username: undefined, + recaptchaScore: TEST_RECAPTCHA_SCORE, + email: "johndoe@gmail.com", + }, + credential: null, + params: {}, + }; + + expect(identity.parseAuthEventContext(decodedJwt, "project-id", time)).to.deep.equal(context); + }); }); describe("validateAuthResponse", () => { @@ -762,4 +822,52 @@ describe("identity", () => { ); }); }); + + describe("generateResponsePayload", () => { + const DISPLAY_NAME_FIELD = "displayName"; + const TEST_RESPONSE = { + displayName: TEST_NAME, + recaptchaActionOverride: BLOCK, + } as identity.BeforeCreateResponse; + + const EXPECT_PAYLOAD = { + userRecord: { displayName: TEST_NAME, updateMask: DISPLAY_NAME_FIELD }, + recaptchaActionOverride: BLOCK, + }; + + const TEST_RESPONSE_RECAPTCHA_ALLOW = { + recaptchaActionOverride: ALLOW, + } as identity.BeforeCreateResponse; + + const EXPECT_PAYLOAD_RECAPTCHA_ALLOW = { + recaptchaActionOverride: ALLOW, + }; + + const TEST_RESPONSE_RECAPTCHA_UNDEFINED = { + displayName: TEST_NAME, + } as identity.BeforeSignInResponse; + + const EXPECT_PAYLOAD_UNDEFINED = { + userRecord: { displayName: TEST_NAME, updateMask: DISPLAY_NAME_FIELD }, + }; + it("should return empty object on undefined response", () => { + expect(identity.generateResponsePayload()).to.eql({}); + }); + + it("should exclude recaptchaActionOverride field from updateMask", () => { + expect(identity.generateResponsePayload(TEST_RESPONSE)).to.deep.equal(EXPECT_PAYLOAD); + }); + + it("should return recaptchaActionOverride when it is true on response", () => { + expect(identity.generateResponsePayload(TEST_RESPONSE_RECAPTCHA_ALLOW)).to.deep.equal( + EXPECT_PAYLOAD_RECAPTCHA_ALLOW + ); + }); + + it("should not return recaptchaActionOverride if undefined", () => { + const payload = identity.generateResponsePayload(TEST_RESPONSE_RECAPTCHA_UNDEFINED); + expect(payload.hasOwnProperty("recaptchaActionOverride")).to.be.false; + expect(payload).to.deep.equal(EXPECT_PAYLOAD_UNDEFINED); + }); + }); }); diff --git a/spec/v1/providers/auth.spec.ts b/spec/v1/providers/auth.spec.ts index f5f6a806d..6901e3fdf 100644 --- a/spec/v1/providers/auth.spec.ts +++ b/spec/v1/providers/auth.spec.ts @@ -305,6 +305,96 @@ describe("Auth Functions", () => { }); }); + describe("beforeEmail", () => { + it("should create function without options", () => { + const fn = auth.user().beforeEmail(() => Promise.resolve()); + + expect(fn.__trigger).to.deep.equal({ + labels: {}, + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + options: { + accessToken: false, + idToken: false, + refreshToken: false, + }, + }, + }); + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V1_ENDPOINT, + platform: "gcfv1", + labels: {}, + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + options: { + accessToken: false, + idToken: false, + refreshToken: false, + }, + }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: "identitytoolkit.googleapis.com", + reason: "Needed for auth blocking functions", + }, + ]); + }); + + it("should create the function with options", () => { + const fn = functions + .region("us-east1") + .runWith({ + timeoutSeconds: 90, + memory: "256MB", + }) + .auth.user({ + blockingOptions: { + accessToken: true, + refreshToken: false, + }, + }) + .beforeEmail(() => Promise.resolve()); + + expect(fn.__trigger).to.deep.equal({ + labels: {}, + regions: ["us-east1"], + availableMemoryMb: 256, + timeout: "90s", + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + options: { + accessToken: true, + idToken: false, + refreshToken: false, + }, + }, + }); + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V1_ENDPOINT, + platform: "gcfv1", + labels: {}, + region: ["us-east1"], + availableMemoryMb: 256, + timeoutSeconds: 90, + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + options: { + accessToken: true, + idToken: false, + refreshToken: false, + }, + }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: "identitytoolkit.googleapis.com", + reason: "Needed for auth blocking functions", + }, + ]); + }); + }); + describe("#_dataConstructor", () => { let cloudFunctionDelete: CloudFunction; diff --git a/spec/v2/providers/identity.spec.ts b/spec/v2/providers/identity.spec.ts index 7559a4133..7d18f0762 100644 --- a/spec/v2/providers/identity.spec.ts +++ b/spec/v2/providers/identity.spec.ts @@ -41,6 +41,15 @@ const BEFORE_SIGN_IN_TRIGGER = { }, }; +const BEFORE_EMAIL_TRIGGER = { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + options: { + accessToken: false, + idToken: false, + refreshToken: false, + }, +}; + const opts: identity.BlockingOptions = { accessToken: true, refreshToken: false, @@ -137,6 +146,50 @@ describe("identity", () => { }); }); + describe("beforeEmailSent", () => { + it("should accept a handler", () => { + const fn = identity.beforeEmailSent(() => Promise.resolve()); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + blockingTrigger: BEFORE_EMAIL_TRIGGER, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: "identitytoolkit.googleapis.com", + reason: "Needed for auth blocking functions", + }, + ]); + }); + }); + + it("should accept options and a handler", () => { + const fn = identity.beforeEmailSent(opts, () => Promise.resolve()); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + minInstances: 1, + region: ["us-west1"], + blockingTrigger: { + ...BEFORE_EMAIL_TRIGGER, + options: { + ...BEFORE_EMAIL_TRIGGER.options, + accessToken: true, + }, + }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: "identitytoolkit.googleapis.com", + reason: "Needed for auth blocking functions", + }, + ]); + }); + describe("beforeOperation", () => { it("should handle eventType and handler for before create events", () => { const fn = identity.beforeOperation("beforeCreate", () => Promise.resolve(), undefined); @@ -172,6 +225,23 @@ describe("identity", () => { ]); }); + it("should handle eventType and handler for before email events", () => { + const fn = identity.beforeOperation("beforeSendEmail", () => Promise.resolve(), undefined); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + blockingTrigger: BEFORE_EMAIL_TRIGGER, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: "identitytoolkit.googleapis.com", + reason: "Needed for auth blocking functions", + }, + ]); + }); + it("should handle eventType, options, and handler for before create events", () => { const fn = identity.beforeOperation("beforeCreate", opts, () => Promise.resolve()); @@ -221,6 +291,31 @@ describe("identity", () => { }, ]); }); + + it("should handle eventType, options, and handler for before send email events", () => { + const fn = identity.beforeOperation("beforeSendEmail", opts, () => Promise.resolve()); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + minInstances: 1, + region: ["us-west1"], + blockingTrigger: { + ...BEFORE_EMAIL_TRIGGER, + options: { + ...BEFORE_EMAIL_TRIGGER.options, + accessToken: true, + }, + }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: "identitytoolkit.googleapis.com", + reason: "Needed for auth blocking functions", + }, + ]); + }); }); describe("getOpts", () => { diff --git a/src/common/providers/identity.ts b/src/common/providers/identity.ts index 9997d7569..ed7b8d275 100644 --- a/src/common/providers/identity.ts +++ b/src/common/providers/identity.ts @@ -55,11 +55,12 @@ const CLAIMS_MAX_PAYLOAD_SIZE = 1000; * @hidden * @alpha */ -export type AuthBlockingEventType = "beforeCreate" | "beforeSignIn"; +export type AuthBlockingEventType = "beforeCreate" | "beforeSignIn" | "beforeSendEmail"; const EVENT_MAPPING: Record = { beforeCreate: "providers/cloud.auth/eventTypes/user.beforeCreate", beforeSignIn: "providers/cloud.auth/eventTypes/user.beforeSignIn", + beforeSendEmail: "providers/cloud.auth/eventTypes/user.beforeSendEmail", }; /** @@ -306,10 +307,12 @@ export interface AuthUserRecord { /** The additional user info component of the auth event context */ export interface AdditionalUserInfo { - providerId: string; + providerId?: string; profile?: any; username?: string; isNewUser: boolean; + recaptchaScore?: number; + email?: string; } /** The credential component of the auth event context */ @@ -324,6 +327,9 @@ export interface Credential { signInMethod: string; } +/** Possible types of emails as described by the GCIP backend. */ +export type EmailType = "EMAIL_SIGNIN" | "PASSWORD_RESET"; + /** Defines the auth event context for blocking events */ export interface AuthEventContext extends EventContext { locale?: string; @@ -331,11 +337,19 @@ export interface AuthEventContext extends EventContext { userAgent: string; additionalUserInfo?: AdditionalUserInfo; credential?: Credential; + emailType?: EmailType; } /** Defines the auth event for v2 blocking events */ export interface AuthBlockingEvent extends AuthEventContext { - data: AuthUserRecord; + data?: AuthUserRecord; +} + +/** The reCAPTCHA action options. */ +export type RecaptchaActionOptions = "ALLOW" | "BLOCK"; + +export interface BeforeEmailResponse { + recaptchaActionOverride?: RecaptchaActionOptions; } /** The handler response type for beforeCreate blocking events */ @@ -345,6 +359,7 @@ export interface BeforeCreateResponse { emailVerified?: boolean; photoURL?: string; customClaims?: object; + recaptchaActionOverride?: RecaptchaActionOptions; } /** The handler response type for beforeSignIn blocking events */ @@ -383,6 +398,7 @@ interface DecodedPayloadUserRecordEnrolledFactors { export interface DecodedPayloadUserRecord { uid: string; email?: string; + email_type?: string; email_verified?: boolean; phone_number?: string; display_name?: string; @@ -405,7 +421,7 @@ export interface DecodedPayload { exp: number; iat: number; iss: string; - sub: string; + sub?: string; event_id: string; event_type: string; ip_address: string; @@ -423,29 +439,45 @@ export interface DecodedPayload { oauth_refresh_token?: string; oauth_token_secret?: string; oauth_expires_in?: number; + recaptcha_score?: number; + email?: string; + email_type?: string; [key: string]: any; } -type HandlerV1 = ( - user: AuthUserRecord, - context: AuthEventContext -) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; +/** + * Internal definition to include all the fields that can be sent as + * a response from the blocking function to the backend. + * This is added mainly to have a type definition for 'generateResponsePayload' + * @internal */ +export interface ResponsePayload { + userRecord?: UserRecordResponsePayload; + recaptchaActionOverride?: RecaptchaActionOptions; +} -type HandlerV2 = ( +/** @internal */ +export interface UserRecordResponsePayload + extends Omit { + updateMask?: string; +} + +export type MaybeAsync = T | Promise; + +// N.B. As we add support for new auth blocking functions, some auth blocking event handlers +// will not receive a user record object. However, we can't make the user record parameter +// optional because it is listed before the required context parameter. +export type HandlerV1 = ( + userOrContext: AuthUserRecord | AuthEventContext, + context?: AuthEventContext +) => MaybeAsync; + +export type HandlerV2 = ( event: AuthBlockingEvent -) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; +) => MaybeAsync; + +export type AgnosticHandler = (HandlerV1 | HandlerV2) & { + platform: string; +}; /** * Checks for a valid identity platform web request, otherwise throws an HttpsError @@ -640,9 +672,40 @@ function parseAdditionalUserInfo(decodedJWT: DecodedPayload): AdditionalUserInfo profile, username, isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false, + recaptchaScore: decodedJWT.recaptcha_score, + email: decodedJWT.email, }; } +/** + * Helper to generate a response from the blocking function to the Firebase Auth backend. + * @internal + */ +export function generateResponsePayload( + authResponse?: BeforeCreateResponse | BeforeSignInResponse +): ResponsePayload { + if (!authResponse) { + return {}; + } + + const { recaptchaActionOverride, ...formattedAuthResponse } = authResponse; + const result = {} as ResponsePayload; + const updateMask = getUpdateMask(formattedAuthResponse); + + if (updateMask.length !== 0) { + result.userRecord = { + ...formattedAuthResponse, + updateMask, + }; + } + + if (recaptchaActionOverride !== undefined) { + result.recaptchaActionOverride = recaptchaActionOverride; + } + + return result; +} + /** Helper to get the Credential from the decoded jwt */ function parseAuthCredential(decodedJWT: DecodedPayload, time: number): Credential { if ( @@ -697,6 +760,7 @@ export function parseAuthEventContext( timestamp: new Date(decodedJWT.iat * 1000).toUTCString(), additionalUserInfo: parseAdditionalUserInfo(decodedJWT), credential: parseAuthCredential(decodedJWT, time), + emailType: decodedJWT.email_type as EmailType, params: {}, }; } @@ -781,7 +845,7 @@ export function getUpdateMask(authResponse?: BeforeCreateResponse | BeforeSignIn } /** @internal */ -export function wrapHandler(eventType: AuthBlockingEventType, handler: HandlerV1 | HandlerV2) { +export function wrapHandler(eventType: AuthBlockingEventType, handler: AgnosticHandler) { return async (req: express.Request, res: express.Response): Promise => { try { const projectId = process.env.GCLOUD_PROJECT; @@ -798,17 +862,20 @@ export function wrapHandler(eventType: AuthBlockingEventType, handler: HandlerV1 const decodedPayload: DecodedPayload = isDebugFeatureEnabled("skipTokenVerification") ? unsafeDecodeAuthBlockingToken(req.body.data.jwt) - : handler.length === 2 + : handler.platform === "gcfv1" ? await auth.getAuth(getApp())._verifyAuthBlockingToken(req.body.data.jwt) : await auth.getAuth(getApp())._verifyAuthBlockingToken(req.body.data.jwt, "run.app"); - - const authUserRecord = parseAuthUserRecord(decodedPayload.user_record); + let authUserRecord: AuthUserRecord | undefined; + if (decodedPayload.user_record) { + authUserRecord = parseAuthUserRecord(decodedPayload.user_record); + } const authEventContext = parseAuthEventContext(decodedPayload, projectId); let authResponse; - if (handler.length === 2) { - authResponse = - (await (handler as HandlerV1)(authUserRecord, authEventContext)) || undefined; + if (handler.platform === "gcfv1") { + authResponse = authUserRecord + ? (await (handler as HandlerV1)(authUserRecord, authEventContext)) || undefined + : (await (handler as HandlerV1)(authEventContext)) || undefined; } else { authResponse = (await (handler as HandlerV2)({ @@ -818,16 +885,7 @@ export function wrapHandler(eventType: AuthBlockingEventType, handler: HandlerV1 } validateAuthResponse(eventType, authResponse); - const updateMask = getUpdateMask(authResponse); - const result = - updateMask.length === 0 - ? {} - : { - userRecord: { - ...authResponse, - updateMask, - }, - }; + const result = generateResponsePayload(authResponse); res.status(200); res.setHeader("Content-Type", "application/json"); diff --git a/src/v1/providers/auth.ts b/src/v1/providers/auth.ts index edef7b0bb..c4435b798 100644 --- a/src/v1/providers/auth.ts +++ b/src/v1/providers/auth.ts @@ -25,8 +25,12 @@ import { AuthEventContext, AuthUserRecord, BeforeCreateResponse, + BeforeEmailResponse, BeforeSignInResponse, + AgnosticHandler, + HandlerV1, HttpsError, + MaybeAsync, UserInfo, UserRecord, userRecordConstructor, @@ -151,7 +155,7 @@ export class UserBuilder { handler: ( user: AuthUserRecord, context: AuthEventContext - ) => BeforeCreateResponse | void | Promise | Promise + ) => MaybeAsync ): BlockingFunction { return this.beforeOperation(handler, "beforeCreate"); } @@ -167,11 +171,17 @@ export class UserBuilder { handler: ( user: AuthUserRecord, context: AuthEventContext - ) => BeforeSignInResponse | void | Promise | Promise + ) => MaybeAsync ): BlockingFunction { return this.beforeOperation(handler, "beforeSignIn"); } + beforeEmail( + handler: (context: AuthEventContext) => MaybeAsync + ): BlockingFunction { + return this.beforeOperation(handler, "beforeSendEmail"); + } + private onOperation( handler: (user: UserRecord, context: EventContext) => PromiseLike | any, eventType: string @@ -189,28 +199,13 @@ export class UserBuilder { }); } - private beforeOperation( - handler: ( - user: AuthUserRecord, - context: AuthEventContext - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise, - eventType: AuthBlockingEventType - ): BlockingFunction { + private beforeOperation(handler: HandlerV1, eventType: AuthBlockingEventType): BlockingFunction { const accessToken = this.userOptions?.blockingOptions?.accessToken || false; const idToken = this.userOptions?.blockingOptions?.idToken || false; const refreshToken = this.userOptions?.blockingOptions?.refreshToken || false; - // Create our own function that just calls the provided function so we know for sure that - // handler takes two arguments. This is something common/providers/identity depends on. - const wrappedHandler = (user: AuthUserRecord, context: AuthEventContext) => - handler(user, context); - const func: any = wrapHandler(eventType, wrappedHandler); + const annotatedHandler: AgnosticHandler = Object.assign(handler, { platform: "gcfv1" }); + const func: any = wrapHandler(eventType, annotatedHandler); const legacyEventType = `providers/cloud.auth/eventTypes/user.${eventType}`; diff --git a/src/v2/providers/identity.ts b/src/v2/providers/identity.ts index 3a0b1b7fc..5f0876bd5 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -31,8 +31,12 @@ import { AuthUserRecord, BeforeCreateResponse, BeforeSignInResponse, + BeforeEmailResponse, + HandlerV2, HttpsError, wrapHandler, + MaybeAsync, + AgnosticHandler, } from "../../common/providers/identity"; import { BlockingFunction } from "../../v1/cloud-functions"; import { wrapTraceContext } from "../trace"; @@ -165,9 +169,7 @@ export interface BlockingOptions { * @param handler - Event handler which is run every time before a user is created */ export function beforeUserCreated( - handler: ( - event: AuthBlockingEvent - ) => BeforeCreateResponse | Promise | void | Promise + handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; /** @@ -177,9 +179,7 @@ export function beforeUserCreated( */ export function beforeUserCreated( opts: BlockingOptions, - handler: ( - event: AuthBlockingEvent - ) => BeforeCreateResponse | Promise | void | Promise + handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; /** @@ -190,12 +190,8 @@ export function beforeUserCreated( export function beforeUserCreated( optsOrHandler: | BlockingOptions - | (( - event: AuthBlockingEvent - ) => BeforeCreateResponse | Promise | void | Promise), - handler?: ( - event: AuthBlockingEvent - ) => BeforeCreateResponse | Promise | void | Promise + | ((event: AuthBlockingEvent) => MaybeAsync), + handler?: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction { return beforeOperation("beforeCreate", optsOrHandler, handler); } @@ -205,9 +201,7 @@ export function beforeUserCreated( * @param handler - Event handler which is run every time before a user is signed in */ export function beforeUserSignedIn( - handler: ( - event: AuthBlockingEvent - ) => BeforeSignInResponse | Promise | void | Promise + handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; /** @@ -217,9 +211,7 @@ export function beforeUserSignedIn( */ export function beforeUserSignedIn( opts: BlockingOptions, - handler: ( - event: AuthBlockingEvent - ) => BeforeSignInResponse | Promise | void | Promise + handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; /** @@ -230,16 +222,44 @@ export function beforeUserSignedIn( export function beforeUserSignedIn( optsOrHandler: | BlockingOptions - | (( - event: AuthBlockingEvent - ) => BeforeSignInResponse | Promise | void | Promise), - handler?: ( - event: AuthBlockingEvent - ) => BeforeSignInResponse | Promise | void | Promise + | ((event: AuthBlockingEvent) => MaybeAsync), + handler?: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction { return beforeOperation("beforeSignIn", optsOrHandler, handler); } +/** + * Handles an event that is triggered before an email is sent to a user. + * @param handler - Event handler that is run before an email is sent to a user. + */ +export function beforeEmailSent( + handler: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction; + +/** + * Handles an event that is triggered before an email is sent to a user. + * @param opts - Object containing function options + * @param handler - Event handler that is run before an email is sent to a user. + */ +export function beforeEmailSent( + opts: BlockingOptions, + handler: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction; + +/** + * Handles an event that is triggered before an email is sent to a user. + * @param optsOrHandler- Either an object containing function options, or an event handler that is run before an email is sent to a user. + * @param handler - Event handler that is run before an email is sent to a user. + */ +export function beforeEmailSent( + optsOrHandler: + | BlockingOptions + | ((event: AuthBlockingEvent) => MaybeAsync), + handler?: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction { + return beforeOperation("beforeSendEmail", optsOrHandler, handler); +} + /** @hidden */ export function beforeOperation( eventType: AuthBlockingEventType, @@ -247,33 +267,13 @@ export function beforeOperation( | BlockingOptions | (( event: AuthBlockingEvent - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise), - handler: ( - event: AuthBlockingEvent - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise + ) => MaybeAsync), + handler: HandlerV2 ): BlockingFunction { if (!handler || typeof optsOrHandler === "function") { handler = optsOrHandler as ( event: AuthBlockingEvent - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; + ) => BeforeEmailResponse | void | Promise | Promise; optsOrHandler = {}; } @@ -281,8 +281,9 @@ export function beforeOperation( // Create our own function that just calls the provided function so we know for sure that // handler takes one argument. This is something common/providers/identity depends on. - const wrappedHandler = (event: AuthBlockingEvent) => handler(event); - const func: any = wrapTraceContext(wrapHandler(eventType, wrappedHandler)); + // const wrappedHandler = (event: AuthBlockingEvent) => handler(event); + const annotatedHandler: AgnosticHandler = Object.assign(handler, { platform: "gcfv2" }); + const func: any = wrapTraceContext(wrapHandler(eventType, annotatedHandler)); const legacyEventType = `providers/cloud.auth/eventTypes/user.${eventType}`;