diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc76447f..51926d296 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -- Add support for beforeEmailSent auth blocking triggers. (#1492) +- 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 167befff3..253a337b2 100644 --- a/spec/common/providers/identity.spec.ts +++ b/spec/common/providers/identity.spec.ts @@ -529,6 +529,7 @@ describe("identity", () => { eventId: "EVENT_ID", eventType: EVENT, emailType: undefined, + smsType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -542,6 +543,7 @@ describe("identity", () => { isNewUser: false, recaptchaScore: TEST_RECAPTCHA_SCORE, email: undefined, + phoneNumber: undefined, }, credential: null, params: {}, @@ -580,6 +582,7 @@ describe("identity", () => { eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeSignIn:password", emailType: undefined, + smsType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -593,6 +596,7 @@ describe("identity", () => { isNewUser: false, recaptchaScore: TEST_RECAPTCHA_SCORE, email: undefined, + phoneNumber: undefined, }, credential: { claims: undefined, @@ -668,6 +672,7 @@ describe("identity", () => { eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeCreate:oidc.provider", emailType: undefined, + smsType: undefined, authType: "USER", resource: { service: "identitytoolkit.googleapis.com", @@ -681,6 +686,7 @@ describe("identity", () => { isNewUser: true, recaptchaScore: TEST_RECAPTCHA_SCORE, email: undefined, + phoneNumber: undefined, }, credential: { claims: undefined, @@ -721,6 +727,7 @@ describe("identity", () => { eventId: "EVENT_ID", eventType: "providers/cloud.auth/eventTypes/user.beforeSendEmail", emailType: "RESET_PASSWORD", + smsType: undefined, authType: "UNAUTHENTICATED", resource: { service: "identitytoolkit.googleapis.com", @@ -734,6 +741,53 @@ describe("identity", () => { 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: {}, diff --git a/spec/v1/providers/auth.spec.ts b/spec/v1/providers/auth.spec.ts index 6901e3fdf..ec1a793f5 100644 --- a/spec/v1/providers/auth.spec.ts +++ b/spec/v1/providers/auth.spec.ts @@ -395,6 +395,96 @@ describe("Auth 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 5d1a7aee0..7984d085f 100644 --- a/spec/v2/providers/identity.spec.ts +++ b/spec/v2/providers/identity.spec.ts @@ -52,6 +52,11 @@ const BEFORE_EMAIL_TRIGGER = { options: {}, }; +const BEFORE_SMS_TRIGGER = { + eventType: "providers/cloud.auth/eventTypes/user.beforeSendSms", + options: {}, +}; + const opts: identity.BlockingOptions = { accessToken: true, refreshToken: false, @@ -233,6 +238,49 @@ describe("identity", () => { }); }); + 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); @@ -285,6 +333,23 @@ describe("identity", () => { ]); }); + it("should handle eventType and handler for before SMS events", () => { + const fn = identity.beforeOperation("beforeSendSms", () => Promise.resolve(), undefined); + + 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 handle eventType, options, and handler for before create events", () => { const fn = identity.beforeOperation("beforeCreate", opts, () => Promise.resolve()); @@ -355,6 +420,27 @@ describe("identity", () => { }, ]); }); + + it("should handle eventType, options, and handler for before send SMS events", () => { + const fn = identity.beforeOperation("beforeSendSms", opts, () => 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("getOpts", () => { diff --git a/src/common/providers/identity.ts b/src/common/providers/identity.ts index bac3cb52e..ee51a5dce 100644 --- a/src/common/providers/identity.ts +++ b/src/common/providers/identity.ts @@ -55,12 +55,17 @@ const CLAIMS_MAX_PAYLOAD_SIZE = 1000; * @hidden * @alpha */ -export type AuthBlockingEventType = "beforeCreate" | "beforeSignIn" | "beforeSendEmail"; +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", }; /** @@ -314,6 +319,7 @@ export interface AdditionalUserInfo { isNewUser: boolean; recaptchaScore?: number; email?: string; + phoneNumber?: string; } /** The credential component of the auth event context */ @@ -331,6 +337,14 @@ export interface Credential { /** Possible types of emails as described by the GCIP backend. */ export type EmailType = "EMAIL_SIGNIN" | "PASSWORD_RESET"; +/** + * The type of SMS message + */ +export type SmsType = + | "SIGN_IN_OR_SIGN_UP" // A sign-in or sign up SMS message + | "MULTI_FACTOR_SIGN_IN" // A multi-factor sign-in SMS message + | "MULTI_FACTOR_ENROLLMENT"; // A multi-factor enrollment SMS message + /** Defines the auth event context for blocking events */ export interface AuthEventContext extends EventContext { locale?: string; @@ -339,6 +353,7 @@ export interface AuthEventContext extends EventContext { additionalUserInfo?: AdditionalUserInfo; credential?: Credential; emailType?: EmailType; + smsType?: SmsType; } /** Defines the auth event for 2nd gen blocking events */ @@ -354,6 +369,11 @@ 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; @@ -443,6 +463,8 @@ export interface DecodedPayload { recaptcha_score?: number; email?: string; email_type?: string; + phone_number?: string; + sms_type?: string; [key: string]: any; } @@ -470,11 +492,15 @@ export type MaybeAsync = T | Promise; export type HandlerV1 = ( userOrContext: AuthUserRecord | AuthEventContext, context?: AuthEventContext -) => MaybeAsync; +) => MaybeAsync< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void +>; export type HandlerV2 = ( event: AuthBlockingEvent -) => MaybeAsync; +) => 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 @@ -676,6 +702,7 @@ function parseAdditionalUserInfo(decodedJWT: DecodedPayload): AdditionalUserInfo isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false, recaptchaScore: decodedJWT.recaptcha_score, email: decodedJWT.email, + phoneNumber: decodedJWT.phone_number, }; } @@ -763,6 +790,7 @@ export function parseAuthEventContext( additionalUserInfo: parseAdditionalUserInfo(decodedJWT), credential: parseAuthCredential(decodedJWT, time), emailType: decodedJWT.email_type as EmailType, + smsType: decodedJWT.sms_type as SmsType, params: {}, }; } diff --git a/src/v1/providers/auth.ts b/src/v1/providers/auth.ts index efab2f488..2a88cd41a 100644 --- a/src/v1/providers/auth.ts +++ b/src/v1/providers/auth.ts @@ -27,6 +27,7 @@ import { BeforeCreateResponse, BeforeEmailResponse, BeforeSignInResponse, + BeforeSmsResponse, HandlerV1, HttpsError, MaybeAsync, @@ -181,6 +182,12 @@ export class UserBuilder { 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 diff --git a/src/v2/providers/identity.ts b/src/v2/providers/identity.ts index 2829cd349..01ece673b 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -32,6 +32,7 @@ import { BeforeCreateResponse, BeforeSignInResponse, BeforeEmailResponse, + BeforeSmsResponse, HandlerV2, HttpsError, wrapHandler, @@ -259,6 +260,37 @@ export function beforeEmailSent( ): 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( @@ -267,13 +299,17 @@ export function beforeOperation( | BlockingOptions | (( event: AuthBlockingEvent - ) => MaybeAsync), + ) => MaybeAsync< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void + >), handler: HandlerV2 ): BlockingFunction { if (!handler || typeof optsOrHandler === "function") { handler = optsOrHandler as ( event: AuthBlockingEvent - ) => MaybeAsync; + ) => MaybeAsync< + BeforeCreateResponse | BeforeSignInResponse | BeforeEmailResponse | BeforeSmsResponse | void + >; optsOrHandler = {}; }