diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..51926d296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add support for beforeSmsSent auth blocking triggers. (#1589) diff --git a/spec/common/providers/identity.spec.ts b/spec/common/providers/identity.spec.ts index cfbaca770..253a337b2 100644 --- a/spec/common/providers/identity.spec.ts +++ b/spec/common/providers/identity.spec.ts @@ -528,6 +528,8 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: EVENT, + emailType: undefined, + smsType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -540,6 +542,8 @@ describe("identity", () => { username: undefined, isNewUser: false, recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, + phoneNumber: undefined, }, credential: null, params: {}, @@ -577,6 +581,8 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeSignIn:password", + emailType: undefined, + smsType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -589,6 +595,8 @@ describe("identity", () => { username: undefined, isNewUser: false, recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, + phoneNumber: undefined, }, credential: { claims: undefined, @@ -663,6 +671,8 @@ describe("identity", () => { userAgent: "USER_AGENT", eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeCreate:oidc.provider", + emailType: undefined, + smsType: undefined, authType: "USER", resource: { service: "identitytoolkit.googleapis.com", @@ -675,6 +685,8 @@ describe("identity", () => { profile: rawUserInfo, isNewUser: true, recaptchaScore: TEST_RECAPTCHA_SCORE, + email: undefined, + phoneNumber: undefined, }, credential: { claims: undefined, @@ -691,6 +703,98 @@ 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", + smsType: undefined, + 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", + phoneNumber: undefined, + }, + credential: null, + params: {}, + }; + + expect(identity.parseAuthEventContext(decodedJwt, "project-id", time)).to.deep.equal(context); + }); + + it("should parse a beforeSendSms 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: "beforeSendSms", + user_agent: "USER_AGENT", + ip_address: "1.2.3.4", + locale: "en", + recaptcha_score: TEST_RECAPTCHA_SCORE, + sms_type: "SIGN_IN_OR_SIGN_UP", + phone_number: "+11234567890", + }; + const context = { + locale: "en", + ipAddress: "1.2.3.4", + userAgent: "USER_AGENT", + eventId: "EVENT_ID", + eventType: "providers/cloud.auth/eventTypes/user.beforeSendSms", + emailType: undefined, + smsType: "SIGN_IN_OR_SIGN_UP", + 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: undefined, + phoneNumber: "+11234567890", + }, + 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..ec1a793f5 100644 --- a/spec/v1/providers/auth.spec.ts +++ b/spec/v1/providers/auth.spec.ts @@ -305,6 +305,186 @@ 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("beforeSms", () => { + it("should create function without options", () => { + const fn = auth.user().beforeSms(() => Promise.resolve()); + + expect(fn.__trigger).to.deep.equal({ + labels: {}, + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendSms", + 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.beforeSendSms", + 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, + }, + }) + .beforeSms(() => Promise.resolve()); + + expect(fn.__trigger).to.deep.equal({ + labels: {}, + regions: ["us-east1"], + availableMemoryMb: 256, + timeout: "90s", + blockingTrigger: { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendSms", + 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.beforeSendSms", + 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 bcd416d29..dbda1189c 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: { @@ -44,11 +47,21 @@ const BEFORE_SIGN_IN_TRIGGER = { }, }; +const BEFORE_EMAIL_TRIGGER = { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", + options: {}, +}; + +const BEFORE_SMS_TRIGGER = { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendSms", + options: {}, +}; + const opts: identity.BlockingOptions = { accessToken: true, refreshToken: false, minInstances: 1, - region: "us-west1", + region: REGION, }; describe("identity", () => { @@ -64,7 +77,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -78,7 +91,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_CREATE_TRIGGER, options: { @@ -89,7 +102,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -129,7 +142,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -143,7 +156,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_SIGN_IN_TRIGGER, options: { @@ -154,7 +167,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -182,6 +195,92 @@ 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: IDENTITY_TOOLKIT_API, + reason: "Needed for auth blocking functions", + }, + ]); + }); + + 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", + }, + ]); + }); + }); + + describe("beforeSmsSent", () => { + it("should accept a handler", () => { + const fn = identity.beforeSmsSent(() => Promise.resolve()); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + blockingTrigger: BEFORE_SMS_TRIGGER, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: IDENTITY_TOOLKIT_API, + reason: "Needed for auth blocking functions", + }, + ]); + }); + + it("should accept options and a handler", () => { + const fn = identity.beforeSmsSent( + { 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_SMS_TRIGGER, + }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + api: IDENTITY_TOOLKIT_API, + 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); @@ -194,7 +293,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -211,12 +310,45 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, + reason: "Needed for auth blocking functions", + }, + ]); + }); + + 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: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); }); + 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: IDENTITY_TOOLKIT_API, + 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()); @@ -225,7 +357,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_CREATE_TRIGGER, options: { @@ -236,7 +368,7 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, reason: "Needed for auth blocking functions", }, ]); @@ -250,7 +382,7 @@ describe("identity", () => { platform: "gcfv2", labels: {}, minInstances: 1, - region: ["us-west1"], + region: [REGION], blockingTrigger: { ...BEFORE_SIGN_IN_TRIGGER, options: { @@ -261,7 +393,28 @@ describe("identity", () => { }); expect(fn.__requiredAPIs).to.deep.equal([ { - api: "identitytoolkit.googleapis.com", + api: IDENTITY_TOOLKIT_API, + reason: "Needed for auth blocking functions", + }, + ]); + }); + + 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: [REGION], + blockingTrigger: { + ...BEFORE_EMAIL_TRIGGER, + }, + }); + expect(fn.__requiredAPIs).to.deep.equal([ + { + 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 5e9551bb7..f2a8a3949 100644 --- a/src/common/providers/identity.ts +++ b/src/common/providers/identity.ts @@ -55,11 +55,17 @@ const CLAIMS_MAX_PAYLOAD_SIZE = 1000; * @hidden * @alpha */ -export type AuthBlockingEventType = "beforeCreate" | "beforeSignIn"; +export type AuthBlockingEventType = + | "beforeCreate" + | "beforeSignIn" + | "beforeSendEmail" + | "beforeSendSms"; 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", + beforeSendSms: "providers/cloud.auth/eventTypes/user.beforeSendSms", }; /** @@ -307,11 +313,13 @@ 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; + phoneNumber?: string; } /** The credential component of the auth event context */ @@ -326,6 +334,21 @@ export interface Credential { signInMethod: string; } +/** + * Possible types of emails as described by the GCIP backend, which can be: + * - A sign-in email + * - A password reset email + */ +export type EmailType = "EMAIL_SIGN_IN" | "PASSWORD_RESET"; + +/** + * The type of SMS message, which can be: + * - A sign-in or sign up SMS message + * - A multi-factor sign-in SMS message + * - A multi-factor enrollment SMS message + */ +export type SmsType = "SIGN_IN_OR_SIGN_UP" | "MULTI_FACTOR_SIGN_IN" | "MULTI_FACTOR_ENROLLMENT"; + /** Defines the auth event context for blocking events */ export interface AuthEventContext extends EventContext { locale?: string; @@ -333,18 +356,28 @@ export interface AuthEventContext extends EventContext { userAgent: string; additionalUserInfo?: AdditionalUserInfo; credential?: Credential; + emailType?: EmailType; + smsType?: SmsType; } /** 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. - */ +/** The reCAPTCHA action options. */ export type RecaptchaActionOptions = "ALLOW" | "BLOCK"; +/** The handler response type for `beforeEmailSent` blocking events */ +export interface BeforeEmailResponse { + recaptchaActionOverride?: RecaptchaActionOptions; +} + +/** The handler response type for `beforeSmsSent` blocking events */ +export interface BeforeSmsResponse { + recaptchaActionOverride?: RecaptchaActionOptions; +} + /** The handler response type for `beforeCreate` blocking events */ export interface BeforeCreateResponse { displayName?: string; @@ -413,7 +446,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 +465,10 @@ export interface DecodedPayload { oauth_token_secret?: string; oauth_expires_in?: number; recaptcha_score?: number; + email?: string; + email_type?: string; + phone_number?: string; + sms_type?: string; [key: string]: any; } @@ -439,7 +476,7 @@ export interface DecodedPayload { * 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 */ + @internal */ export interface ResponsePayload { userRecord?: UserRecordResponsePayload; recaptchaActionOverride?: RecaptchaActionOptions; @@ -451,26 +488,28 @@ export interface UserRecordResponsePayload updateMask?: string; } -type HandlerV1 = ( - user: AuthUserRecord, - context: AuthEventContext -) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; +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< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void +>; -type HandlerV2 = ( +export type HandlerV2 = ( event: AuthBlockingEvent -) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; +) => MaybeAsync< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void +>; + +export type AuthBlockingEventHandler = (HandlerV1 | HandlerV2) & { + // Specify the GCF gen of the trigger that the auth blocking event handler was written for + platform: "gcfv1" | "gcfv2"; +}; /** * Checks for a valid identity platform web request, otherwise throws an HttpsError. @@ -666,6 +705,8 @@ function parseAdditionalUserInfo(decodedJWT: DecodedPayload): AdditionalUserInfo username, isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false, recaptchaScore: decodedJWT.recaptcha_score, + email: decodedJWT.email, + phoneNumber: decodedJWT.phone_number, }; } @@ -752,6 +793,8 @@ export function parseAuthEventContext( timestamp: new Date(decodedJWT.iat * 1000).toUTCString(), additionalUserInfo: parseAdditionalUserInfo(decodedJWT), credential: parseAuthCredential(decodedJWT, time), + emailType: decodedJWT.email_type as EmailType, + smsType: decodedJWT.sms_type as SmsType, params: {}, }; } @@ -836,7 +879,7 @@ export function getUpdateMask(authResponse?: BeforeCreateResponse | BeforeSignIn } /** @internal */ -export function wrapHandler(eventType: AuthBlockingEventType, handler: HandlerV1 | HandlerV2) { +export function wrapHandler(eventType: AuthBlockingEventType, handler: AuthBlockingEventHandler) { return async (req: express.Request, res: express.Response): Promise => { try { const projectId = process.env.GCLOUD_PROJECT; @@ -853,16 +896,23 @@ 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.event_type === "beforeCreate" || + decodedPayload.event_type === "beforeSignIn" + ) { + 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..2a88cd41a 100644 --- a/src/v1/providers/auth.ts +++ b/src/v1/providers/auth.ts @@ -25,8 +25,12 @@ import { AuthEventContext, AuthUserRecord, BeforeCreateResponse, + BeforeEmailResponse, BeforeSignInResponse, + BeforeSmsResponse, + 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,23 @@ 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"); + } + + beforeSms( + handler: (context: AuthEventContext) => MaybeAsync + ): BlockingFunction { + return this.beforeOperation(handler, "beforeSendSms"); + } + private onOperation( handler: (user: UserRecord, context: EventContext) => PromiseLike | any, eventType: string @@ -189,28 +205,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 = 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 aa93edc2f..e5b276516 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -31,8 +31,12 @@ import { AuthUserRecord, BeforeCreateResponse, BeforeSignInResponse, + BeforeEmailResponse, + BeforeSmsResponse, + HandlerV2, HttpsError, wrapHandler, + MaybeAsync, } from "../../common/providers/identity"; import { BlockingFunction } from "../../v1/cloud-functions"; import { wrapTraceContext } from "../trace"; @@ -163,84 +167,131 @@ export interface BlockingOptions { /** * Handles an event that is triggered before a user is created. - * @param handler - Event handler which is run every time before a user is created + * @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; /** * Handles an event that is triggered before a user is created. - * @param opts - Object containing function options - * @param handler - Event handler which is run every time before a user is created + * @param opts - Object containing function options. + * @param handler - Event handler which is run every time before a user is created. */ export function beforeUserCreated( opts: BlockingOptions, - handler: ( - event: AuthBlockingEvent - ) => BeforeCreateResponse | Promise | void | Promise + handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; /** - * Handles an event that is triggered before a user is created - * @param optsOrHandler - Either an object containing function options, or an event handler (run before user creation) - * @param handler? - If defined, an event handler which is run every time before a user is created + * Handles an event that is triggered before a user is created. + * @param optsOrHandler - Either an object containing function options, or an event handler (run before user creation). + * @param handler? - If defined, an event handler which is run every time before a user is created. */ 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); } /** * Handles an event that is triggered before a user is signed in. - * @param handler - Event handler which is run every time before a user is signed in + * @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; /** * Handles an event that is triggered before a user is signed in. - * @param opts - Object containing function options - * @param handler - Event handler which is run every time before a user is signed in + * @param opts - Object containing function options. + * @param handler - Event handler which is run every time before a user is signed in. */ export function beforeUserSignedIn( opts: BlockingOptions, - handler: ( - event: AuthBlockingEvent - ) => BeforeSignInResponse | Promise | void | Promise + handler: (event: AuthBlockingEvent) => MaybeAsync ): BlockingFunction; /** * Handles an event that is triggered before a user is signed in. - * @param optsOrHandler - Either an object containing function options, or an event handler (run before user signin) - * @param handler - Event handler which is run every time before a user is signed in + * @param optsOrHandler - Either an object containing function options, or an event handler (run before user signin). + * @param handler - Event handler which is run every time before a user is signed in. */ 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: Omit, + 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: + | Omit + | ((event: AuthBlockingEvent) => MaybeAsync), + handler?: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction { + return beforeOperation("beforeSendEmail", optsOrHandler, handler); +} +/** + * Handles an event that is triggered before an SMS is sent to a user. + * @param handler - Event handler that is run before an SMS is sent to a user. + */ +export function beforeSmsSent( + handler: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction; + +/** + * Handles an event that is triggered before an SMS is sent to a user. + * @param opts - Object containing function options. + * @param handler - Event handler that is run before an SMS is sent to a user. + */ +export function beforeSmsSent( + opts: Omit, + handler: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction; + +/** + * Handles an event that is triggered before an SMS is sent to a user. + * @param optsOrHandler - Either an object containing function options, or an event handler that is run before an SMS is sent to a user. + * @param handler - Event handler that is run before an SMS is sent to a user. + */ +export function beforeSmsSent( + optsOrHandler: + | Omit + | ((event: AuthBlockingEvent) => MaybeAsync), + handler?: (event: AuthBlockingEvent) => MaybeAsync +): BlockingFunction { + return beforeOperation("beforeSendSms", optsOrHandler, handler); +} + /** @hidden */ export function beforeOperation( eventType: AuthBlockingEventType, @@ -248,42 +299,26 @@ export function beforeOperation( | BlockingOptions | (( event: AuthBlockingEvent - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise), - handler: ( - event: AuthBlockingEvent - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise + ) => MaybeAsync< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void + >), + handler: HandlerV2 ): BlockingFunction { if (!handler || typeof optsOrHandler === "function") { handler = optsOrHandler as ( event: AuthBlockingEvent - ) => - | BeforeCreateResponse - | BeforeSignInResponse - | void - | Promise - | Promise - | Promise; + ) => MaybeAsync< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void + >; 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. - const wrappedHandler = (event: AuthBlockingEvent) => handler(event); - const func: any = wrapTraceContext(withInit(wrapHandler(eventType, wrappedHandler))); + 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}`; @@ -302,9 +337,7 @@ export function beforeOperation( blockingTrigger: { eventType: legacyEventType, options: { - accessToken, - idToken, - refreshToken, + ...((eventType === "beforeCreate" || eventType === "beforeSignIn") && blockingOptions), }, }, };