Skip to content

Commit 6b8e0c1

Browse files
author
renkelvin
authored
Recaptcha public preview (#7193)
1 parent b66908d commit 6b8e0c1

32 files changed

+1970
-72
lines changed

.changeset/smart-llamas-compete.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/auth': minor
3+
'firebase': minor
4+
---
5+
6+
[feature] Add reCAPTCHA enterprise support.

common/api-review/auth.api.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ export const AuthErrorCodes: {
226226
readonly WEAK_PASSWORD: "auth/weak-password";
227227
readonly WEB_STORAGE_UNSUPPORTED: "auth/web-storage-unsupported";
228228
readonly ALREADY_INITIALIZED: "auth/already-initialized";
229+
readonly RECAPTCHA_NOT_ENABLED: "auth/recaptcha-not-enabled";
230+
readonly MISSING_RECAPTCHA_TOKEN: "auth/missing-recaptcha-token";
231+
readonly INVALID_RECAPTCHA_TOKEN: "auth/invalid-recaptcha-token";
232+
readonly INVALID_RECAPTCHA_ACTION: "auth/invalid-recaptcha-action";
233+
readonly MISSING_CLIENT_TYPE: "auth/missing-client-type";
234+
readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version";
235+
readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version";
236+
readonly INVALID_REQ_TYPE: "auth/invalid-req-type";
229237
};
230238

231239
// @public
@@ -422,6 +430,9 @@ export const indexedDBLocalPersistence: Persistence;
422430
// @public
423431
export function initializeAuth(app: FirebaseApp, deps?: Dependencies): Auth;
424432

433+
// @public
434+
export function initializeRecaptchaConfig(auth: Auth): Promise<void>;
435+
425436
// @public
426437
export const inMemoryPersistence: Persistence;
427438

docs-devsite/auth.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Firebase Authentication
3131
| [fetchSignInMethodsForEmail(auth, email)](./auth.md#fetchsigninmethodsforemail) | Gets the list of possible sign in methods for the given email address. |
3232
| [getMultiFactorResolver(auth, error)](./auth.md#getmultifactorresolver) | Provides a [MultiFactorResolver](./auth.multifactorresolver.md#multifactorresolver_interface) suitable for completion of a multi-factor flow. |
3333
| [getRedirectResult(auth, resolver)](./auth.md#getredirectresult) | Returns a [UserCredential](./auth.usercredential.md#usercredential_interface) from the redirect-based sign-in flow. |
34+
| [initializeRecaptchaConfig(auth)](./auth.md#initializerecaptchaconfig) | Loads the reCAPTCHA configuration into the <code>Auth</code> instance. |
3435
| [isSignInWithEmailLink(auth, emailLink)](./auth.md#issigninwithemaillink) | Checks if an incoming link is a sign-in with email link suitable for [signInWithEmailLink()](./auth.md#signinwithemaillink)<!-- -->. |
3536
| [onAuthStateChanged(auth, nextOrObserver, error, completed)](./auth.md#onauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
3637
| [onIdTokenChanged(auth, nextOrObserver, error, completed)](./auth.md#onidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
@@ -486,6 +487,40 @@ const operationType = result.operationType;
486487

487488
```
488489

490+
## initializeRecaptchaConfig()
491+
492+
Loads the reCAPTCHA configuration into the `Auth` instance.
493+
494+
This will load the reCAPTCHA config, which indicates whether the reCAPTCHA verification flow should be triggered for each auth provider, into the current Auth session.
495+
496+
If initializeRecaptchaConfig() is not invoked, the auth flow will always start without reCAPTCHA verification. If the provider is configured to require reCAPTCHA verification, the SDK will transparently load the reCAPTCHA config and restart the auth flows.
497+
498+
Thus, by calling this optional method, you will reduce the latency of future auth flows. Loading the reCAPTCHA config early will also enhance the signal collected by reCAPTCHA.
499+
500+
<b>Signature:</b>
501+
502+
```typescript
503+
export declare function initializeRecaptchaConfig(auth: Auth): Promise<void>;
504+
```
505+
506+
### Parameters
507+
508+
| Parameter | Type | Description |
509+
| --- | --- | --- |
510+
| auth | [Auth](./auth.auth.md#auth_interface) | The [Auth](./auth.auth.md#auth_interface) instance. |
511+
512+
<b>Returns:</b>
513+
514+
Promise&lt;void&gt;
515+
516+
### Example
517+
518+
519+
```javascript
520+
initializeRecaptchaConfig(auth);
521+
522+
```
523+
489524
## isSignInWithEmailLink()
490525

491526
Checks if an incoming link is a sign-in with email link suitable for [signInWithEmailLink()](./auth.md#signinwithemaillink)<!-- -->.
@@ -1795,6 +1830,14 @@ AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY: {
17951830
readonly WEAK_PASSWORD: "auth/weak-password";
17961831
readonly WEB_STORAGE_UNSUPPORTED: "auth/web-storage-unsupported";
17971832
readonly ALREADY_INITIALIZED: "auth/already-initialized";
1833+
readonly RECAPTCHA_NOT_ENABLED: "auth/recaptcha-not-enabled";
1834+
readonly MISSING_RECAPTCHA_TOKEN: "auth/missing-recaptcha-token";
1835+
readonly INVALID_RECAPTCHA_TOKEN: "auth/invalid-recaptcha-token";
1836+
readonly INVALID_RECAPTCHA_ACTION: "auth/invalid-recaptcha-action";
1837+
readonly MISSING_CLIENT_TYPE: "auth/missing-client-type";
1838+
readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version";
1839+
readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version";
1840+
readonly INVALID_REQ_TYPE: "auth/invalid-req-type";
17981841
}
17991842
```
18001843

packages/auth/demo/public/index.html

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,21 @@
232232
<!-- Set Tenant -->
233233
<div class="group">Set Tenant</div>
234234
<form class="form form-bordered no-submit">
235-
<input type="text" id="set-tenant"
236-
class="form-control"
237-
placeholder="Tenant" />
238-
<button class="btn btn-block btn-primary"
239-
id="set-tenant-btn">
240-
Set Tenant
235+
<input type="text" id="tenant-id" class="form-control"
236+
placeholder="Tenant ID" />
237+
<button class="btn btn-block btn-primary set-tenant-id"
238+
data-expired=false>
239+
Set Tenant ID
241240
</button>
242241
</form>
243242

243+
<!-- Recaptcha Configs -->
244+
<div class="group">Recaptcha Configs</div>
245+
<button class="btn btn-block btn-primary"
246+
id="initialize-recaptcha-config">
247+
Initialize reCAPTCHA Config
248+
</button>
249+
244250
<!-- Sign up -->
245251
<div class="group">Sign Up</div>
246252
<form class="form form-bordered no-submit">

packages/auth/demo/src/index.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ import {
7171
reauthenticateWithRedirect,
7272
getRedirectResult,
7373
browserPopupRedirectResolver,
74-
connectAuthEmulator
74+
connectAuthEmulator,
75+
initializeRecaptchaConfig
7576
} from '@firebase/auth';
7677

7778
import { config } from './config';
@@ -480,6 +481,18 @@ function onSignInAnonymously() {
480481
signInAnonymously(auth).then(onAuthUserCredentialSuccess, onAuthError);
481482
}
482483

484+
function onSetTenantID(_event) {
485+
const tenantId = $('#tenant-id').val();
486+
auth.tenantId = tenantId;
487+
if (tenantId === '') {
488+
auth.tenantId = null;
489+
}
490+
}
491+
492+
function onInitializeRecaptchaConfig() {
493+
initializeRecaptchaConfig(auth);
494+
}
495+
483496
/**
484497
* Signs in with a generic IdP credential.
485498
*/
@@ -2018,6 +2031,8 @@ function initApp() {
20182031
);
20192032
$('.sign-in-with-custom-token').click(onSignInWithCustomToken);
20202033
$('#sign-in-anonymously').click(onSignInAnonymously);
2034+
$('.set-tenant-id').click(onSetTenantID);
2035+
$('#initialize-recaptcha-config').click(onInitializeRecaptchaConfig);
20212036
$('#sign-in-with-generic-idp-credential').click(
20222037
onSignInWithGenericIdPCredential
20232038
);

packages/auth/src/api/authentication/email_and_password.test.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import chaiAsPromised from 'chai-as-promised';
2121
import { ActionCodeOperation } from '../../model/public_types';
2222
import { FirebaseError } from '@firebase/util';
2323

24-
import { Endpoint, HttpHeader } from '../';
24+
import {
25+
Endpoint,
26+
HttpHeader,
27+
RecaptchaClientType,
28+
RecaptchaVersion
29+
} from '../';
2530
import { mockEndpoint } from '../../../test/helpers/api/helper';
2631
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
2732
import * as mockFetch from '../../../test/helpers/mock_fetch';
@@ -44,7 +49,10 @@ describe('api/authentication/signInWithPassword', () => {
4449
const request = {
4550
returnSecureToken: true,
4651
47-
password: 'my-password'
52+
password: 'my-password',
53+
captchaResponse: 'recaptcha-token',
54+
clientType: RecaptchaClientType.WEB,
55+
recaptchaVersion: RecaptchaVersion.ENTERPRISE
4856
};
4957

5058
let auth: TestAuth;
@@ -187,7 +195,10 @@ describe('api/authentication/sendEmailVerification', () => {
187195
describe('api/authentication/sendPasswordResetEmail', () => {
188196
const request: PasswordResetRequest = {
189197
requestType: ActionCodeOperation.PASSWORD_RESET,
190-
198+
199+
captchaResp: 'recaptcha-token',
200+
clientType: RecaptchaClientType.WEB,
201+
recaptchaVersion: RecaptchaVersion.ENTERPRISE
191202
};
192203

193204
let auth: TestAuth;
@@ -245,7 +256,10 @@ describe('api/authentication/sendPasswordResetEmail', () => {
245256
describe('api/authentication/sendSignInLinkToEmail', () => {
246257
const request: EmailSignInRequest = {
247258
requestType: ActionCodeOperation.EMAIL_SIGNIN,
248-
259+
260+
captchaResp: 'recaptcha-token',
261+
clientType: RecaptchaClientType.WEB,
262+
recaptchaVersion: RecaptchaVersion.ENTERPRISE
249263
};
250264

251265
let auth: TestAuth;

packages/auth/src/api/authentication/email_and_password.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { ActionCodeOperation, Auth } from '../../model/public_types';
2020
import {
2121
Endpoint,
2222
HttpMethod,
23+
RecaptchaClientType,
24+
RecaptchaVersion,
2325
_addTidIfNecessary,
2426
_performApiRequest,
2527
_performSignInRequest
@@ -31,6 +33,9 @@ export interface SignInWithPasswordRequest {
3133
email: string;
3234
password: string;
3335
tenantId?: string;
36+
captchaResponse?: string;
37+
clientType?: RecaptchaClientType;
38+
recaptchaVersion?: RecaptchaVersion;
3439
}
3540

3641
export interface SignInWithPasswordResponse extends IdTokenResponse {
@@ -76,11 +81,16 @@ export interface PasswordResetRequest extends GetOobCodeRequest {
7681
requestType: ActionCodeOperation.PASSWORD_RESET;
7782
email: string;
7883
captchaResp?: string;
84+
clientType?: RecaptchaClientType;
85+
recaptchaVersion?: RecaptchaVersion;
7986
}
8087

8188
export interface EmailSignInRequest extends GetOobCodeRequest {
8289
requestType: ActionCodeOperation.EMAIL_SIGNIN;
8390
email: string;
91+
captchaResp?: string;
92+
clientType?: RecaptchaClientType;
93+
recaptchaVersion?: RecaptchaVersion;
8494
}
8595

8696
export interface VerifyAndChangeEmailRequest extends GetOobCodeRequest {

packages/auth/src/api/authentication/recaptcha.test.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,20 @@ import chaiAsPromised from 'chai-as-promised';
2020

2121
import { FirebaseError } from '@firebase/util';
2222

23-
import { Endpoint, HttpHeader } from '../';
24-
import { mockEndpoint } from '../../../test/helpers/api/helper';
23+
import {
24+
Endpoint,
25+
HttpHeader,
26+
RecaptchaClientType,
27+
RecaptchaVersion
28+
} from '../';
29+
import {
30+
mockEndpoint,
31+
mockEndpointWithParams
32+
} from '../../../test/helpers/api/helper';
2533
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
2634
import * as mockFetch from '../../../test/helpers/mock_fetch';
2735
import { ServerError } from '../errors';
28-
import { getRecaptchaParams } from './recaptcha';
36+
import { getRecaptchaParams, getRecaptchaConfig } from './recaptcha';
2937

3038
use(chaiAsPromised);
3139

@@ -80,3 +88,58 @@ describe('api/authentication/getRecaptchaParams', () => {
8088
expect(mock.calls[0].request).to.be.undefined;
8189
});
8290
});
91+
92+
describe('api/authentication/getRecaptchaConfig', () => {
93+
const request = {
94+
clientType: RecaptchaClientType.WEB,
95+
recaptchaVersion: RecaptchaVersion.ENTERPRISE
96+
};
97+
98+
let auth: TestAuth;
99+
100+
beforeEach(async () => {
101+
auth = await testAuth();
102+
mockFetch.setUp();
103+
});
104+
105+
afterEach(mockFetch.tearDown);
106+
107+
it('should GET to the correct endpoint', async () => {
108+
const mock = mockEndpointWithParams(
109+
Endpoint.GET_RECAPTCHA_CONFIG,
110+
request,
111+
{
112+
recaptchaKey: 'site-key'
113+
}
114+
);
115+
116+
const response = await getRecaptchaConfig(auth, request);
117+
expect(response.recaptchaKey).to.eq('site-key');
118+
expect(mock.calls[0].method).to.eq('GET');
119+
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
120+
'application/json'
121+
);
122+
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
123+
'testSDK/0.0.0'
124+
);
125+
});
126+
127+
it('should handle errors', async () => {
128+
mockEndpointWithParams(
129+
Endpoint.GET_RECAPTCHA_CONFIG,
130+
request,
131+
{
132+
error: {
133+
code: 400,
134+
message: ServerError.UNAUTHORIZED_DOMAIN
135+
}
136+
},
137+
400
138+
);
139+
140+
await expect(getRecaptchaConfig(auth, request)).to.be.rejectedWith(
141+
FirebaseError,
142+
'auth/unauthorized-continue-uri'
143+
);
144+
});
145+
});

packages/auth/src/api/authentication/recaptcha.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { Endpoint, HttpMethod, _performApiRequest } from '../index';
18+
import {
19+
Endpoint,
20+
HttpMethod,
21+
RecaptchaClientType,
22+
RecaptchaVersion,
23+
_performApiRequest,
24+
_addTidIfNecessary
25+
} from '../index';
1926
import { Auth } from '../../model/public_types';
2027

2128
interface GetRecaptchaParamResponse {
@@ -33,3 +40,35 @@ export async function getRecaptchaParams(auth: Auth): Promise<string> {
3340
).recaptchaSiteKey || ''
3441
);
3542
}
43+
44+
// The following functions are for reCAPTCHA enterprise integration.
45+
interface GetRecaptchaConfigRequest {
46+
tenantId?: string;
47+
clientType?: RecaptchaClientType;
48+
version?: RecaptchaVersion;
49+
}
50+
51+
interface RecaptchaEnforcementState {
52+
provider: string;
53+
enforcementState: string;
54+
}
55+
56+
export interface GetRecaptchaConfigResponse {
57+
recaptchaKey: string;
58+
recaptchaEnforcementState: RecaptchaEnforcementState[];
59+
}
60+
61+
export async function getRecaptchaConfig(
62+
auth: Auth,
63+
request: GetRecaptchaConfigRequest
64+
): Promise<GetRecaptchaConfigResponse> {
65+
return _performApiRequest<
66+
GetRecaptchaConfigRequest,
67+
GetRecaptchaConfigResponse
68+
>(
69+
auth,
70+
HttpMethod.GET,
71+
Endpoint.GET_RECAPTCHA_CONFIG,
72+
_addTidIfNecessary(auth, request)
73+
);
74+
}

0 commit comments

Comments
 (0)