Skip to content

Commit

Permalink
Implement reCAPTCHA Enterprise flow for phone provider (#7814)
Browse files Browse the repository at this point in the history
* Update injectRecaptchaFields to inject recaptcha enterprise fields into phone API requests (#7786)

* Update injectRecaptchaFields to inject recaptcha fields into phone API requests

* Fix lint

* Rename captchaResp and fakeToken params

* Format

* Implement reCAPTCHA Enterprise flow for phone provider

* Cleanup tests

* Make recaptchaEnterpriseVerifier.verify return a mock when appVerificationDisabledForTesting is true

* Lint fix

* yarn docgen devsite

* Mark appVerifier param in Phone Auth APIs as required

* Update API reports

* Change RecaptchaProvider to RecaptchaAuthProvider

* Fix reference docs

* Add more unit tests

---------

Co-authored-by: NhienLam <[email protected]>
  • Loading branch information
NhienLam and NhienLam committed Jun 9, 2024
1 parent 24954c3 commit 56f904c
Show file tree
Hide file tree
Showing 16 changed files with 1,186 additions and 176 deletions.
2 changes: 1 addition & 1 deletion docs-devsite/auth.phoneauthprovider.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ verifyPhoneNumber(phoneOptions: PhoneInfoOptions | string, applicationVerifier:

Promise&lt;string&gt;

A Promise for a verification ID that can be passed to [PhoneAuthProvider.credential()](./auth.phoneauthprovider.md#phoneauthprovidercredential) to identify this flow..
A Promise for a verification ID that can be passed to [PhoneAuthProvider.credential()](./auth.phoneauthprovider.md#phoneauthprovidercredential) to identify this flow.

### Example 1

Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export const enum EnforcementState {
}

// Providers that have reCAPTCHA Enterprise support.
export const enum RecaptchaProvider {
export const enum RecaptchaAuthProvider {
EMAIL_PASSWORD_PROVIDER = 'EMAIL_PASSWORD_PROVIDER',
PHONE_PROVIDER = 'PHONE_PROVIDER'
}
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/core/credentials/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('core/credentials/email', () => {

beforeEach(async () => {
auth = await testAuth();
auth.settings.appVerificationDisabledForTesting = false;
});

context('email & password', () => {
Expand Down
12 changes: 9 additions & 3 deletions packages/auth/src/core/credentials/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import { AuthErrorCode } from '../errors';
import { _fail } from '../util/assert';
import { AuthCredential } from './auth_credential';
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
import {
RecaptchaActionName,
RecaptchaClientType,
RecaptchaAuthProvider
} from '../../api';
import { SignUpRequest } from '../../api/authentication/sign_up';
/**
* Interface that represents the credentials returned by {@link EmailAuthProvider} for
Expand Down Expand Up @@ -128,7 +132,8 @@ export class EmailAuthCredential extends AuthCredential {
auth,
request,
RecaptchaActionName.SIGN_IN_WITH_PASSWORD,
signInWithPassword
signInWithPassword,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
case SignInMethod.EMAIL_LINK:
return signInWithEmailLink(auth, {
Expand Down Expand Up @@ -158,7 +163,8 @@ export class EmailAuthCredential extends AuthCredential {
auth,
request,
RecaptchaActionName.SIGN_UP_PASSWORD,
linkEmailPassword
linkEmailPassword,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
case SignInMethod.EMAIL_LINK:
return signInWithEmailLinkForLinking(auth, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('core/strategies/sendPasswordResetEmail', () => {

beforeEach(async () => {
auth = await testAuth();
auth.settings.appVerificationDisabledForTesting = false;
mockFetch.setUp();
});

Expand Down
12 changes: 9 additions & 3 deletions packages/auth/src/core/strategies/email_and_password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ import { getModularInstance } from '@firebase/util';
import { OperationType } from '../../model/enums';
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
import { IdTokenResponse } from '../../model/id_token';
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
import {
RecaptchaActionName,
RecaptchaClientType,
RecaptchaAuthProvider
} from '../../api';
import { _isFirebaseServerApp } from '@firebase/app';

/**
Expand Down Expand Up @@ -117,7 +121,8 @@ export async function sendPasswordResetEmail(
authInternal,
request,
RecaptchaActionName.GET_OOB_CODE,
authentication.sendPasswordResetEmail
authentication.sendPasswordResetEmail,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
}

Expand Down Expand Up @@ -291,7 +296,8 @@ export async function createUserWithEmailAndPassword(
authInternal,
request,
RecaptchaActionName.SIGN_UP_PASSWORD,
signUp
signUp,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
const response = await signUpResponse.catch(error => {
if (
Expand Down
1 change: 1 addition & 0 deletions packages/auth/src/core/strategies/email_link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('core/strategies/sendSignInLinkToEmail', () => {

beforeEach(async () => {
auth = await testAuth();
auth.settings.appVerificationDisabledForTesting = false;
mockFetch.setUp();
});

Expand Down
9 changes: 7 additions & 2 deletions packages/auth/src/core/strategies/email_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ import { _assert } from '../util/assert';
import { getModularInstance } from '@firebase/util';
import { _castAuth } from '../auth/auth_impl';
import { handleRecaptchaFlow } from '../../platform_browser/recaptcha/recaptcha_enterprise_verifier';
import { RecaptchaActionName, RecaptchaClientType } from '../../api';
import {
RecaptchaActionName,
RecaptchaClientType,
RecaptchaAuthProvider
} from '../../api';
import { _isFirebaseServerApp } from '@firebase/app';
import { _serverAppCurrentUserOperationNotSupportedError } from '../../core/util/assert';

Expand Down Expand Up @@ -108,7 +112,8 @@ export async function sendSignInLinkToEmail(
authInternal,
request,
RecaptchaActionName.GET_OOB_CODE,
api.sendSignInLinkToEmail
api.sendSignInLinkToEmail,
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
);
}

Expand Down
110 changes: 99 additions & 11 deletions packages/auth/src/platform_browser/providers/phone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,37 @@
import { expect } from 'chai';
import * as sinon from 'sinon';

import { mockEndpoint } from '../../../test/helpers/api/helper';
import {
mockEndpoint,
mockEndpointWithParams
} from '../../../test/helpers/api/helper';
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as fetch from '../../../test/helpers/mock_fetch';
import { Endpoint } from '../../api';
import {
Endpoint,
RecaptchaClientType,
RecaptchaVersion,
RecaptchaAuthProvider,
EnforcementState
} from '../../api';
import { RecaptchaVerifier } from '../../platform_browser/recaptcha/recaptcha_verifier';
import { PhoneAuthProvider } from './phone';
import { FAKE_TOKEN } from '../recaptcha/recaptcha_enterprise_verifier';
import { MockGreCAPTCHATopLevel } from '../recaptcha/recaptcha_mock';
import { ApplicationVerifierInternal } from '../../model/application_verifier';

describe('platform_browser/providers/phone', () => {
let auth: TestAuth;
let v2Verifier: ApplicationVerifierInternal;

beforeEach(async () => {
fetch.setUp();
auth = await testAuth();
auth.settings.appVerificationDisabledForTesting = false;
v2Verifier = new RecaptchaVerifier(auth, document.createElement('div'), {});
sinon
.stub(v2Verifier, 'verify')
.returns(Promise.resolve('verification-code'));
});

afterEach(() => {
Expand All @@ -39,26 +57,96 @@ describe('platform_browser/providers/phone', () => {
});

context('#verifyPhoneNumber', () => {
it('calls verify on the appVerifier and then calls the server', async () => {
it('calls verify on the appVerifier and then calls the server when recaptcha enterprise is disabled', async () => {
const recaptchaConfigResponseOff = {
recaptchaKey: 'foo/bar/to/site-key',
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
}
]
};
const recaptcha = new MockGreCAPTCHATopLevel();
if (typeof window === 'undefined') {
return;
}
window.grecaptcha = recaptcha;
sinon
.stub(recaptcha.enterprise, 'execute')
.returns(Promise.resolve('enterprise-token'));

mockEndpointWithParams(
Endpoint.GET_RECAPTCHA_CONFIG,
{
clientType: RecaptchaClientType.WEB,
version: RecaptchaVersion.ENTERPRISE
},
recaptchaConfigResponseOff
);

const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
sessionInfo: 'verification-id'
});

const verifier = new RecaptchaVerifier(
auth,
document.createElement('div'),
{}
const provider = new PhoneAuthProvider(auth);
const result = await provider.verifyPhoneNumber(
'+15105550000',
v2Verifier
);
expect(result).to.eq('verification-id');
expect(route.calls[0].request).to.eql({
phoneNumber: '+15105550000',
recaptchaToken: 'verification-code',
captchaResponse: FAKE_TOKEN,
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
});
});

it('calls the server when recaptcha enterprise is enabled', async () => {
const recaptchaConfigResponseEnforce = {
recaptchaKey: 'foo/bar/to/site-key',
recaptchaEnforcementState: [
{
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.ENFORCE
}
]
};
const recaptcha = new MockGreCAPTCHATopLevel();
if (typeof window === 'undefined') {
return;
}
window.grecaptcha = recaptcha;
sinon
.stub(verifier, 'verify')
.returns(Promise.resolve('verification-code'));
.stub(recaptcha.enterprise, 'execute')
.returns(Promise.resolve('enterprise-token'));

mockEndpointWithParams(
Endpoint.GET_RECAPTCHA_CONFIG,
{
clientType: RecaptchaClientType.WEB,
version: RecaptchaVersion.ENTERPRISE
},
recaptchaConfigResponseEnforce
);

const route = mockEndpoint(Endpoint.SEND_VERIFICATION_CODE, {
sessionInfo: 'verification-id'
});

const provider = new PhoneAuthProvider(auth);
const result = await provider.verifyPhoneNumber('+15105550000', verifier);
const result = await provider.verifyPhoneNumber(
'+15105550000',
v2Verifier
);
expect(result).to.eq('verification-id');
expect(route.calls[0].request).to.eql({
phoneNumber: '+15105550000',
recaptchaToken: 'verification-code'
captchaResponse: 'enterprise-token',
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion packages/auth/src/platform_browser/providers/phone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class PhoneAuthProvider {
* {@link RecaptchaVerifier}.
*
* @returns A Promise for a verification ID that can be passed to
* {@link PhoneAuthProvider.credential} to identify this flow..
* {@link PhoneAuthProvider.credential} to identify this flow.
*/
verifyPhoneNumber(
phoneOptions: PhoneInfoOptions | string,
Expand Down
32 changes: 16 additions & 16 deletions packages/auth/src/platform_browser/recaptcha/recaptcha.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {

import { isV2, isEnterprise, RecaptchaConfig } from './recaptcha';
import { GetRecaptchaConfigResponse } from '../../api/authentication/recaptcha';
import { EnforcementState, RecaptchaProvider } from '../../api/index';
import { EnforcementState, RecaptchaAuthProvider } from '../../api/index';

use(chaiAsPromised);
use(sinonChai);
Expand All @@ -46,11 +46,11 @@ describe('platform_browser/recaptcha/recaptcha', () => {
recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY,
recaptchaEnforcementState: [
{
provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER,
provider: RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER,
enforcementState: EnforcementState.ENFORCE
},
{
provider: RecaptchaProvider.PHONE_PROVIDER,
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.AUDIT
}
]
Expand All @@ -60,11 +60,11 @@ describe('platform_browser/recaptcha/recaptcha', () => {
recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY,
recaptchaEnforcementState: [
{
provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER,
provider: RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER,
enforcementState: EnforcementState.OFF
},
{
provider: RecaptchaProvider.PHONE_PROVIDER,
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
}
]
Expand All @@ -75,11 +75,11 @@ describe('platform_browser/recaptcha/recaptcha', () => {
recaptchaKey: 'projects/testproj/keys/' + TEST_SITE_KEY,
recaptchaEnforcementState: [
{
provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER,
provider: RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER,
enforcementState: EnforcementState.ENFORCE
},
{
provider: RecaptchaProvider.PHONE_PROVIDER,
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
}
]
Expand Down Expand Up @@ -120,33 +120,33 @@ describe('platform_browser/recaptcha/recaptcha', () => {
it('should construct the recaptcha config from the backend response', () => {
expect(recaptchaConfig.siteKey).to.eq(TEST_SITE_KEY);
expect(recaptchaConfig.recaptchaEnforcementState[0]).to.eql({
provider: RecaptchaProvider.EMAIL_PASSWORD_PROVIDER,
provider: RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER,
enforcementState: EnforcementState.ENFORCE
});
expect(recaptchaConfig.recaptchaEnforcementState[1]).to.eql({
provider: RecaptchaProvider.PHONE_PROVIDER,
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.AUDIT
});
expect(recaptchaConfigEnforceAndOff.recaptchaEnforcementState[1]).to.eql({
provider: RecaptchaProvider.PHONE_PROVIDER,
provider: RecaptchaAuthProvider.PHONE_PROVIDER,
enforcementState: EnforcementState.OFF
});
});

it('#getProviderEnforcementState should return the correct enforcement state of the provider', () => {
expect(
recaptchaConfig.getProviderEnforcementState(
RecaptchaProvider.EMAIL_PASSWORD_PROVIDER
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
)
).to.eq(EnforcementState.ENFORCE);
expect(
recaptchaConfig.getProviderEnforcementState(
RecaptchaProvider.PHONE_PROVIDER
RecaptchaAuthProvider.PHONE_PROVIDER
)
).to.eq(EnforcementState.AUDIT);
expect(
recaptchaConfigEnforceAndOff.getProviderEnforcementState(
RecaptchaProvider.PHONE_PROVIDER
RecaptchaAuthProvider.PHONE_PROVIDER
)
).to.eq(EnforcementState.OFF);
expect(recaptchaConfig.getProviderEnforcementState('invalid-provider')).to
Expand All @@ -156,15 +156,15 @@ describe('platform_browser/recaptcha/recaptcha', () => {
it('#isProviderEnabled should return the enablement state of the provider', () => {
expect(
recaptchaConfig.isProviderEnabled(
RecaptchaProvider.EMAIL_PASSWORD_PROVIDER
RecaptchaAuthProvider.EMAIL_PASSWORD_PROVIDER
)
).to.be.true;
expect(
recaptchaConfig.isProviderEnabled(RecaptchaProvider.PHONE_PROVIDER)
recaptchaConfig.isProviderEnabled(RecaptchaAuthProvider.PHONE_PROVIDER)
).to.be.true;
expect(
recaptchaConfigEnforceAndOff.isProviderEnabled(
RecaptchaProvider.PHONE_PROVIDER
RecaptchaAuthProvider.PHONE_PROVIDER
)
).to.be.false;
expect(recaptchaConfig.isProviderEnabled('invalid-provider')).to.be.false;
Expand Down
Loading

0 comments on commit 56f904c

Please sign in to comment.