@@ -36,6 +36,7 @@ import {
3636} from '../../lib/auth/index' ;
3737import * as sinon from 'sinon' ;
3838import * as sinonChai from 'sinon-chai' ;
39+ import { ActionCodeSettings } from '../../src/auth/action-code-settings-builder' ;
3940
4041const chalk = require ( 'chalk' ) ; // eslint-disable-line @typescript-eslint/no-var-requires
4142
@@ -75,10 +76,18 @@ const mockUserData = {
7576 photoURL : 'http://www.example.com/' + newUserUid + '/photo.png' ,
7677 disabled : false ,
7778} ;
78- const actionCodeSettings = {
79+ const actionCodeSettings : ActionCodeSettings = {
7980 url : 'http://localhost/?a=1&b=2#c=3' ,
8081 handleCodeInApp : false ,
8182} ;
83+ const actionCodeSettingsWithCustomDomain : ActionCodeSettings = {
84+ url : 'http://localhost/?a=1&b=2#c=3' ,
85+ handleCodeInApp : true ,
86+ linkDomain : 'liubin-dev2.testdomaindonotuse.com' ,
87+ iOS : {
88+ bundleId : 'com.google.experimental1.dev.extension0' ,
89+ } ,
90+ }
8291let deleteQueue = Promise . resolve ( ) ;
8392
8493interface UserImportTest {
@@ -1199,6 +1208,114 @@ describe('admin.auth', () => {
11991208 expect ( result . user ! . emailVerified ) . to . be . true ;
12001209 } ) ;
12011210 } ) ;
1211+
1212+ it ( 'generateSignInWithEmailLink() with custom linkDomain should return error in case of invalid hosting domain' ,
1213+ function ( ) {
1214+ if ( authEmulatorHost ) {
1215+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1216+ }
1217+ const actionCodeSettingsWithInvalidLinkDomain = deepCopy ( actionCodeSettings ) ;
1218+ actionCodeSettingsWithInvalidLinkDomain . linkDomain = 'invaliddomain.firebaseapp.com' ;
1219+ return getAuth ( ) . generateSignInWithEmailLink ( email , actionCodeSettingsWithInvalidLinkDomain )
1220+ . catch ( ( error ) => {
1221+ expect ( error . code ) . to . equal ( 'auth/invalid-hosting-link-domain' ) ;
1222+ } ) ;
1223+ } ) ;
1224+
1225+ it ( 'generatePasswordResetLink() should return a password reset link with custom domain' , function ( ) {
1226+ if ( authEmulatorHost ) {
1227+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1228+ }
1229+ // Ensure old password set on created user.
1230+ return getAuth ( ) . updateUser ( uid , { password : 'password' } )
1231+ . then ( ( ) => {
1232+ return getAuth ( ) . generatePasswordResetLink ( email , actionCodeSettingsWithCustomDomain ) ;
1233+ } )
1234+ . then ( ( link ) => {
1235+ const code = getActionCode ( link ) ;
1236+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettings . url ) ;
1237+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1238+ return clientAuth ( ) . confirmPasswordReset ( code , newPassword ) ;
1239+ } )
1240+ . then ( ( ) => {
1241+ return clientAuth ( ) . signInWithEmailAndPassword ( email , newPassword ) ;
1242+ } )
1243+ . then ( ( result ) => {
1244+ expect ( result . user ) . to . exist ;
1245+ expect ( result . user ! . email ) . to . equal ( email ) ;
1246+ // Password reset also verifies the user's email.
1247+ expect ( result . user ! . emailVerified ) . to . be . true ;
1248+ } ) ;
1249+ } ) ;
1250+
1251+ it ( 'generateEmailVerificationLink() should return a verification link with custom domain' , function ( ) {
1252+ if ( authEmulatorHost ) {
1253+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1254+ }
1255+ // Ensure the user's email is unverified.
1256+ return getAuth ( ) . updateUser ( uid , { password : '123456' , emailVerified : false } )
1257+ . then ( ( userRecord ) => {
1258+ expect ( userRecord . emailVerified ) . to . be . false ;
1259+ return getAuth ( ) . generateEmailVerificationLink ( email , actionCodeSettingsWithCustomDomain ) ;
1260+ } )
1261+ . then ( ( link ) => {
1262+ const code = getActionCode ( link ) ;
1263+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettings . url ) ;
1264+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1265+ return clientAuth ( ) . applyActionCode ( code ) ;
1266+ } )
1267+ . then ( ( ) => {
1268+ return clientAuth ( ) . signInWithEmailAndPassword ( email , userData . password ) ;
1269+ } )
1270+ . then ( ( result ) => {
1271+ expect ( result . user ) . to . exist ;
1272+ expect ( result . user ! . email ) . to . equal ( email ) ;
1273+ expect ( result . user ! . emailVerified ) . to . be . true ;
1274+ } ) ;
1275+ } ) ;
1276+
1277+ it ( 'generateSignInWithEmailLink() should return a sign-in link with custom domain' , function ( ) {
1278+ if ( authEmulatorHost ) {
1279+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1280+ }
1281+ return getAuth ( ) . generateSignInWithEmailLink ( email , actionCodeSettingsWithCustomDomain )
1282+ . then ( ( link ) => {
1283+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . url ) ;
1284+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1285+ return clientAuth ( ) . signInWithEmailLink ( email , link ) ;
1286+ } )
1287+ . then ( ( result ) => {
1288+ expect ( result . user ) . to . exist ;
1289+ expect ( result . user ! . email ) . to . equal ( email ) ;
1290+ expect ( result . user ! . emailVerified ) . to . be . true ;
1291+ } ) ;
1292+ } ) ;
1293+
1294+ it ( 'generateVerifyAndChangeEmailLink() should return a verification link with custom domain' , function ( ) {
1295+ if ( authEmulatorHost ) {
1296+ return this . skip ( ) ; // Not yet supported in Auth Emulator.
1297+ }
1298+ // Ensure the user's email is verified.
1299+ return getAuth ( ) . updateUser ( uid , { password : '123456' , emailVerified : true } )
1300+ . then ( ( userRecord ) => {
1301+ expect ( userRecord . emailVerified ) . to . be . true ;
1302+ return getAuth ( ) . generateVerifyAndChangeEmailLink ( email , newEmail , actionCodeSettingsWithCustomDomain ) ;
1303+ } )
1304+ . then ( ( link ) => {
1305+ const code = getActionCode ( link ) ;
1306+ expect ( getContinueUrlForInAppRequest ( link ) ) . equal ( actionCodeSettings . url ) ;
1307+ expect ( getHostName ( link ) ) . equal ( actionCodeSettingsWithCustomDomain . linkDomain ) ;
1308+ return clientAuth ( ) . applyActionCode ( code ) ;
1309+ } )
1310+ . then ( ( ) => {
1311+ return clientAuth ( ) . signInWithEmailAndPassword ( newEmail , 'password' ) ;
1312+ } )
1313+ . then ( ( result ) => {
1314+ expect ( result . user ) . to . exist ;
1315+ expect ( result . user ! . email ) . to . equal ( newEmail ) ;
1316+ expect ( result . user ! . emailVerified ) . to . be . true ;
1317+ } ) ;
1318+ } ) ;
12021319 } ) ;
12031320
12041321 describe ( 'Project config management operations' , ( ) => {
@@ -1285,6 +1402,9 @@ describe('admin.auth', () => {
12851402 } ,
12861403 emailPrivacyConfig : {
12871404 enableImprovedEmailPrivacy : true ,
1405+ } ,
1406+ mobileLinksConfig : {
1407+ domain : 'HOSTING_DOMAIN' ,
12881408 }
12891409 } ;
12901410 const projectConfigOption2 : UpdateProjectConfigRequest = {
@@ -1318,6 +1438,9 @@ describe('admin.auth', () => {
13181438 emailPrivacyConfig : {
13191439 enableImprovedEmailPrivacy : true ,
13201440 } ,
1441+ mobileLinksConfig : {
1442+ domain : 'HOSTING_DOMAIN' ,
1443+ } ,
13211444 } ;
13221445 const expectedProjectConfig2 : any = {
13231446 smsRegionConfig : smsRegionAllowlistOnlyConfig ,
@@ -1333,6 +1456,9 @@ describe('admin.auth', () => {
13331456 ] ,
13341457 } ,
13351458 emailPrivacyConfig : { } ,
1459+ mobileLinksConfig : {
1460+ domain : 'HOSTING_DOMAIN' ,
1461+ } ,
13361462 } ;
13371463 const expectedProjectConfigSmsEnabledTotpDisabled : any = {
13381464 smsRegionConfig : smsRegionAllowlistOnlyConfig ,
@@ -1348,6 +1474,9 @@ describe('admin.auth', () => {
13481474 ] ,
13491475 } ,
13501476 emailPrivacyConfig : { } ,
1477+ mobileLinksConfig : {
1478+ domain : 'HOSTING_DOMAIN' ,
1479+ } ,
13511480 } ;
13521481
13531482 it ( 'updateProjectConfig() should resolve with the updated project config' , ( ) => {
@@ -3194,6 +3323,38 @@ function getContinueUrl(link: string): string {
31943323 return continueUrl ! ;
31953324}
31963325
3326+ /**
3327+ * Returns the host name corresponding to the link.
3328+ *
3329+ * @param link The link to parse for hostname
3330+ * @returns Hostname in the link
3331+ */
3332+ function getHostName ( link : string ) : string {
3333+ const parsedUrl = new url . URL ( link ) ;
3334+ return parsedUrl . hostname ;
3335+ }
3336+
3337+ /**
3338+ * Returns continue URL for handling in app requests.
3339+ * URL will be of the form, http://abc/__/auth/link?link=<action link url>
3340+ * Coninue URL will be part of action link url
3341+ *
3342+ * @param link
3343+ * @returns
3344+ */
3345+ function getContinueUrlForInAppRequest ( link : string ) : string {
3346+ // Extract action url from link param
3347+ const parsedUrl = new url . URL ( link ) ;
3348+ const linkParam = parsedUrl . searchParams . get ( 'link' ) ?? '' ;
3349+ expect ( linkParam ) . is . not . empty ;
3350+
3351+ // Extract continueUrl param from action url
3352+ const actionUrl = new url . URL ( linkParam ) ;
3353+ const continueUrl = actionUrl . searchParams . get ( 'continueUrl' ) ;
3354+ expect ( continueUrl ) . to . exist ;
3355+ return continueUrl ! ;
3356+ }
3357+
31973358/**
31983359 * Returns the tenant ID corresponding to the link.
31993360 *
0 commit comments