diff --git a/.changeset/polite-lies-vanish.md b/.changeset/polite-lies-vanish.md new file mode 100644 index 00000000000..698359235e2 --- /dev/null +++ b/.changeset/polite-lies-vanish.md @@ -0,0 +1,7 @@ +--- +'firebase': minor +'@firebase/auth-types': minor +'@firebase/auth': minor +--- + +Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain that is used in mobile out-of-band email action flows. Also, deprecated `ActionCodeSettings.dynamicLinkDomain`. diff --git a/common/api-review/auth.api.md b/common/api-review/auth.api.md index b011b803774..7ec0db38bdc 100644 --- a/common/api-review/auth.api.md +++ b/common/api-review/auth.api.md @@ -40,11 +40,13 @@ export interface ActionCodeSettings { minimumVersion?: string; packageName: string; }; + // @deprecated dynamicLinkDomain?: string; handleCodeInApp?: boolean; iOS?: { bundleId: string; }; + linkDomain?: string; url: string; } @@ -236,6 +238,7 @@ export const AuthErrorCodes: { readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version"; readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version"; readonly INVALID_REQ_TYPE: "auth/invalid-req-type"; + readonly INVALID_HOSTING_LINK_DOMAIN: "auth/invalid-hosting-link-domain"; }; // @public diff --git a/docs-devsite/auth.actioncodesettings.md b/docs-devsite/auth.actioncodesettings.md index a12144adaf4..610bea0a429 100644 --- a/docs-devsite/auth.actioncodesettings.md +++ b/docs-devsite/auth.actioncodesettings.md @@ -26,13 +26,14 @@ export interface ActionCodeSettings | [dynamicLinkDomain](./auth.actioncodesettings.md#actioncodesettingsdynamiclinkdomain) | string | When multiple custom dynamic link domains are defined for a project, specify which one to use when the link is to be opened via a specified mobile app (for example, example.page.link). | | [handleCodeInApp](./auth.actioncodesettings.md#actioncodesettingshandlecodeinapp) | boolean | When set to true, the action code link will be be sent as a Universal Link or Android App Link and will be opened by the app if installed. | | [iOS](./auth.actioncodesettings.md#actioncodesettingsios) | { bundleId: string; } | Sets the iOS bundle ID. | +| [linkDomain](./auth.actioncodesettings.md#actioncodesettingslinkdomain) | string | The optional custom Firebase Hosting domain to use when the link is to be opened via a specified mobile app. The domain must be configured in Firebase Hosting and owned by the project. This cannot be a default Hosting domain (web.app or firebaseapp.com). | | [url](./auth.actioncodesettings.md#actioncodesettingsurl) | string | Sets the link continue/state URL. | ## ActionCodeSettings.android Sets the Android package name. -This will try to open the link in an android app if it is installed. If `installApp` is passed, it specifies whether to install the Android app if the device supports it and the app is not already installed. If this field is provided without a `packageName`, an error is thrown explaining that the `packageName` must be provided in conjunction with this field. If `minimumVersion` is specified, and an older version of the app is installed, the user is taken to the Play Store to upgrade the app. +This will try to open the link in an Android app if it is installed. Signature: @@ -46,6 +47,11 @@ android?: { ## ActionCodeSettings.dynamicLinkDomain +> Warning: This API is now obsolete. +> +> Firebase Dynamic Links is deprecated and will be shut down as early as August 2025. Instead, use [ActionCodeSettings.linkDomain](./auth.actioncodesettings.md#actioncodesettingslinkdomain) to set a custom domain for mobile links. Learn more in the [Dynamic Links deprecation FAQ](https://firebase.google.com/support/dynamic-links-faq). +> + When multiple custom dynamic link domains are defined for a project, specify which one to use when the link is to be opened via a specified mobile app (for example, `example.page.link`). Signature: @@ -72,8 +78,6 @@ Sets the iOS bundle ID. This will try to open the link in an iOS app if it is installed. -App installation is not supported for iOS. - Signature: ```typescript @@ -82,11 +86,21 @@ iOS?: { }; ``` +## ActionCodeSettings.linkDomain + +The optional custom Firebase Hosting domain to use when the link is to be opened via a specified mobile app. The domain must be configured in Firebase Hosting and owned by the project. This cannot be a default Hosting domain (`web.app` or `firebaseapp.com`). + +Signature: + +```typescript +linkDomain?: string; +``` + ## ActionCodeSettings.url Sets the link continue/state URL. -This has different meanings in different contexts: - When the link is handled in the web action widgets, this is the deep link in the `continueUrl` query parameter. - When the link is handled in the app directly, this is the `continueUrl` query parameter in the deep link of the Dynamic Link. +This has different meanings in different contexts: - When the link is handled in the web action widgets, this is the deep link in the `continueUrl` query parameter. - When the link is handled in the app directly, this is the `continueUrl` query parameter in the deep link of the Dynamic Link or Hosting link. Signature: diff --git a/docs-devsite/auth.md b/docs-devsite/auth.md index 83c91db1de2..82f8a3dc196 100644 --- a/docs-devsite/auth.md +++ b/docs-devsite/auth.md @@ -1956,6 +1956,7 @@ AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY: { readonly MISSING_RECAPTCHA_VERSION: "auth/missing-recaptcha-version"; readonly INVALID_RECAPTCHA_VERSION: "auth/invalid-recaptcha-version"; readonly INVALID_REQ_TYPE: "auth/invalid-req-type"; + readonly INVALID_HOSTING_LINK_DOMAIN: "auth/invalid-hosting-link-domain"; } ``` diff --git a/packages/auth-types/index.d.ts b/packages/auth-types/index.d.ts index bebf4e5b18a..4b0192df925 100644 --- a/packages/auth-types/index.d.ts +++ b/packages/auth-types/index.d.ts @@ -130,6 +130,7 @@ export type ActionCodeSettings = { iOS?: { bundleId: string }; url: string; dynamicLinkDomain?: string; + linkDomain?: string; }; export type AdditionalUserInfo = { diff --git a/packages/auth/demo/public/index.html b/packages/auth/demo/public/index.html index 030e730a835..78b14b8dd92 100644 --- a/packages/auth/demo/public/index.html +++ b/packages/auth/demo/public/index.html @@ -823,6 +823,11 @@ +
Mobile link
+
+ +
diff --git a/packages/auth/demo/src/index.js b/packages/auth/demo/src/index.js index 387e98799b4..876a345671a 100644 --- a/packages/auth/demo/src/index.js +++ b/packages/auth/demo/src/index.js @@ -49,8 +49,8 @@ import { signInWithCredential, signInWithCustomToken, signInWithEmailAndPassword, + signInWithEmailLink, TotpMultiFactorGenerator, - TotpSecret, unlink, updateEmail, updatePassword, @@ -995,6 +995,7 @@ function getActionCodeSettings() { const installApp = $('input[name=install-app]:checked').val() === 'Yes'; const handleCodeInApp = $('input[name=handle-in-app]:checked').val() === 'Yes'; + const hostingLinkDomain = $('#hostingLinkDomain').val(); if (url || apn || ibi) { actionCodeSettings['url'] = url; if (apn) { @@ -1010,6 +1011,9 @@ function getActionCodeSettings() { }; } actionCodeSettings['handleCodeInApp'] = handleCodeInApp; + if (hostingLinkDomain) { + actionCodeSettings['linkDomain'] = hostingLinkDomain; + } } return actionCodeSettings; } @@ -1020,6 +1024,7 @@ function onActionCodeSettingsReset() { $('#apn').val(''); $('#amv').val(''); $('#ibi').val(''); + $('#hostingLinkDomain').val(''); } /** diff --git a/packages/auth/karma.conf.js b/packages/auth/karma.conf.js index 161f4819d32..749ad0672c4 100644 --- a/packages/auth/karma.conf.js +++ b/packages/auth/karma.conf.js @@ -41,7 +41,8 @@ function getTestFiles(argv) { return [ 'test/integration/flows/totp.test.ts', 'test/integration/flows/password_policy.test.ts', - 'test/integration/flows/recaptcha_enterprise.test.ts' + 'test/integration/flows/recaptcha_enterprise.test.ts', + 'test/integration/flows/hosting_link.test.ts' ]; } return argv.local diff --git a/packages/auth/src/api/authentication/email_and_password.ts b/packages/auth/src/api/authentication/email_and_password.ts index 2f9664f72db..9012b95d213 100644 --- a/packages/auth/src/api/authentication/email_and_password.ts +++ b/packages/auth/src/api/authentication/email_and_password.ts @@ -70,6 +70,7 @@ export interface GetOobCodeRequest { dynamicLinkDomain?: string; tenantId?: string; targetProjectid?: string; + linkDomain?: string; } export interface VerifyEmailRequest extends GetOobCodeRequest { diff --git a/packages/auth/src/api/errors.ts b/packages/auth/src/api/errors.ts index d8f73f72821..0e59e3babbd 100644 --- a/packages/auth/src/api/errors.ts +++ b/packages/auth/src/api/errors.ts @@ -100,7 +100,8 @@ export const enum ServerError { MISSING_RECAPTCHA_VERSION = 'MISSING_RECAPTCHA_VERSION', INVALID_RECAPTCHA_VERSION = 'INVALID_RECAPTCHA_VERSION', INVALID_REQ_TYPE = 'INVALID_REQ_TYPE', - PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'PASSWORD_DOES_NOT_MEET_REQUIREMENTS' + PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'PASSWORD_DOES_NOT_MEET_REQUIREMENTS', + INVALID_HOSTING_LINK_DOMAIN = 'INVALID_HOSTING_LINK_DOMAIN' } /** diff --git a/packages/auth/src/core/errors.ts b/packages/auth/src/core/errors.ts index 9494658b9f0..0e7fb53059c 100644 --- a/packages/auth/src/core/errors.ts +++ b/packages/auth/src/core/errors.ts @@ -134,7 +134,8 @@ export const enum AuthErrorCode { INVALID_RECAPTCHA_VERSION = 'invalid-recaptcha-version', INVALID_REQ_TYPE = 'invalid-req-type', UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION = 'unsupported-password-policy-schema-version', - PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'password-does-not-meet-requirements' + PASSWORD_DOES_NOT_MEET_REQUIREMENTS = 'password-does-not-meet-requirements', + INVALID_HOSTING_LINK_DOMAIN = 'invalid-hosting-link-domain' } function _debugErrorMap(): ErrorMap { @@ -387,7 +388,10 @@ function _debugErrorMap(): ErrorMap { [AuthErrorCode.UNSUPPORTED_PASSWORD_POLICY_SCHEMA_VERSION]: 'The password policy received from the backend uses a schema version that is not supported by this version of the Firebase SDK.', [AuthErrorCode.PASSWORD_DOES_NOT_MEET_REQUIREMENTS]: - 'The password does not meet the requirements.' + 'The password does not meet the requirements.', + [AuthErrorCode.INVALID_HOSTING_LINK_DOMAIN]: + 'The provided Hosting link domain is not configured in Firebase Hosting or is not owned by ' + + 'the current project. This cannot be a default Hosting domain (`web.app` or `firebaseapp.com`).' }; } @@ -598,5 +602,6 @@ export const AUTH_ERROR_CODES_MAP_DO_NOT_USE_INTERNALLY = { MISSING_CLIENT_TYPE: 'auth/missing-client-type', MISSING_RECAPTCHA_VERSION: 'auth/missing-recaptcha-version', INVALID_RECAPTCHA_VERSION: 'auth/invalid-recaptcha-version', - INVALID_REQ_TYPE: 'auth/invalid-req-type' + INVALID_REQ_TYPE: 'auth/invalid-req-type', + INVALID_HOSTING_LINK_DOMAIN: 'auth/invalid-hosting-link-domain' } as const; diff --git a/packages/auth/src/core/strategies/action_code_settings.test.ts b/packages/auth/src/core/strategies/action_code_settings.test.ts index 36784151156..09bc672ee06 100644 --- a/packages/auth/src/core/strategies/action_code_settings.test.ts +++ b/packages/auth/src/core/strategies/action_code_settings.test.ts @@ -26,6 +26,10 @@ describe('core/strategies/action_code_settings', () => { let auth: TestAuth; const request: GetOobCodeRequest = {}; + const TEST_BUNDLE_ID = 'my-bundle'; + const TEST_FDL_DOMAIN = 'fdl-domain'; + const TEST_URL = 'my-url'; + beforeEach(async () => { auth = await testAuth(); }); @@ -35,10 +39,10 @@ describe('core/strategies/action_code_settings', () => { _setActionCodeSettingsOnRequest(auth, request, { handleCodeInApp: true, iOS: { - bundleId: 'my-bundle' + bundleId: TEST_BUNDLE_ID }, url: '', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: TEST_FDL_DOMAIN }) ).to.throw(FirebaseError, '(auth/invalid-continue-uri)'); }); @@ -48,9 +52,9 @@ describe('core/strategies/action_code_settings', () => { _setActionCodeSettingsOnRequest(auth, request, { handleCodeInApp: true, iOS: { - bundleId: 'my-´bundle' + bundleId: TEST_BUNDLE_ID }, - url: 'my-url' + url: TEST_URL }) ).to.not.throw(); }); @@ -60,14 +64,27 @@ describe('core/strategies/action_code_settings', () => { _setActionCodeSettingsOnRequest(auth, request, { handleCodeInApp: true, iOS: { - bundleId: 'my-´bundle' + bundleId: TEST_BUNDLE_ID }, - url: 'my-url', + url: TEST_URL, dynamicLinkDomain: '' }) ).to.throw(FirebaseError, '(auth/invalid-dynamic-link-domain)'); }); + it('should require a non empty Hosting link URL', () => { + expect(() => + _setActionCodeSettingsOnRequest(auth, request, { + handleCodeInApp: true, + iOS: { + bundleId: TEST_BUNDLE_ID + }, + url: TEST_URL, + linkDomain: '' + }) + ).to.throw(FirebaseError, '(auth/invalid-hosting-link-domain)'); + }); + it('should require a non-empty bundle ID', () => { expect(() => _setActionCodeSettingsOnRequest(auth, request, { @@ -75,8 +92,8 @@ describe('core/strategies/action_code_settings', () => { iOS: { bundleId: '' }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + url: TEST_URL, + dynamicLinkDomain: TEST_FDL_DOMAIN }) ).to.throw(FirebaseError, '(auth/missing-ios-bundle-id)'); }); @@ -88,8 +105,8 @@ describe('core/strategies/action_code_settings', () => { android: { packageName: '' }, - url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + url: TEST_URL, + dynamicLinkDomain: TEST_FDL_DOMAIN }) ).to.throw(FirebaseError, '(auth/missing-android-pkg-name)'); }); diff --git a/packages/auth/src/core/strategies/action_code_settings.ts b/packages/auth/src/core/strategies/action_code_settings.ts index 4afa54f2ab3..e7e4c994838 100644 --- a/packages/auth/src/core/strategies/action_code_settings.ts +++ b/packages/auth/src/core/strategies/action_code_settings.ts @@ -37,9 +37,16 @@ export function _setActionCodeSettingsOnRequest( auth, AuthErrorCode.INVALID_DYNAMIC_LINK_DOMAIN ); + _assert( + typeof actionCodeSettings.linkDomain === 'undefined' || + actionCodeSettings.linkDomain.length > 0, + auth, + AuthErrorCode.INVALID_HOSTING_LINK_DOMAIN + ); request.continueUrl = actionCodeSettings.url; request.dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain; + request.linkDomain = actionCodeSettings.linkDomain; request.canHandleCodeInApp = actionCodeSettings.handleCodeInApp; if (actionCodeSettings.iOS) { diff --git a/packages/auth/src/core/strategies/email.test.ts b/packages/auth/src/core/strategies/email.test.ts index 85493a89aef..b2edc26ebb2 100644 --- a/packages/auth/src/core/strategies/email.test.ts +++ b/packages/auth/src/core/strategies/email.test.ts @@ -162,7 +162,8 @@ describe('core/strategies/sendEmailVerification', () => { bundleId: 'my-bundle' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ @@ -170,6 +171,7 @@ describe('core/strategies/sendEmailVerification', () => { idToken, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, iOSBundleId: 'my-bundle' }); @@ -190,13 +192,15 @@ describe('core/strategies/sendEmailVerification', () => { packageName: 'my-package' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ requestType: ActionCodeOperation.VERIFY_EMAIL, idToken, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, androidInstallApp: false, androidMinimumVersionCode: 'my-version', @@ -270,7 +274,8 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => { bundleId: 'my-bundle' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ @@ -279,6 +284,7 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => { newEmail, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, iOSBundleId: 'my-bundle' }); @@ -299,7 +305,8 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => { packageName: 'my-package' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ requestType: ActionCodeOperation.VERIFY_AND_CHANGE_EMAIL, @@ -307,6 +314,7 @@ describe('core/strategies/verifyBeforeUpdateEmail', () => { newEmail, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, androidInstallApp: false, androidMinimumVersionCode: 'my-version', diff --git a/packages/auth/src/core/strategies/email_and_password.test.ts b/packages/auth/src/core/strategies/email_and_password.test.ts index 047e86dc17f..e92f5abb32d 100644 --- a/packages/auth/src/core/strategies/email_and_password.test.ts +++ b/packages/auth/src/core/strategies/email_and_password.test.ts @@ -121,7 +121,8 @@ describe('core/strategies/sendPasswordResetEmail', () => { bundleId: 'my-bundle' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ @@ -129,6 +130,7 @@ describe('core/strategies/sendPasswordResetEmail', () => { email, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, iOSBundleId: 'my-bundle', clientType: 'CLIENT_TYPE_WEB' @@ -149,13 +151,15 @@ describe('core/strategies/sendPasswordResetEmail', () => { packageName: 'my-package' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ requestType: ActionCodeOperation.PASSWORD_RESET, email, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, androidInstallApp: false, androidMinimumVersionCode: 'my-version', diff --git a/packages/auth/src/core/strategies/email_link.test.ts b/packages/auth/src/core/strategies/email_link.test.ts index 97f8cdc7c7a..c58089bf497 100644 --- a/packages/auth/src/core/strategies/email_link.test.ts +++ b/packages/auth/src/core/strategies/email_link.test.ts @@ -124,7 +124,8 @@ describe('core/strategies/sendSignInLinkToEmail', () => { bundleId: 'my-bundle' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ @@ -132,6 +133,7 @@ describe('core/strategies/sendSignInLinkToEmail', () => { email, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, iOSBundleId: 'my-bundle', clientType: 'CLIENT_TYPE_WEB' @@ -152,13 +154,15 @@ describe('core/strategies/sendSignInLinkToEmail', () => { packageName: 'my-package' }, url: 'my-url', - dynamicLinkDomain: 'fdl-domain' + dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain' }); expect(mock.calls[0].request).to.eql({ requestType: ActionCodeOperation.EMAIL_SIGNIN, email, continueUrl: 'my-url', dynamicLinkDomain: 'fdl-domain', + linkDomain: 'hosting-link-domain', canHandleCodeInApp: true, androidInstallApp: false, androidMinimumVersionCode: 'my-version', diff --git a/packages/auth/src/model/public_types.ts b/packages/auth/src/model/public_types.ts index 07e56a6b9aa..ac1face6b6a 100644 --- a/packages/auth/src/model/public_types.ts +++ b/packages/auth/src/model/public_types.ts @@ -462,12 +462,7 @@ export interface ActionCodeSettings { * Sets the Android package name. * * @remarks - * This will try to open the link in an android app if it is - * installed. If `installApp` is passed, it specifies whether to install the Android app if the - * device supports it and the app is not already installed. If this field is provided without - * a `packageName`, an error is thrown explaining that the `packageName` must be provided in - * conjunction with this field. If `minimumVersion` is specified, and an older version of the - * app is installed, the user is taken to the Play Store to upgrade the app. + * This will try to open the link in an Android app if it is installed. */ android?: { installApp?: boolean; @@ -490,8 +485,6 @@ export interface ActionCodeSettings { * * @remarks * This will try to open the link in an iOS app if it is installed. - * - * App installation is not supported for iOS. */ iOS?: { bundleId: string; @@ -504,7 +497,7 @@ export interface ActionCodeSettings { * - When the link is handled in the web action widgets, this is the deep link in the * `continueUrl` query parameter. * - When the link is handled in the app directly, this is the `continueUrl` query parameter in - * the deep link of the Dynamic Link. + * the deep link of the Dynamic Link or Hosting link. */ url: string; /** @@ -513,8 +506,20 @@ export interface ActionCodeSettings { * * * @defaultValue The first domain is automatically selected. + * + * @deprecated Firebase Dynamic Links is deprecated and will be shut down as early as August + * 2025. Instead, use {@link ActionCodeSettings.linkDomain} to set a custom domain for mobile + * links. Learn more in the {@link https://firebase.google.com/support/dynamic-links-faq | Dynamic Links deprecation FAQ}. */ dynamicLinkDomain?: string; + /** + * The optional custom Firebase Hosting domain to use when the link is to be opened via + * a specified mobile app. The domain must be configured in Firebase Hosting and owned + * by the project. This cannot be a default Hosting domain (`web.app` or `firebaseapp.com`). + * + * @defaultValue The default Hosting domain will be used (for example, `example.firebaseapp.com`). + */ + linkDomain?: string; } /** diff --git a/packages/auth/test/integration/flows/hosting_link.test.ts b/packages/auth/test/integration/flows/hosting_link.test.ts new file mode 100644 index 00000000000..7de8e29f534 --- /dev/null +++ b/packages/auth/test/integration/flows/hosting_link.test.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { + ActionCodeSettings, + Auth, + sendSignInLinkToEmail +} from '@firebase/auth'; +import { expect, use } from 'chai'; +import { + cleanUpTestInstance, + getTestInstance, + randomEmail +} from '../../helpers/integration/helpers'; +import { getEmulatorUrl } from '../../helpers/integration/settings'; +import chaiAsPromised from 'chai-as-promised'; +import { FirebaseError } from '@firebase/util'; + +use(chaiAsPromised); + +// Assumes mobileLinksConfig.domain is set as "HOSTING_DOMAIN" in the test GCP-project. +describe('Integration test: hosting link validation', () => { + let auth: Auth; + let email: string; + + const AUTHORIZED_CUSTOM_DOMAIN = 'localhost/action_code_return'; + const ANDROID_PACKAGE_NAME = 'com.google.firebase.test.thin'; + const BASE_SETTINGS: ActionCodeSettings = { + url: 'http://' + AUTHORIZED_CUSTOM_DOMAIN, + handleCodeInApp: true, + android: { packageName: ANDROID_PACKAGE_NAME } + }; + const VALID_LINK_DOMAIN = 'jscore-sandbox.testdomaindonotuse.com'; + const INVALID_LINK_DOMAIN = 'invalid.testdomaindonotuse.com'; + const INVALID_LINK_DOMAIN_ERROR = 'auth/invalid-hosting-link-domain'; + const TEST_TENANT_ID = 'passpol-tenant-d7hha'; + + beforeEach(function () { + auth = getTestInstance(); + email = randomEmail(); + + if (getEmulatorUrl()) { + this.skip(); + } + }); + + afterEach(async () => { + await cleanUpTestInstance(auth); + }); + + it('allows user to sign in with default firebase hosting link', async () => { + // Sends email link to user using default hosting link. + await sendSignInLinkToEmail(auth, email, BASE_SETTINGS); + }); + + it('allows user to sign in to a tenant with default firebase hosting link', async () => { + auth.tenantId = TEST_TENANT_ID; + // Sends email link to user using default hosting link. + await sendSignInLinkToEmail(auth, email, BASE_SETTINGS); + }); + + it('allows user to sign in with custom firebase hosting link', async () => { + // Sends email link to user using custom hosting link. + await sendSignInLinkToEmail(auth, email, { + ...BASE_SETTINGS, + linkDomain: VALID_LINK_DOMAIN + }); + }); + + it('allows user to sign in to a tenant with custom firebase hosting link', async () => { + // Sends email link to user using custom hosting link. + auth.tenantId = TEST_TENANT_ID; + await sendSignInLinkToEmail(auth, email, { + ...BASE_SETTINGS, + linkDomain: VALID_LINK_DOMAIN + }); + }); + + it('sign in with invalid firebase hosting link throws exception', async () => { + // Throws an exception while sening email link to user using invalid hosting link. + await expect( + sendSignInLinkToEmail(auth, email, { + ...BASE_SETTINGS, + linkDomain: INVALID_LINK_DOMAIN + }) + ).to.be.rejectedWith( + FirebaseError, + new RegExp('.*' + INVALID_LINK_DOMAIN_ERROR + '.*') + ); + }); +});