Skip to content

Commit b897b0d

Browse files
supporting recaptcha verdict for auth blocking functions (#1458)
Added recaptcha support in auth blocking functions beforeCreate and beforeSignIn. This allows developers to see the recaptcha scores for authentication actions and override the recaptcha actions.
1 parent 2841ebd commit b897b0d

File tree

2 files changed

+124
-22
lines changed

2 files changed

+124
-22
lines changed

spec/common/providers/identity.spec.ts

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import * as identity from "../../../src/common/providers/identity";
2626

2727
const EVENT = "EVENT_TYPE";
2828
const now = new Date();
29+
const TEST_NAME = "John Doe";
30+
const ALLOW = "ALLOW";
31+
const BLOCK = "BLOCK";
2932

3033
describe("identity", () => {
3134
describe("userRecordConstructor", () => {
@@ -232,14 +235,14 @@ describe("identity", () => {
232235
describe("parseProviderData", () => {
233236
const decodedUserInfo = {
234237
provider_id: "google.com",
235-
display_name: "John Doe",
238+
display_name: TEST_NAME,
236239
photo_url: "https://lh3.googleusercontent.com/1234567890/photo.jpg",
237240
uid: "1234567890",
238241
239242
};
240243
const userInfo = {
241244
providerId: "google.com",
242-
displayName: "John Doe",
245+
displayName: TEST_NAME,
243246
photoURL: "https://lh3.googleusercontent.com/1234567890/photo.jpg",
244247
uid: "1234567890",
245248
@@ -340,12 +343,12 @@ describe("identity", () => {
340343
uid: "abcdefghijklmnopqrstuvwxyz",
341344
342345
email_verified: true,
343-
display_name: "John Doe",
346+
display_name: TEST_NAME,
344347
phone_number: "+11234567890",
345348
provider_data: [
346349
{
347350
provider_id: "google.com",
348-
display_name: "John Doe",
351+
display_name: TEST_NAME,
349352
photo_url: "https://lh3.googleusercontent.com/1234567890/photo.jpg",
350353
351354
uid: "1234567890",
@@ -366,7 +369,7 @@ describe("identity", () => {
366369
provider_id: "password",
367370
368371
369-
display_name: "John Doe",
372+
display_name: TEST_NAME,
370373
},
371374
],
372375
password_hash: "passwordHash",
@@ -407,11 +410,11 @@ describe("identity", () => {
407410
phoneNumber: "+11234567890",
408411
emailVerified: true,
409412
disabled: false,
410-
displayName: "John Doe",
413+
displayName: TEST_NAME,
411414
providerData: [
412415
{
413416
providerId: "google.com",
414-
displayName: "John Doe",
417+
displayName: TEST_NAME,
415418
photoURL: "https://lh3.googleusercontent.com/1234567890/photo.jpg",
416419
417420
uid: "1234567890",
@@ -435,7 +438,7 @@ describe("identity", () => {
435438
},
436439
{
437440
providerId: "password",
438-
displayName: "John Doe",
441+
displayName: TEST_NAME,
439442
photoURL: undefined,
440443
441444
@@ -489,8 +492,9 @@ describe("identity", () => {
489492
});
490493

491494
describe("parseAuthEventContext", () => {
495+
const TEST_RECAPTCHA_SCORE = 0.9;
492496
const rawUserInfo = {
493-
name: "John Doe",
497+
name: TEST_NAME,
494498
granted_scopes:
495499
"openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
496500
id: "123456789",
@@ -516,6 +520,7 @@ describe("identity", () => {
516520
user_agent: "USER_AGENT",
517521
locale: "en",
518522
raw_user_info: JSON.stringify(rawUserInfo),
523+
recaptcha_score: TEST_RECAPTCHA_SCORE,
519524
};
520525
const context = {
521526
locale: "en",
@@ -534,6 +539,7 @@ describe("identity", () => {
534539
profile: rawUserInfo,
535540
username: undefined,
536541
isNewUser: false,
542+
recaptchaScore: TEST_RECAPTCHA_SCORE,
537543
},
538544
credential: null,
539545
params: {},
@@ -563,6 +569,7 @@ describe("identity", () => {
563569
oauth_refresh_token: "REFRESH_TOKEN",
564570
oauth_token_secret: "OAUTH_TOKEN_SECRET",
565571
oauth_expires_in: 3600,
572+
recaptcha_score: TEST_RECAPTCHA_SCORE,
566573
};
567574
const context = {
568575
locale: "en",
@@ -581,6 +588,7 @@ describe("identity", () => {
581588
profile: rawUserInfo,
582589
username: undefined,
583590
isNewUser: false,
591+
recaptchaScore: TEST_RECAPTCHA_SCORE,
584592
},
585593
credential: {
586594
claims: undefined,
@@ -619,14 +627,14 @@ describe("identity", () => {
619627
uid: "abcdefghijklmnopqrstuvwxyz",
620628
621629
email_verified: true,
622-
display_name: "John Doe",
630+
display_name: TEST_NAME,
623631
phone_number: "+11234567890",
624632
provider_data: [
625633
{
626634
provider_id: "oidc.provider",
627635
628636
629-
display_name: "John Doe",
637+
display_name: TEST_NAME,
630638
},
631639
],
632640
photo_url: "https://lh3.googleusercontent.com/1234567890/photo.jpg",
@@ -647,6 +655,7 @@ describe("identity", () => {
647655
oauth_token_secret: "OAUTH_TOKEN_SECRET",
648656
oauth_expires_in: 3600,
649657
raw_user_info: JSON.stringify(rawUserInfo),
658+
recaptcha_score: TEST_RECAPTCHA_SCORE,
650659
};
651660
const context = {
652661
locale: "en",
@@ -665,6 +674,7 @@ describe("identity", () => {
665674
providerId: "oidc.provider",
666675
profile: rawUserInfo,
667676
isNewUser: true,
677+
recaptchaScore: TEST_RECAPTCHA_SCORE,
668678
},
669679
credential: {
670680
claims: undefined,
@@ -762,4 +772,52 @@ describe("identity", () => {
762772
);
763773
});
764774
});
775+
776+
describe("generateResponsePayload", () => {
777+
const DISPLAY_NAME_FIELD = "displayName";
778+
const TEST_RESPONSE = {
779+
displayName: TEST_NAME,
780+
recaptchaActionOverride: BLOCK,
781+
} as identity.BeforeCreateResponse;
782+
783+
const EXPECT_PAYLOAD = {
784+
userRecord: { displayName: TEST_NAME, updateMask: DISPLAY_NAME_FIELD },
785+
recaptchaActionOverride: BLOCK,
786+
};
787+
788+
const TEST_RESPONSE_RECAPTCHA_ALLOW = {
789+
recaptchaActionOverride: ALLOW,
790+
} as identity.BeforeCreateResponse;
791+
792+
const EXPECT_PAYLOAD_RECAPTCHA_ALLOW = {
793+
recaptchaActionOverride: ALLOW,
794+
};
795+
796+
const TEST_RESPONSE_RECAPTCHA_UNDEFINED = {
797+
displayName: TEST_NAME,
798+
} as identity.BeforeSignInResponse;
799+
800+
const EXPECT_PAYLOAD_UNDEFINED = {
801+
userRecord: { displayName: TEST_NAME, updateMask: DISPLAY_NAME_FIELD },
802+
};
803+
it("should return empty object on undefined response", () => {
804+
expect(identity.generateResponsePayload()).to.eql({});
805+
});
806+
807+
it("should exclude recaptchaActionOverride field from updateMask", () => {
808+
expect(identity.generateResponsePayload(TEST_RESPONSE)).to.deep.equal(EXPECT_PAYLOAD);
809+
});
810+
811+
it("should return recaptchaActionOverride when it is true on response", () => {
812+
expect(identity.generateResponsePayload(TEST_RESPONSE_RECAPTCHA_ALLOW)).to.deep.equal(
813+
EXPECT_PAYLOAD_RECAPTCHA_ALLOW
814+
);
815+
});
816+
817+
it("should not return recaptchaActionOverride if undefined", () => {
818+
const payload = identity.generateResponsePayload(TEST_RESPONSE_RECAPTCHA_UNDEFINED);
819+
expect(payload.hasOwnProperty("recaptchaActionOverride")).to.be.false;
820+
expect(payload).to.deep.equal(EXPECT_PAYLOAD_UNDEFINED);
821+
});
822+
});
765823
});

src/common/providers/identity.ts

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export interface AdditionalUserInfo {
310310
profile?: any;
311311
username?: string;
312312
isNewUser: boolean;
313+
recaptchaScore?: number;
313314
}
314315

315316
/** The credential component of the auth event context */
@@ -338,13 +339,19 @@ export interface AuthBlockingEvent extends AuthEventContext {
338339
data: AuthUserRecord;
339340
}
340341

342+
/**
343+
* The reCAPTCHA action options.
344+
*/
345+
export type RecaptchaActionOptions = "ALLOW" | "BLOCK";
346+
341347
/** The handler response type for beforeCreate blocking events */
342348
export interface BeforeCreateResponse {
343349
displayName?: string;
344350
disabled?: boolean;
345351
emailVerified?: boolean;
346352
photoURL?: string;
347353
customClaims?: object;
354+
recaptchaActionOverride?: RecaptchaActionOptions;
348355
}
349356

350357
/** The handler response type for beforeSignIn blocking events */
@@ -423,9 +430,26 @@ export interface DecodedPayload {
423430
oauth_refresh_token?: string;
424431
oauth_token_secret?: string;
425432
oauth_expires_in?: number;
433+
recaptcha_score?: number;
426434
[key: string]: any;
427435
}
428436

437+
/**
438+
* Internal definition to include all the fields that can be sent as
439+
* a response from the blocking function to the backend.
440+
* This is added mainly to have a type definition for 'generateResponsePayload'
441+
* @internal */
442+
export interface ResponsePayload {
443+
userRecord?: UserRecordResponsePayload;
444+
recaptchaActionOverride?: RecaptchaActionOptions;
445+
}
446+
447+
/** @internal */
448+
export interface UserRecordResponsePayload
449+
extends Omit<BeforeSignInResponse, "recaptchaActionOverride"> {
450+
updateMask?: string;
451+
}
452+
429453
type HandlerV1 = (
430454
user: AuthUserRecord,
431455
context: AuthEventContext
@@ -640,9 +664,39 @@ function parseAdditionalUserInfo(decodedJWT: DecodedPayload): AdditionalUserInfo
640664
profile,
641665
username,
642666
isNewUser: decodedJWT.event_type === "beforeCreate" ? true : false,
667+
recaptchaScore: decodedJWT.recaptcha_score,
643668
};
644669
}
645670

671+
/**
672+
* Helper to generate a response from the blocking function to the Firebase Auth backend.
673+
* @internal
674+
*/
675+
export function generateResponsePayload(
676+
authResponse?: BeforeCreateResponse | BeforeSignInResponse
677+
): ResponsePayload {
678+
if (!authResponse) {
679+
return {};
680+
}
681+
682+
const { recaptchaActionOverride, ...formattedAuthResponse } = authResponse;
683+
const result = {} as ResponsePayload;
684+
const updateMask = getUpdateMask(formattedAuthResponse);
685+
686+
if (updateMask.length !== 0) {
687+
result.userRecord = {
688+
...formattedAuthResponse,
689+
updateMask,
690+
};
691+
}
692+
693+
if (recaptchaActionOverride !== undefined) {
694+
result.recaptchaActionOverride = recaptchaActionOverride;
695+
}
696+
697+
return result;
698+
}
699+
646700
/** Helper to get the Credential from the decoded jwt */
647701
function parseAuthCredential(decodedJWT: DecodedPayload, time: number): Credential {
648702
if (
@@ -801,7 +855,6 @@ export function wrapHandler(eventType: AuthBlockingEventType, handler: HandlerV1
801855
: handler.length === 2
802856
? await auth.getAuth(getApp())._verifyAuthBlockingToken(req.body.data.jwt)
803857
: await auth.getAuth(getApp())._verifyAuthBlockingToken(req.body.data.jwt, "run.app");
804-
805858
const authUserRecord = parseAuthUserRecord(decodedPayload.user_record);
806859
const authEventContext = parseAuthEventContext(decodedPayload, projectId);
807860

@@ -818,16 +871,7 @@ export function wrapHandler(eventType: AuthBlockingEventType, handler: HandlerV1
818871
}
819872

820873
validateAuthResponse(eventType, authResponse);
821-
const updateMask = getUpdateMask(authResponse);
822-
const result =
823-
updateMask.length === 0
824-
? {}
825-
: {
826-
userRecord: {
827-
...authResponse,
828-
updateMask,
829-
},
830-
};
874+
const result = generateResponsePayload(authResponse);
831875

832876
res.status(200);
833877
res.setHeader("Content-Type", "application/json");

0 commit comments

Comments
 (0)