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 6 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
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) | Initializes the reCAPTCHA configuration on 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()

Initializes the reCAPTCHA configuration on the `Auth` instance.

This will initialize reCAPTCHA config of the current Auth session which affects the future auth requests.

The reCAPTCHA config indicates whether the reCAPTCHA verification flow should be triggered for a specific auth provider. Note that this only affect the client auth request but won't override the actual enablement state on the server side.

For example, assume that reCAPTCHA verfication is enabled for Email provider via Cloud console or Admin SDKs. If the enablement is initialized to false via `initializeRecaptchaConfig()`<!-- -->, the auth flow will be started without the reCAPTCHA verfication. This will result in a `reCAPTCHA token missing` error while the SDK will automatically start the auth flow again with the reCAPTCHA verfication flow. Developers can avoid such round trip by enabling the reCAPTCHA flow with this method.

<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
39 changes: 39 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) | Initializes the reCAPTCHA configuration on 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,36 @@ const operationType = result.operationType;

```

## initializeRecaptchaConfig()

Initializes the reCAPTCHA configuration on the `Auth` instance.

This will pull the reCAPTCHA config to the current Auth session and affect future auth requests, which indicates whether the reCAPTCHA verification flow should be triggered for a specific auth provider. If initializeRecaptchaConfig() is not invoked, the auth flow will start without reCAPTCHA verification. But if reCAPTCHA verification is required, the reCAPTCHA config will be automatically pulled internally and the flows will restart. Thus, calling this optional method will reduce the latency of auth flows.

<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 +1826,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
19 changes: 11 additions & 8 deletions packages/auth/demo/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -229,17 +229,20 @@
</button>
</form>

<!-- Set Tenant -->
<div class="group">Set Tenant</div>
<!-- Recaptcha Configs -->
<div class="group">Recaptcha Configs</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>
<button class="btn btn-block btn-primary"
id="initialize-recaptcha-config">
Initialize reCAPTCHA Config
</button>

<!-- Sign up -->
<div class="group">Sign Up</div>
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