Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recaptcha public preview #7193

Merged
merged 20 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/smart-llamas-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/auth': minor
'firebase': minor
---

[feature] Add reCAPTCHA enterprise support.
12 changes: 12 additions & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface Auth {
readonly config: Config;
readonly currentUser: User | null;
readonly emulatorConfig: EmulatorConfig | null;
initializeRecaptchaConfig(): Promise<void>;
languageCode: string | null;
readonly name: string;
onAuthStateChanged(nextOrObserver: NextOrObserver<User | null>, error?: ErrorFn, completed?: CompleteFn): Unsubscribe;
Expand Down Expand Up @@ -226,6 +227,14 @@ export const AuthErrorCodes: {
readonly WEAK_PASSWORD: "auth/weak-password";
readonly WEB_STORAGE_UNSUPPORTED: "auth/web-storage-unsupported";
readonly ALREADY_INITIALIZED: "auth/already-initialized";
readonly RECAPTCHA_NOT_ENABLED: "auth/recaptcha-not-enabled";
readonly MISSING_RECAPTCHA_TOKEN: "auth/missing-recaptcha-token";
readonly INVALID_RECAPTCHA_TOKEN: "auth/invalid-recaptcha-token";
readonly INVALID_RECAPTCHA_ACTION: "auth/invalid-recaptcha-action";
readonly MISSING_CLIENT_TYPE: "auth/missing-client-type";
readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version";
readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version";
readonly INVALID_REQ_TYPE: "auth/invalid-req-type";
};

// @public
Expand Down Expand Up @@ -422,6 +431,9 @@ export const indexedDBLocalPersistence: Persistence;
// @public
export function initializeAuth(app: FirebaseApp, deps?: Dependencies): Auth;

// @public
export function initializeRecaptchaConfig(auth: Auth): Promise<void>;

// @public
export const inMemoryPersistence: Persistence;

Expand Down
28 changes: 28 additions & 0 deletions docs-devsite/auth.auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface Auth
| Method | Description |
| --- | --- |
| [beforeAuthStateChanged(callback, onAbort)](./auth.auth.md#authbeforeauthstatechanged) | Adds a blocking callback that runs before an auth state change sets a new user. |
| [initializeRecaptchaConfig()](./auth.auth.md#authinitializerecaptchaconfig) | Loads the reCAPTCHA configuration into the <code>Auth</code> instance. |
| [onAuthStateChanged(nextOrObserver, error, completed)](./auth.auth.md#authonauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
| [onIdTokenChanged(nextOrObserver, error, completed)](./auth.auth.md#authonidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
| [setPersistence(persistence)](./auth.auth.md#authsetpersistence) | Changes the type of persistence on the <code>Auth</code> instance. |
Expand Down Expand Up @@ -165,6 +166,33 @@ beforeAuthStateChanged(callback: (user: User | null) => void | Promise<void>, on

[Unsubscribe](./util.md#unsubscribe)

## Auth.initializeRecaptchaConfig()

Loads the reCAPTCHA configuration into the `Auth` instance.

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.

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.

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.

<b>Signature:</b>

```typescript
initializeRecaptchaConfig(): Promise<void>;
```
<b>Returns:</b>

Promise&lt;void&gt;

### Example


```javascript
auth.initializeRecaptchaConfig();

```

## Auth.onAuthStateChanged()

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

```

## initializeRecaptchaConfig()

Loads the reCAPTCHA configuration into the `Auth` instance.

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.

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.

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.

<b>Signature:</b>

```typescript
export declare function initializeRecaptchaConfig(auth: Auth): Promise<void>;
```

### Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| auth | [Auth](./auth.auth.md#auth_interface) | The [Auth](./auth.auth.md#auth_interface) instance. |

<b>Returns:</b>

Promise&lt;void&gt;

### Example


```javascript
initializeRecaptchaConfig(auth);

```

## isSignInWithEmailLink()

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

Expand Down
18 changes: 12 additions & 6 deletions packages/auth/demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,21 @@
<!-- Set Tenant -->
<div class="group">Set Tenant</div>
<form class="form form-bordered no-submit">
<input type="text" id="set-tenant"
class="form-control"
placeholder="Tenant" />
<button class="btn btn-block btn-primary"
id="set-tenant-btn">
Set Tenant
<input type="text" id="tenant-id" class="form-control"
renkelvin marked this conversation as resolved.
Show resolved Hide resolved
placeholder="Tenant ID" />
<button class="btn btn-block btn-primary set-tenant-id"
data-expired=false>
Set Tenant ID
</button>
</form>

<!-- Recaptcha Configs -->
<div class="group">Recaptcha Configs</div>
<button class="btn btn-block btn-primary"
id="initialize-recaptcha-config">
Initialize reCAPTCHA Config
</button>

<!-- Sign up -->
<div class="group">Sign Up</div>
<form class="form form-bordered no-submit">
Expand Down
17 changes: 16 additions & 1 deletion packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ import {
reauthenticateWithRedirect,
getRedirectResult,
browserPopupRedirectResolver,
connectAuthEmulator
connectAuthEmulator,
initializeRecaptchaConfig
} from '@firebase/auth';

import { config } from './config';
Expand Down Expand Up @@ -480,6 +481,18 @@ function onSignInAnonymously() {
signInAnonymously(auth).then(onAuthUserCredentialSuccess, onAuthError);
}

function onSetTenantID(_event) {
const tenantId = $('#tenant-id').val();
auth.tenantId = tenantId;
if (tenantId === '') {
auth.tenantId = null;
}
}

function onInitializeRecaptchaConfig() {
initializeRecaptchaConfig(auth);
}

/**
* Signs in with a generic IdP credential.
*/
Expand Down Expand Up @@ -2018,6 +2031,8 @@ function initApp() {
);
$('.sign-in-with-custom-token').click(onSignInWithCustomToken);
$('#sign-in-anonymously').click(onSignInAnonymously);
$('.set-tenant-id').click(onSetTenantID);
$('#initialize-recaptcha-config').click(onInitializeRecaptchaConfig);
$('#sign-in-with-generic-idp-credential').click(
onSignInWithGenericIdPCredential
);
Expand Down
22 changes: 18 additions & 4 deletions packages/auth/src/api/authentication/email_and_password.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import chaiAsPromised from 'chai-as-promised';
import { ActionCodeOperation } from '../../model/public_types';
import { FirebaseError } from '@firebase/util';

import { Endpoint, HttpHeader } from '../';
import {
Endpoint,
HttpHeader,
RecaptchaClientType,
RecaptchaVersion
} from '../';
import { mockEndpoint } from '../../../test/helpers/api/helper';
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as mockFetch from '../../../test/helpers/mock_fetch';
Expand All @@ -44,7 +49,10 @@ describe('api/authentication/signInWithPassword', () => {
const request = {
returnSecureToken: true,
email: 'test@foo.com',
password: 'my-password'
password: 'my-password',
captchaResponse: 'recaptcha-token',
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
};

let auth: TestAuth;
Expand Down Expand Up @@ -187,7 +195,10 @@ describe('api/authentication/sendEmailVerification', () => {
describe('api/authentication/sendPasswordResetEmail', () => {
const request: PasswordResetRequest = {
requestType: ActionCodeOperation.PASSWORD_RESET,
email: 'test@foo.com'
email: 'test@foo.com',
captchaResp: 'recaptcha-token',
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
};

let auth: TestAuth;
Expand Down Expand Up @@ -245,7 +256,10 @@ describe('api/authentication/sendPasswordResetEmail', () => {
describe('api/authentication/sendSignInLinkToEmail', () => {
const request: EmailSignInRequest = {
requestType: ActionCodeOperation.EMAIL_SIGNIN,
email: 'test@foo.com'
email: 'test@foo.com',
captchaResp: 'recaptcha-token',
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
};

let auth: TestAuth;
Expand Down
10 changes: 10 additions & 0 deletions packages/auth/src/api/authentication/email_and_password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { ActionCodeOperation, Auth } from '../../model/public_types';
import {
Endpoint,
HttpMethod,
RecaptchaClientType,
RecaptchaVersion,
_addTidIfNecessary,
_performApiRequest,
_performSignInRequest
Expand All @@ -31,6 +33,9 @@ export interface SignInWithPasswordRequest {
email: string;
password: string;
tenantId?: string;
captchaResponse?: string;
clientType?: RecaptchaClientType;
recaptchaVersion?: RecaptchaVersion;
}

export interface SignInWithPasswordResponse extends IdTokenResponse {
Expand Down Expand Up @@ -76,11 +81,16 @@ export interface PasswordResetRequest extends GetOobCodeRequest {
requestType: ActionCodeOperation.PASSWORD_RESET;
email: string;
captchaResp?: string;
clientType?: RecaptchaClientType;
recaptchaVersion?: RecaptchaVersion;
}

export interface EmailSignInRequest extends GetOobCodeRequest {
requestType: ActionCodeOperation.EMAIL_SIGNIN;
email: string;
captchaResp?: string;
clientType?: RecaptchaClientType;
recaptchaVersion?: RecaptchaVersion;
}

export interface VerifyAndChangeEmailRequest extends GetOobCodeRequest {
Expand Down
69 changes: 66 additions & 3 deletions packages/auth/src/api/authentication/recaptcha.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,20 @@ import chaiAsPromised from 'chai-as-promised';

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

import { Endpoint, HttpHeader } from '../';
import { mockEndpoint } from '../../../test/helpers/api/helper';
import {
Endpoint,
HttpHeader,
RecaptchaClientType,
RecaptchaVersion
} from '../';
import {
mockEndpoint,
mockEndpointWithParams
} from '../../../test/helpers/api/helper';
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as mockFetch from '../../../test/helpers/mock_fetch';
import { ServerError } from '../errors';
import { getRecaptchaParams } from './recaptcha';
import { getRecaptchaParams, getRecaptchaConfig } from './recaptcha';

use(chaiAsPromised);

Expand Down Expand Up @@ -80,3 +88,58 @@ describe('api/authentication/getRecaptchaParams', () => {
expect(mock.calls[0].request).to.be.undefined;
});
});

describe('api/authentication/getRecaptchaConfig', () => {
const request = {
clientType: RecaptchaClientType.WEB,
recaptchaVersion: RecaptchaVersion.ENTERPRISE
};

let auth: TestAuth;

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

afterEach(mockFetch.tearDown);

it('should GET to the correct endpoint', async () => {
const mock = mockEndpointWithParams(
Endpoint.GET_RECAPTCHA_CONFIG,
request,
{
recaptchaKey: 'site-key'
}
);

const response = await getRecaptchaConfig(auth, request);
expect(response.recaptchaKey).to.eq('site-key');
expect(mock.calls[0].method).to.eq('GET');
expect(mock.calls[0].headers!.get(HttpHeader.CONTENT_TYPE)).to.eq(
'application/json'
);
expect(mock.calls[0].headers!.get(HttpHeader.X_CLIENT_VERSION)).to.eq(
'testSDK/0.0.0'
);
});

it('should handle errors', async () => {
mockEndpointWithParams(
Endpoint.GET_RECAPTCHA_CONFIG,
request,
{
error: {
code: 400,
message: ServerError.UNAUTHORIZED_DOMAIN
}
},
400
);

await expect(getRecaptchaConfig(auth, request)).to.be.rejectedWith(
FirebaseError,
'auth/unauthorized-continue-uri'
);
});
});
Loading