@@ -32,7 +32,7 @@ import {
3232 AuthProviderConfig , CreateTenantRequest , DeleteUsersResult , PhoneMultiFactorInfo ,
3333 TenantAwareAuth , UpdatePhoneMultiFactorInfoRequest , UpdateTenantRequest , UserImportOptions ,
3434 UserImportRecord , UserRecord , getAuth , UpdateProjectConfigRequest , UserMetadata , MultiFactorConfig ,
35- PasswordPolicyConfig , SmsRegionConfig ,
35+ PasswordPolicyConfig , SmsRegionConfig , ActionCodeSettings ,
3636} from '../../lib/auth/index' ;
3737import * as sinon from 'sinon' ;
3838import * as sinonChai from 'sinon-chai' ;
@@ -75,10 +75,25 @@ const mockUserData = {
7575 photoURL : 'http://www.example.com/' + newUserUid + '/photo.png' ,
7676 disabled : false ,
7777} ;
78- const actionCodeSettings = {
78+ const actionCodeSettings : ActionCodeSettings = {
7979 url : 'http://localhost/?a=1&b=2#c=3' ,
8080 handleCodeInApp : false ,
8181} ;
82+ const actionCodeSettingsWithCustomDomain : ActionCodeSettings = {
83+ url : 'http://localhost/?a=1&b=2#c=3' ,
84+ handleCodeInApp : true ,
85+ linkDomain : 'kobayashimaru.testdomaindonotuse.com' ,
86+ iOS : {
87+ bundleId : 'testBundleId' ,
88+ } ,
89+ }
90+ const actionCodeSettingsForFdlLinks : ActionCodeSettings = {
91+ url : 'http://localhost/?a=1&b=2#c=3' ,
92+ handleCodeInApp : true ,
93+ iOS : {
94+ bundleId : 'testBundleId' ,
95+ } ,
96+ }
8297let deleteQueue = Promise . resolve ( ) ;
8398
8499interface UserImportTest {
@@ -1105,6 +1120,13 @@ describe('admin.auth', () => {
11051120
11061121 // Create the test user before running this suite of tests.
11071122 before ( ( ) => {
1123+ // Update project config to have HOSTING_DOMAIN as mobileLinksConfig after each test
1124+ const updateMobileLinksRequest : UpdateProjectConfigRequest = {
1125+ mobileLinksConfig : {
1126+ domain : 'HOSTING_DOMAIN' ,
1127+ } ,
1128+ } ;
1129+ getAuth ( ) . projectConfigManager ( ) . updateProjectConfig ( updateMobileLinksRequest ) ;
11081130 return getAuth ( ) . createUser ( userData ) ;
11091131 } ) ;
11101132
@@ -1199,6 +1221,162 @@ describe('admin.auth', () => {
11991221 expect ( result . user ! . emailVerified ) . to . be . true ;
12001222 } ) ;
12011223 } ) ;
1224+
1225+ it ( 'generateSignInWithEmailLink() with custom linkDomain should return error in case of invalid hosting domain' ,
1226+ function ( ) {
1227+ if ( authEmulatorHost ) {
1228+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1229+ }
1230+ const actionCodeSettingsWithInvalidLinkDomain = deepCopy ( actionCodeSettings ) ;
1231+ actionCodeSettingsWithInvalidLinkDomain . linkDomain = 'invaliddomain.firebaseapp.com' ;
1232+ return getAuth ( ) . generateSignInWithEmailLink ( email , actionCodeSettingsWithInvalidLinkDomain )
1233+ . catch ( ( error ) => {
1234+ expect ( error . code ) . to . equal ( 'auth/invalid-hosting-link-domain' ) ;
1235+ } ) ;
1236+ } ) ;
1237+
1238+ it ( 'generatePasswordResetLink() should return a password reset link with custom domain' , function ( ) {
1239+ if ( authEmulatorHost ) {
1240+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1241+ }
1242+ // Ensure old password set on created user.
1243+ return getAuth ( ) . updateUser ( uid , { password : 'password' } )
1244+ . then ( ( ) => {
1245+ return getAuth ( ) . generatePasswordResetLink ( email , actionCodeSettingsWithCustomDomain ) ;
1246+ } )
1247+ . then ( ( link ) => {
1248+ const code = getActionCode ( link ) ;
1249+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettings . url ) ;
1250+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1251+ return clientAuth ( ) . confirmPasswordReset ( code , newPassword ) ;
1252+ } )
1253+ . then ( ( ) => {
1254+ return clientAuth ( ) . signInWithEmailAndPassword ( email , newPassword ) ;
1255+ } )
1256+ . then ( ( result ) => {
1257+ expect ( result . user ) . to . exist ;
1258+ expect ( result . user ! . email ) . to . equal ( email ) ;
1259+ // Password reset also verifies the user's email.
1260+ expect ( result . user ! . emailVerified ) . to . be . true ;
1261+ } ) ;
1262+ } ) ;
1263+
1264+ it ( 'generateEmailVerificationLink() should return a verification link with custom domain' , function ( ) {
1265+ if ( authEmulatorHost ) {
1266+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1267+ }
1268+ // Ensure the user's email is unverified.
1269+ return getAuth ( ) . updateUser ( uid , { password : '123456' , emailVerified : false } )
1270+ . then ( ( userRecord ) => {
1271+ expect ( userRecord . emailVerified ) . to . be . false ;
1272+ return getAuth ( ) . generateEmailVerificationLink ( email , actionCodeSettingsWithCustomDomain ) ;
1273+ } )
1274+ . then ( ( link ) => {
1275+ const code = getActionCode ( link ) ;
1276+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettings . url ) ;
1277+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1278+ return clientAuth ( ) . applyActionCode ( code ) ;
1279+ } )
1280+ . then ( ( ) => {
1281+ return clientAuth ( ) . signInWithEmailAndPassword ( email , userData . password ) ;
1282+ } )
1283+ . then ( ( result ) => {
1284+ expect ( result . user ) . to . exist ;
1285+ expect ( result . user ! . email ) . to . equal ( email ) ;
1286+ expect ( result . user ! . emailVerified ) . to . be . true ;
1287+ } ) ;
1288+ } ) ;
1289+
1290+ it ( 'generateSignInWithEmailLink() should return a sign-in link with custom domain' , function ( ) {
1291+ if ( authEmulatorHost ) {
1292+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1293+ }
1294+ return getAuth ( ) . generateSignInWithEmailLink ( email , actionCodeSettingsWithCustomDomain )
1295+ . then ( ( link ) => {
1296+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . url ) ;
1297+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1298+ return clientAuth ( ) . signInWithEmailLink ( email , link ) ;
1299+ } )
1300+ . then ( ( result ) => {
1301+ expect ( result . user ) . to . exist ;
1302+ expect ( result . user ! . email ) . to . equal ( email ) ;
1303+ expect ( result . user ! . emailVerified ) . to . be . true ;
1304+ } ) ;
1305+ } ) ;
1306+
1307+ it ( 'generateVerifyAndChangeEmailLink() should return a verification link with custom domain' , function ( ) {
1308+ if ( authEmulatorHost ) {
1309+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1310+ }
1311+ // Ensure the user's email is verified.
1312+ return getAuth ( ) . updateUser ( uid , { password : '123456' , emailVerified : true } )
1313+ . then ( ( userRecord ) => {
1314+ expect ( userRecord . emailVerified ) . to . be . true ;
1315+ return getAuth ( ) . generateVerifyAndChangeEmailLink ( email , newEmail , actionCodeSettingsWithCustomDomain ) ;
1316+ } )
1317+ . then ( ( link ) => {
1318+ const code = getActionCode ( link ) ;
1319+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettings . url ) ;
1320+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1321+ return clientAuth ( ) . applyActionCode ( code ) ;
1322+ } )
1323+ . then ( ( ) => {
1324+ return clientAuth ( ) . signInWithEmailAndPassword ( newEmail , 'password' ) ;
1325+ } )
1326+ . then ( ( result ) => {
1327+ expect ( result . user ) . to . exist ;
1328+ expect ( result . user ! . email ) . to . equal ( newEmail ) ;
1329+ expect ( result . user ! . emailVerified ) . to . be . true ;
1330+ } ) ;
1331+ } ) ;
1332+
1333+ it ( 'generateSignInWithEmailLink() should return a FDL sign-in'
1334+ + 'link with mobileLinksConfig set to FIREBASE_DYNAMIC_LINK_DOMAIN' , function ( ) {
1335+ if ( authEmulatorHost ) {
1336+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1337+ }
1338+
1339+ const updateMobileLinksRequest : UpdateProjectConfigRequest = {
1340+ mobileLinksConfig : {
1341+ domain : 'FIREBASE_DYNAMIC_LINK_DOMAIN' ,
1342+ }
1343+ } ;
1344+ return getAuth ( ) . projectConfigManager ( ) . updateProjectConfig ( updateMobileLinksRequest )
1345+ . then ( ( projectConfig ) => {
1346+ expect ( projectConfig ?. mobileLinksConfig ?. domain ) . equal ( 'FIREBASE_DYNAMIC_LINK_DOMAIN' ) ;
1347+ return getAuth ( ) . generateSignInWithEmailLink ( email , actionCodeSettingsForFdlLinks ) ;
1348+ } ) . then ( ( link ) => {
1349+ expectFDLLink ( link ) ;
1350+ return clientAuth ( ) . signInWithEmailLink ( email , link ) ;
1351+ } ) . then ( ( result ) => {
1352+ expect ( result . user ) . to . exist ;
1353+ expect ( result . user ! . email ) . to . equal ( email ) ;
1354+ expect ( result . user ! . emailVerified ) . to . be . true ;
1355+ } ) ;
1356+ } ) ;
1357+
1358+ it ( 'generateSignInWithEmailLink() should return a FDL sign-in link with empty mobileLinksConfig' , function ( ) {
1359+ if ( authEmulatorHost ) {
1360+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1361+ }
1362+
1363+ const updateMobileLinksRequest : UpdateProjectConfigRequest = {
1364+ mobileLinksConfig : {
1365+ }
1366+ } ;
1367+ return getAuth ( ) . projectConfigManager ( ) . updateProjectConfig ( updateMobileLinksRequest )
1368+ . then ( ( projectConfig ) => {
1369+ expect ( projectConfig . mobileLinksConfig ) . is . empty ;
1370+ return getAuth ( ) . generateSignInWithEmailLink ( email , actionCodeSettingsForFdlLinks ) ;
1371+ } ) . then ( ( link ) => {
1372+ expectFDLLink ( link ) ;
1373+ return clientAuth ( ) . signInWithEmailLink ( email , link ) ;
1374+ } ) . then ( ( result ) => {
1375+ expect ( result . user ) . to . exist ;
1376+ expect ( result . user ! . email ) . to . equal ( email ) ;
1377+ expect ( result . user ! . emailVerified ) . to . be . true ;
1378+ } ) ;
1379+ } ) ;
12021380 } ) ;
12031381
12041382 describe ( 'Project config management operations' , ( ) => {
@@ -1285,6 +1463,9 @@ describe('admin.auth', () => {
12851463 } ,
12861464 emailPrivacyConfig : {
12871465 enableImprovedEmailPrivacy : true ,
1466+ } ,
1467+ mobileLinksConfig : {
1468+ domain : 'HOSTING_DOMAIN' ,
12881469 }
12891470 } ;
12901471 const projectConfigOption2 : UpdateProjectConfigRequest = {
@@ -1318,6 +1499,9 @@ describe('admin.auth', () => {
13181499 emailPrivacyConfig : {
13191500 enableImprovedEmailPrivacy : true ,
13201501 } ,
1502+ mobileLinksConfig : {
1503+ domain : 'HOSTING_DOMAIN' ,
1504+ } ,
13211505 } ;
13221506 const expectedProjectConfig2 : any = {
13231507 smsRegionConfig : smsRegionAllowlistOnlyConfig ,
@@ -1333,6 +1517,9 @@ describe('admin.auth', () => {
13331517 ] ,
13341518 } ,
13351519 emailPrivacyConfig : { } ,
1520+ mobileLinksConfig : {
1521+ domain : 'HOSTING_DOMAIN' ,
1522+ } ,
13361523 } ;
13371524 const expectedProjectConfigSmsEnabledTotpDisabled : any = {
13381525 smsRegionConfig : smsRegionAllowlistOnlyConfig ,
@@ -1348,6 +1535,9 @@ describe('admin.auth', () => {
13481535 ] ,
13491536 } ,
13501537 emailPrivacyConfig : { } ,
1538+ mobileLinksConfig : {
1539+ domain : 'HOSTING_DOMAIN' ,
1540+ } ,
13511541 } ;
13521542
13531543 it ( 'updateProjectConfig() should resolve with the updated project config' , ( ) => {
@@ -3194,6 +3384,53 @@ function getContinueUrl(link: string): string {
31943384 return continueUrl ! ;
31953385}
31963386
3387+ /**
3388+ * Returns the host name corresponding to the link.
3389+ *
3390+ * @param link The link to parse for hostname
3391+ * @returns Hostname in the link
3392+ */
3393+ function getHostName ( link : string ) : string {
3394+ const parsedUrl = new url . URL ( link ) ;
3395+ return parsedUrl . hostname ;
3396+ }
3397+
3398+ /**
3399+ * Returns continue URL for handling in app requests.
3400+ * URL will be of the form, http://abc/__/auth/link?link=<action link url>
3401+ * Coninue URL will be part of action link url
3402+ *
3403+ * @param link The link to parse for continue url
3404+ * @returns
3405+ */
3406+ function getContinueUrlForInAppRequest ( link : string ) : string {
3407+ // Extract action url from link param
3408+ const parsedUrl = new url . URL ( link ) ;
3409+ const linkParam = parsedUrl . searchParams . get ( 'link' ) ?? '' ;
3410+ expect ( linkParam ) . is . not . empty ;
3411+
3412+ // Extract continueUrl param from action url
3413+ const actionUrl = new url . URL ( linkParam ) ;
3414+ const continueUrl = actionUrl . searchParams . get ( 'continueUrl' ) ;
3415+ expect ( continueUrl ) . to . exist ;
3416+ return continueUrl ! ;
3417+ }
3418+
3419+ /**
3420+ * Verify if the generated link is generated by FDL
3421+ * We leverage the params created by FDL to test whether a given link is FDL
3422+ *
3423+ * @param link Link to check whether it is FDL
3424+ */
3425+ function expectFDLLink ( link : string ) : void {
3426+ const parsedUrl = new url . URL ( link ) ;
3427+ // For ios, FDL creates a fallback url with param ifl
3428+ // We leverage that to test whether a given link is FDL link
3429+ // Note: This param does not exist when the link is generated for HOSTING_DOMAIN
3430+ const iflParam = parsedUrl . searchParams . get ( 'ifl' ) ;
3431+ expect ( iflParam ) . is . not . null ;
3432+ }
3433+
31973434/**
31983435 * Returns the tenant ID corresponding to the link.
31993436 *
0 commit comments