Skip to content

Commit

Permalink
Recaptcha public preview (#7193)
Browse files Browse the repository at this point in the history
  • Loading branch information
renkelvin authored Apr 13, 2023
1 parent b66908d commit 6b8e0c1
Show file tree
Hide file tree
Showing 32 changed files with 1,970 additions and 72 deletions.
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.
11 changes: 11 additions & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,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 +430,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
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"
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: '[email protected]',
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: '[email protected]'
email: '[email protected]',
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: '[email protected]'
email: '[email protected]',
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'
);
});
});
41 changes: 40 additions & 1 deletion packages/auth/src/api/authentication/recaptcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
* limitations under the License.
*/

import { Endpoint, HttpMethod, _performApiRequest } from '../index';
import {
Endpoint,
HttpMethod,
RecaptchaClientType,
RecaptchaVersion,
_performApiRequest,
_addTidIfNecessary
} from '../index';
import { Auth } from '../../model/public_types';

interface GetRecaptchaParamResponse {
Expand All @@ -33,3 +40,35 @@ export async function getRecaptchaParams(auth: Auth): Promise<string> {
).recaptchaSiteKey || ''
);
}

// The following functions are for reCAPTCHA enterprise integration.
interface GetRecaptchaConfigRequest {
tenantId?: string;
clientType?: RecaptchaClientType;
version?: RecaptchaVersion;
}

interface RecaptchaEnforcementState {
provider: string;
enforcementState: string;
}

export interface GetRecaptchaConfigResponse {
recaptchaKey: string;
recaptchaEnforcementState: RecaptchaEnforcementState[];
}

export async function getRecaptchaConfig(
auth: Auth,
request: GetRecaptchaConfigRequest
): Promise<GetRecaptchaConfigResponse> {
return _performApiRequest<
GetRecaptchaConfigRequest,
GetRecaptchaConfigResponse
>(
auth,
HttpMethod.GET,
Endpoint.GET_RECAPTCHA_CONFIG,
_addTidIfNecessary(auth, request)
);
}
Loading

0 comments on commit 6b8e0c1

Please sign in to comment.