From 44e4b9202ac8559a98b7e0a66d298af2cbf466eb Mon Sep 17 00:00:00 2001 From: Brian Li Date: Tue, 28 Nov 2023 16:47:55 -0500 Subject: [PATCH 1/5] add support for beforeEmail trigger --- spec/common/providers/identity.spec.ts | 50 +++++++++++++ spec/v1/providers/auth.spec.ts | 90 +++++++++++++++++++++++ spec/v2/providers/identity.spec.ts | 95 ++++++++++++++++++++++++ src/common/providers/identity.ts | 78 +++++++++++--------- src/v1/providers/auth.ts | 35 ++++----- src/v2/providers/identity.ts | 99 +++++++++++++------------- 6 files changed, 346 insertions(+), 101 deletions(-) diff --git a/spec/common/providers/identity.spec.ts b/spec/common/providers/identity.spec.ts index cfbaca770..167befff3 100644 --- a/spec/common/providers/identity.spec.ts +++ b/spec/common/providers/identity.spec.ts @@ -528,6 +528,7 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: EVENT, + emailType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -540,6 +541,7 @@ describe("identity", () => { username: undefined, isNewUser: false, recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, }, credential: null, params: {}, @@ -577,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", @@ -589,6 +592,7 @@ describe("identity", () => { username: undefined, isNewUser: false, recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, }, credential: { claims: undefined, @@ -663,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", @@ -675,6 +680,7 @@ describe("identity", () => { profile: rawUserInfo, isNewUser: true, recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, }, credential: { claims: undefined, @@ -691,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", () => { 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 5e9551bb7..156e25ba8 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", }; /** @@ -307,11 +308,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 */ @@ -326,6 +328,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; @@ -333,19 +338,22 @@ export interface AuthEventContext extends EventContext { userAgent: string; additionalUserInfo?: AdditionalUserInfo; credential?: Credential; + emailType?: EmailType; } /** Defines the auth event for 2nd gen blocking events */ export interface AuthBlockingEvent extends AuthEventContext { - data: AuthUserRecord; + data?: AuthUserRecord; } -/** - * The reCAPTCHA action options. - */ +/** The reCAPTCHA action options. */ export type RecaptchaActionOptions = "ALLOW" | "BLOCK"; -/** The handler response type for `beforeCreate` blocking events */ +export interface BeforeEmailResponse { + recaptchaActionOverride?: RecaptchaActionOptions; +} + +/** The handler response type for beforeCreate blocking events */ export interface BeforeCreateResponse { displayName?: string; disabled?: boolean; @@ -391,6 +399,7 @@ interface DecodedPayloadUserRecordEnrolledFactors { export interface DecodedPayloadUserRecord { uid: string; email?: string; + email_type?: string; email_verified?: boolean; phone_number?: string; display_name?: string; @@ -413,7 +422,7 @@ export interface DecodedPayload { exp: number; iat: number; iss: string; - sub: string; + sub?: string; event_id: string; event_type: string; ip_address: string; @@ -432,6 +441,8 @@ export interface DecodedPayload { oauth_token_secret?: string; oauth_expires_in?: number; recaptcha_score?: number; + email?: string; + email_type?: string; [key: string]: any; } @@ -451,26 +462,23 @@ export interface UserRecordResponsePayload updateMask?: string; } -type HandlerV1 = ( - user: AuthUserRecord, - context: AuthEventContext -) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; +export type MaybeAsync = T | Promise; -type HandlerV2 = ( +// 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. @@ -666,6 +674,7 @@ function parseAdditionalUserInfo(decodedJWT: DecodedPayload): AdditionalUserInfo username, isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false, recaptchaScore: decodedJWT.recaptcha_score, + email: decodedJWT.email, }; } @@ -752,6 +761,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: {}, }; } @@ -836,7 +846,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; @@ -853,16 +863,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)({ 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}`; From 9f7b83bfe6e7c521eb194af560903decaf35517b Mon Sep 17 00:00:00 2001 From: Brian Li Date: Mon, 22 Jul 2024 17:45:13 -0500 Subject: [PATCH 2/5] remove opts from beforeemail & decode user record only for beforeCreate and beforeSignIn --- spec/v2/providers/identity.spec.ts | 47 +++++++++++++++--------------- src/common/providers/identity.ts | 14 +++++---- src/v1/providers/auth.ts | 3 +- src/v2/providers/identity.ts | 7 ++--- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/spec/v2/providers/identity.spec.ts b/spec/v2/providers/identity.spec.ts index 822231f18..d7ca6aeb2 100644 --- a/spec/v2/providers/identity.spec.ts +++ b/spec/v2/providers/identity.spec.ts @@ -26,6 +26,9 @@ import { onInit } from "../../../src/v2/core"; import { MockRequest } from "../../fixtures/mockrequest"; import { runHandler } from "../../helper"; +const IDENTITY_TOOLKIT_API = "identitytoolkit.googleapis.com"; +const REGION = "us-west1"; + const BEFORE_CREATE_TRIGGER = { eventType: "providers/cloud.auth/eventTypes/user.beforeCreate", options: { @@ -46,18 +49,14 @@ const BEFORE_SIGN_IN_TRIGGER = { const BEFORE_EMAIL_TRIGGER = { eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", - options: { - accessToken: false, - idToken: false, - refreshToken: false, - }, + options: {}, }; const opts: identity.BlockingOptions = { accessToken: true, refreshToken: false, minInstances: 1, - region: "us-west1", + region: REGION, }; describe("identity", () => { @@ -73,7 +72,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -87,7 +86,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_CREATE_TRIGGER, options: { @@ -98,7 +97,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -138,7 +137,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -152,7 +151,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_SIGN_IN_TRIGGER, options: { @@ -163,7 +162,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -203,7 +202,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -218,7 +217,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_EMAIL_TRIGGER, options: { @@ -229,7 +228,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -247,7 +246,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -264,7 +263,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -281,7 +280,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -295,7 +294,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_CREATE_TRIGGER, options: { @@ -306,7 +305,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -320,7 +319,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_SIGN_IN_TRIGGER, options: { @@ -331,7 +330,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -345,7 +344,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_EMAIL_TRIGGER, options: { @@ -356,7 +355,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); diff --git a/src/common/providers/identity.ts b/src/common/providers/identity.ts index 156e25ba8..bac3cb52e 100644 --- a/src/common/providers/identity.ts +++ b/src/common/providers/identity.ts @@ -349,6 +349,7 @@ export interface AuthBlockingEvent extends AuthEventContext { /** The reCAPTCHA action options. */ export type RecaptchaActionOptions = "ALLOW" | "BLOCK"; +/** The handler response type for `beforeEmailSent` blocking events */ export interface BeforeEmailResponse { recaptchaActionOverride?: RecaptchaActionOptions; } @@ -399,7 +400,6 @@ interface DecodedPayloadUserRecordEnrolledFactors { export interface DecodedPayloadUserRecord { uid: string; email?: string; - email_type?: string; email_verified?: boolean; phone_number?: string; display_name?: string; @@ -476,8 +476,9 @@ export type HandlerV2 = ( event: AuthBlockingEvent ) => MaybeAsync; -export type AgnosticHandler = (HandlerV1 | HandlerV2) & { - platform: string; +export type AuthBlockingEventHandler = (HandlerV1 | HandlerV2) & { + // Specify the GCF gen of the trigger that the auth blocking event handler was written for + platform: "gcfv1" | "gcfv2"; }; /** @@ -846,7 +847,7 @@ export function getUpdateMask(authResponse?: BeforeCreateResponse | BeforeSignIn } /** @internal */ -export function wrapHandler(eventType: AuthBlockingEventType, handler: AgnosticHandler) { +export function wrapHandler(eventType: AuthBlockingEventType, handler: AuthBlockingEventHandler) { return async (req: express.Request, res: express.Response): Promise => { try { const projectId = process.env.GCLOUD_PROJECT; @@ -867,7 +868,10 @@ export function wrapHandler(eventType: AuthBlockingEventType, handler: AgnosticH ? await auth.getAuth(getApp())._verifyAuthBlockingToken(req.body.data.jwt) : await auth.getAuth(getApp())._verifyAuthBlockingToken(req.body.data.jwt, "run.app"); let authUserRecord: AuthUserRecord | undefined; - if (decodedPayload.user_record) { + if ( + decodedPayload.event_type === "beforeCreate" || + decodedPayload.event_type === "beforeSignIn" + ) { authUserRecord = parseAuthUserRecord(decodedPayload.user_record); } const authEventContext = parseAuthEventContext(decodedPayload, projectId); diff --git a/src/v1/providers/auth.ts b/src/v1/providers/auth.ts index c4435b798..efab2f488 100644 --- a/src/v1/providers/auth.ts +++ b/src/v1/providers/auth.ts @@ -27,7 +27,6 @@ import { BeforeCreateResponse, BeforeEmailResponse, BeforeSignInResponse, - AgnosticHandler, HandlerV1, HttpsError, MaybeAsync, @@ -204,7 +203,7 @@ export class UserBuilder { const idToken = this.userOptions?.blockingOptions?.idToken || false; const refreshToken = this.userOptions?.blockingOptions?.refreshToken || false; - const annotatedHandler: AgnosticHandler = Object.assign(handler, { platform: "gcfv1" }); + const annotatedHandler = Object.assign(handler, { platform: "gcfv1" as const }); 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 ffeac8952..737976ac3 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -36,7 +36,6 @@ import { HttpsError, wrapHandler, MaybeAsync, - AgnosticHandler, } from "../../common/providers/identity"; import { BlockingFunction } from "../../v1/cloud-functions"; import { wrapTraceContext } from "../trace"; @@ -243,7 +242,7 @@ export function beforeEmailSent( * @param handler - Event handler that is run before an email is sent to a user. */ export function beforeEmailSent( - opts: BlockingOptions, + opts: Omit, handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; @@ -254,7 +253,7 @@ export function beforeEmailSent( */ export function beforeEmailSent( optsOrHandler: - | BlockingOptions + | Omit | ((event: AuthBlockingEvent) => MaybeAsync), handler?: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction { @@ -283,7 +282,7 @@ 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 annotatedHandler: AgnosticHandler = Object.assign(handler, { platform: "gcfv2" }); + const annotatedHandler = Object.assign(handler, { platform: "gcfv2" as const }); const func: any = wrapTraceContext(withInit(wrapHandler(eventType, annotatedHandler))); const legacyEventType = `providers/cloud.auth/eventTypes/user.${eventType}`; From 5919c318706291fe5b702ee570ad74dcf59a512f Mon Sep 17 00:00:00 2001 From: Brian Li Date: Mon, 22 Jul 2024 19:17:31 -0500 Subject: [PATCH 3/5] fix unit tests --- spec/v2/providers/identity.spec.ts | 47 +++++++++++++----------------- src/v2/providers/identity.ts | 8 ++--- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/spec/v2/providers/identity.spec.ts b/spec/v2/providers/identity.spec.ts index d7ca6aeb2..5d1a7aee0 100644 --- a/spec/v2/providers/identity.spec.ts +++ b/spec/v2/providers/identity.spec.ts @@ -207,31 +207,30 @@ describe("identity", () => { }, ]); }); - }); - 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: [REGION], - blockingTrigger: { - ...BEFORE_EMAIL_TRIGGER, - options: { - ...BEFORE_EMAIL_TRIGGER.options, - accessToken: true, + it("should accept options and a handler", () => { + const fn = identity.beforeEmailSent( + { region: opts.region, minInstances: opts.minInstances }, + () => Promise.resolve() + ); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + minInstances: 1, + region: [REGION], + blockingTrigger: { + ...BEFORE_EMAIL_TRIGGER, }, - }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: IDENTITY_TOOLKIT_API, + reason: "Needed for auth blocking functions", + }, + ]); }); - expect(fn.__requiredAPIs).to.deep.equal([ - { - api: IDENTITY_TOOLKIT_API, - reason: "Needed for auth blocking functions", - }, - ]); }); describe("beforeOperation", () => { @@ -347,10 +346,6 @@ describe("identity", () => { region: [REGION], blockingTrigger: { ...BEFORE_EMAIL_TRIGGER, - options: { - ...BEFORE_EMAIL_TRIGGER.options, - accessToken: true, - }, }, }); expect(fn.__requiredAPIs).to.deep.equal([ diff --git a/src/v2/providers/identity.ts b/src/v2/providers/identity.ts index 737976ac3..2829cd349 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -273,11 +273,11 @@ export function beforeOperation( if (!handler || typeof optsOrHandler === "function") { handler = optsOrHandler as ( event: AuthBlockingEvent - ) => BeforeEmailResponse | void | Promise | Promise; + ) => MaybeAsync; optsOrHandler = {}; } - const { opts, accessToken, idToken, refreshToken } = getOpts(optsOrHandler); + const { opts, ...blockingOptions } = getOpts(optsOrHandler); // 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. @@ -302,9 +302,7 @@ export function beforeOperation( blockingTrigger: { eventType: legacyEventType, options: { - accessToken, - idToken, - refreshToken, + ...((eventType === "beforeCreate" || eventType === "beforeSignIn") && blockingOptions), }, }, }; From b63afc98c4545fb50f83e3352bcb07e792e5ec42 Mon Sep 17 00:00:00 2001 From: Brian Li Date: Mon, 22 Jul 2024 19:18:29 -0500 Subject: [PATCH 4/5] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..3fc76447f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add support for beforeEmailSent auth blocking triggers. (#1492) From 9f7b42f334441f3577aa112a8f29d768a1fa7b69 Mon Sep 17 00:00:00 2001 From: Brian Li Date: Tue, 6 Aug 2024 15:23:20 -0400 Subject: [PATCH 5/5] add comment to authblockingevent type --- src/common/providers/identity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/providers/identity.ts b/src/common/providers/identity.ts index bac3cb52e..e4373d43a 100644 --- a/src/common/providers/identity.ts +++ b/src/common/providers/identity.ts @@ -343,7 +343,7 @@ export interface AuthEventContext extends EventContext { /** Defines the auth event for 2nd gen blocking events */ export interface AuthBlockingEvent extends AuthEventContext { - data?: AuthUserRecord; + data?: AuthUserRecord; // will be undefined for beforeEmailSent and beforeSmsSent event types } /** The reCAPTCHA action options. */