diff --git a/common/api-review/app.api.md b/common/api-review/app.api.md index 266e7730610..087318ae27f 100644 --- a/common/api-review/app.api.md +++ b/common/api-review/app.api.md @@ -77,6 +77,7 @@ export interface FirebaseServerApp extends FirebaseApp { authIdTokenVerified: () => Promise; installationTokenVerified: () => Promise; name: string; + readonly settings: FirebaseServerAppSettings; } // @public @@ -119,6 +120,9 @@ export function initializeServerApp(options: FirebaseOptions | FirebaseApp, conf // @internal (undocumented) export function _isFirebaseApp(obj: FirebaseApp | FirebaseOptions): obj is FirebaseApp; +// @internal (undocumented) +export function _isFirebaseServerApp(obj: FirebaseApp | FirebaseServerApp): obj is FirebaseServerApp; + // @public export function onLog(logCallback: LogCallback | null, options?: LogOptions): void; diff --git a/docs-devsite/app.firebaseserverapp.md b/docs-devsite/app.firebaseserverapp.md index cf41ee0a633..387e27a9822 100644 --- a/docs-devsite/app.firebaseserverapp.md +++ b/docs-devsite/app.firebaseserverapp.md @@ -29,6 +29,7 @@ export interface FirebaseServerApp extends FirebaseApp | [authIdTokenVerified](./app.firebaseserverapp.md#firebaseserverappauthidtokenverified) | () => Promise<void> | Checks to see if the verification of the authIdToken provided to has completed.It is recommend that your application awaits this promise if an authIdToken was provided during FirebaseServerApp initialization before invoking getAuth(). If an instance of Auth is created before the Auth ID Token is validated, then the token will not be used by that instance of the Auth SDK.The returned Promise is completed immediately if the optional authIdToken parameter was omitted from FirebaseServerApp initialization. | | [installationTokenVerified](./app.firebaseserverapp.md#firebaseserverappinstallationtokenverified) | () => Promise<void> | Checks to see if the verification of the installationToken provided to has completed.It is recommend that your application awaits this promise before initializing any Firebase products that use Firebase Installations. The Firebase SDKs will not use Installation Auth tokens that are determined to be invalid or those that have not yet completed validation.The returned Promise is completed immediately if the optional appCheckToken parameter was omitted from FirebaseServerApp initialization. | | [name](./app.firebaseserverapp.md#firebaseserverappname) | string | There is no get for FirebaseServerApp, so the name is not relevant. However, it's declared here so that FirebaseServerApp conforms to the FirebaseApp interface declaration. Internally this string will always be empty for FirebaseServerApp instances. | +| [settings](./app.firebaseserverapp.md#firebaseserverappsettings) | [FirebaseServerAppSettings](./app.firebaseserverappsettings.md#firebaseserverappsettings_interface) | The (read-only) configuration settings for this server app. These are the original parameters given in [initializeServerApp()](./app.md#initializeserverapp_30ab697). | ## FirebaseServerApp.appCheckTokenVerified @@ -81,3 +82,23 @@ There is no get for FirebaseServerApp, so the name is not relevant. However, it' ```typescript name: string; ``` + +## FirebaseServerApp.settings + +The (read-only) configuration settings for this server app. These are the original parameters given in [initializeServerApp()](./app.md#initializeserverapp_30ab697). + +Signature: + +```typescript +readonly settings: FirebaseServerAppSettings; +``` + +### Example + + +```javascript +const app = initializeServerApp(settings); +console.log(app.settings.authIdToken === options.authIdToken); // true + +``` + diff --git a/packages/app/src/firebaseServerApp.ts b/packages/app/src/firebaseServerApp.ts index cc4264ae504..7c51511d087 100644 --- a/packages/app/src/firebaseServerApp.ts +++ b/packages/app/src/firebaseServerApp.ts @@ -83,7 +83,7 @@ export class FirebaseServerAppImpl void deleteApp(serverApp); } - get serverAppConfig(): FirebaseServerAppSettings { + get settings(): FirebaseServerAppSettings { this.checkDestroyed(); return this._serverConfig; } diff --git a/packages/app/src/internal.ts b/packages/app/src/internal.ts index 94af4b9434c..da0ba3f73fc 100644 --- a/packages/app/src/internal.ts +++ b/packages/app/src/internal.ts @@ -155,6 +155,20 @@ export function _isFirebaseApp( return (obj as FirebaseApp).options !== undefined; } +/** + * + * @param obj - an object of type FirebaseApp. + * + * @returns true if the provided object is of type FirebaseServerAppImpl. + * + * @internal + */ +export function _isFirebaseServerApp( + obj: FirebaseApp | FirebaseServerApp +): obj is FirebaseServerApp { + return (obj as FirebaseServerApp).authIdTokenVerified !== undefined; +} + /** * Test only * diff --git a/packages/app/src/public-types.ts b/packages/app/src/public-types.ts index f179e31dac3..04ba4f0b24c 100644 --- a/packages/app/src/public-types.ts +++ b/packages/app/src/public-types.ts @@ -132,6 +132,18 @@ export interface FirebaseServerApp extends FirebaseApp { * string will always be empty for FirebaseServerApp instances. */ name: string; + + /** + * The (read-only) configuration settings for this server app. These are the original + * parameters given in {@link (initializeServerApp:1) | initializeServerApp()}. + * + * @example + * ```javascript + * const app = initializeServerApp(settings); + * console.log(app.settings.authIdToken === options.authIdToken); // true + * ``` + */ + readonly settings: FirebaseServerAppSettings; } /** diff --git a/packages/auth/karma.conf.js b/packages/auth/karma.conf.js index 6845f0bd91d..198b079a15b 100644 --- a/packages/auth/karma.conf.js +++ b/packages/auth/karma.conf.js @@ -65,7 +65,8 @@ function getTestFiles(argv) { 'src/**/*.test.ts', 'test/helpers/**/*.test.ts', 'test/integration/flows/anonymous.test.ts', - 'test/integration/flows/email.test.ts' + 'test/integration/flows/email.test.ts', + 'test/integration/flows/firebaseserverapp.test.ts' ]; } } diff --git a/packages/auth/scripts/run_node_tests.ts b/packages/auth/scripts/run_node_tests.ts index 2bfc593d8fd..ce913612f64 100644 --- a/packages/auth/scripts/run_node_tests.ts +++ b/packages/auth/scripts/run_node_tests.ts @@ -48,7 +48,9 @@ let testConfig = [ ]; if (argv.integration) { - testConfig = ['test/integration/flows/{email,anonymous}.test.ts']; + testConfig = [ + 'test/integration/flows/{email,anonymous,firebaseserverapp}.test.ts' + ]; if (argv.local) { testConfig.push('test/integration/flows/*.local.test.ts'); } diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index cd75276e006..55f474eda9e 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -15,7 +15,11 @@ * limitations under the License. */ -import { _FirebaseService, FirebaseApp } from '@firebase/app'; +import { + _isFirebaseServerApp, + _FirebaseService, + FirebaseApp +} from '@firebase/app'; import { Provider } from '@firebase/component'; import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types'; import { @@ -167,7 +171,11 @@ export class AuthImpl implements AuthInternal, _FirebaseService { } } - await this.initializeCurrentUser(popupRedirectResolver); + // Skip loading users from persistence in FirebaseServerApp Auth instances. + if (!_isFirebaseServerApp(this.app)) { + await this.initializeCurrentUser(popupRedirectResolver); + } + this.lastNotifiedUid = this.currentUser?.uid || null; if (this._deleted) { diff --git a/packages/auth/src/core/auth/initialize.ts b/packages/auth/src/core/auth/initialize.ts index c6218953508..3bb21364718 100644 --- a/packages/auth/src/core/auth/initialize.ts +++ b/packages/auth/src/core/auth/initialize.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { _getProvider, FirebaseApp } from '@firebase/app'; +import { _getProvider, _isFirebaseServerApp, FirebaseApp } from '@firebase/app'; import { deepEqual } from '@firebase/util'; import { Auth, Dependencies } from '../../model/public_types'; @@ -23,7 +23,9 @@ import { AuthErrorCode } from '../errors'; import { PersistenceInternal } from '../persistence'; import { _fail } from '../util/assert'; import { _getInstance } from '../util/instantiator'; -import { AuthImpl } from './auth_impl'; +import { AuthImpl, _castAuth } from './auth_impl'; +import { UserImpl } from '../user/user_impl'; +import { getAccountInfo } from '../../api/account_management/account'; /** * Initializes an {@link Auth} instance with fine-grained control over @@ -65,9 +67,40 @@ export function initializeAuth(app: FirebaseApp, deps?: Dependencies): Auth { const auth = provider.initialize({ options: deps }) as AuthImpl; + if (_isFirebaseServerApp(app)) { + if (app.settings.authIdToken !== undefined) { + const idToken = app.settings.authIdToken; + // Start the auth operation in the next tick to allow a moment for the customer's app to + // attach an emulator, if desired. + setTimeout(() => void _loadUserFromIdToken(auth, idToken), 0); + } + } + return auth; } +export async function _loadUserFromIdToken( + auth: Auth, + idToken: string +): Promise { + try { + const response = await getAccountInfo(auth, { idToken }); + const authInternal = _castAuth(auth); + await authInternal._initializationPromise; + const user = await UserImpl._fromGetAccountInfoResponse( + authInternal, + response, + idToken + ); + await authInternal._updateCurrentUser(user); + } catch (err) { + console.warn( + 'FirebaseServerApp could not login user with provided authIdToken: ', + err + ); + } +} + export function _initializeAuthInstance( auth: AuthImpl, deps?: Dependencies diff --git a/packages/auth/src/core/user/reload.ts b/packages/auth/src/core/user/reload.ts index fc0a33b937a..ac9a1683e2d 100644 --- a/packages/auth/src/core/user/reload.ts +++ b/packages/auth/src/core/user/reload.ts @@ -102,7 +102,7 @@ function mergeProviderData( return [...deduped, ...newData]; } -function extractProviderData(providers: ProviderUserInfo[]): UserInfo[] { +export function extractProviderData(providers: ProviderUserInfo[]): UserInfo[] { return providers.map(({ providerId, ...provider }) => { return { providerId, diff --git a/packages/auth/src/core/user/token_manager.test.ts b/packages/auth/src/core/user/token_manager.test.ts index e1648d4eb32..b2e1609692f 100644 --- a/packages/auth/src/core/user/token_manager.test.ts +++ b/packages/auth/src/core/user/token_manager.test.ts @@ -144,8 +144,35 @@ describe('core/user/token_manager', () => { }); }); - it('returns null if the refresh token is missing', async () => { - expect(await stsTokenManager.getToken(auth)).to.be.null; + it('returns non-null if the refresh token is missing but token still valid', async () => { + Object.assign(stsTokenManager, { + accessToken: 'token', + expirationTime: now + 100_000 + }); + const tokens = await stsTokenManager.getToken(auth, false); + expect(tokens).to.eql('token'); + }); + + it('throws an error if the refresh token is missing and force refresh is true', async () => { + Object.assign(stsTokenManager, { + accessToken: 'token', + expirationTime: now + 100_000 + }); + await expect(stsTokenManager.getToken(auth, true)).to.be.rejectedWith( + FirebaseError, + "Firebase: The user's credential is no longer valid. The user must sign in again. (auth/user-token-expired)" + ); + }); + + it('throws an error if the refresh token is missing and token is no longer valid', async () => { + Object.assign(stsTokenManager, { + accessToken: 'old-access-token', + expirationTime: now - 1 + }); + await expect(stsTokenManager.getToken(auth)).to.be.rejectedWith( + FirebaseError, + "Firebase: The user's credential is no longer valid. The user must sign in again. (auth/user-token-expired)" + ); }); it('throws an error if expired but refresh token is missing', async () => { diff --git a/packages/auth/src/core/user/token_manager.ts b/packages/auth/src/core/user/token_manager.ts index 5f56f88afb6..14969005d89 100644 --- a/packages/auth/src/core/user/token_manager.ts +++ b/packages/auth/src/core/user/token_manager.ts @@ -73,20 +73,22 @@ export class StsTokenManager { ); } + updateFromIdToken(idToken: string): void { + _assert(idToken.length !== 0, AuthErrorCode.INTERNAL_ERROR); + const expiresIn = _tokenExpiresIn(idToken); + this.updateTokensAndExpiration(idToken, null, expiresIn); + } + async getToken( auth: AuthInternal, forceRefresh = false ): Promise { - _assert( - !this.accessToken || this.refreshToken, - auth, - AuthErrorCode.TOKEN_EXPIRED - ); - if (!forceRefresh && this.accessToken && !this.isExpired) { return this.accessToken; } + _assert(this.refreshToken, auth, AuthErrorCode.TOKEN_EXPIRED); + if (this.refreshToken) { await this.refresh(auth, this.refreshToken!); return this.accessToken; @@ -113,7 +115,7 @@ export class StsTokenManager { private updateTokensAndExpiration( accessToken: string, - refreshToken: string, + refreshToken: string | null, expiresInSec: number ): void { this.refreshToken = refreshToken || null; diff --git a/packages/auth/src/core/user/user_impl.ts b/packages/auth/src/core/user/user_impl.ts index 44192cc4617..0aa91861cb9 100644 --- a/packages/auth/src/core/user/user_impl.ts +++ b/packages/auth/src/core/user/user_impl.ts @@ -15,11 +15,11 @@ * limitations under the License. */ -import { IdTokenResult } from '../../model/public_types'; +import { IdTokenResult, UserInfo } from '../../model/public_types'; import { NextFn } from '@firebase/util'; - import { APIUserInfo, + GetAccountInfoResponse, deleteAccount } from '../../api/account_management/account'; import { FinalizeMfaResponse } from '../../api/authentication/mfa'; @@ -36,7 +36,7 @@ import { _assert } from '../util/assert'; import { getIdTokenResult } from './id_token_result'; import { _logoutIfInvalidated } from './invalidation'; import { ProactiveRefresh } from './proactive_refresh'; -import { _reloadWithoutSaving, reload } from './reload'; +import { extractProviderData, _reloadWithoutSaving, reload } from './reload'; import { StsTokenManager } from './token_manager'; import { UserMetadata } from './user_metadata'; import { ProviderId } from '../../model/enums'; @@ -333,4 +333,59 @@ export class UserImpl implements UserInternal { await _reloadWithoutSaving(user); return user; } + + /** + * Initialize a User from an idToken server response + * @param auth + * @param idTokenResponse + */ + static async _fromGetAccountInfoResponse( + auth: AuthInternal, + response: GetAccountInfoResponse, + idToken: string + ): Promise { + const coreAccount = response.users[0]; + _assert(coreAccount.localId !== undefined, AuthErrorCode.INTERNAL_ERROR); + + const providerData: UserInfo[] = + coreAccount.providerUserInfo !== undefined + ? extractProviderData(coreAccount.providerUserInfo) + : []; + + const isAnonymous = + !(coreAccount.email && coreAccount.passwordHash) && !providerData?.length; + + const stsTokenManager = new StsTokenManager(); + stsTokenManager.updateFromIdToken(idToken); + + // Initialize the Firebase Auth user. + const user = new UserImpl({ + uid: coreAccount.localId, + auth, + stsTokenManager, + isAnonymous + }); + + // update the user with data from the GetAccountInfo response. + const updates: Partial = { + uid: coreAccount.localId, + displayName: coreAccount.displayName || null, + photoURL: coreAccount.photoUrl || null, + email: coreAccount.email || null, + emailVerified: coreAccount.emailVerified || false, + phoneNumber: coreAccount.phoneNumber || null, + tenantId: coreAccount.tenantId || null, + providerData, + metadata: new UserMetadata( + coreAccount.createdAt, + coreAccount.lastLoginAt + ), + isAnonymous: + !(coreAccount.email && coreAccount.passwordHash) && + !providerData?.length + }; + + Object.assign(user, updates); + return user; + } } diff --git a/packages/auth/test/helpers/integration/helpers.ts b/packages/auth/test/helpers/integration/helpers.ts index 9825a8f4ba0..17865228784 100644 --- a/packages/auth/test/helpers/integration/helpers.ts +++ b/packages/auth/test/helpers/integration/helpers.ts @@ -16,7 +16,7 @@ */ import * as sinon from 'sinon'; -import { deleteApp, initializeApp } from '@firebase/app'; +import { FirebaseServerApp, deleteApp, initializeApp } from '@firebase/app'; import { Auth, User } from '@firebase/auth'; import { getAuth, connectAuthEmulator } from '../../../'; // Use browser OR node dist entrypoint depending on test env. @@ -80,6 +80,33 @@ export function getTestInstance(requireEmulator = false): Auth { return auth; } +export function getTestInstanceForServerApp( + serverApp: FirebaseServerApp, + requireEmulator = false +): Auth { + const auth = getAuth(serverApp) as IntegrationTestAuth; + auth.settings.appVerificationDisabledForTesting = true; + const emulatorUrl = getEmulatorUrl(); + + if (emulatorUrl) { + connectAuthEmulator(auth, emulatorUrl, { disableWarnings: true }); + } else if (requireEmulator) { + /* Emulator wasn't configured but test must use emulator */ + throw new Error('Test may only be run using the Auth Emulator!'); + } + + // Don't track created users on the created Auth instance like we do for Auth objects created in + // getTestInstance(...) above. FirebaseServerApp testing re-uses users created by the Auth + // instances returned by getTestInstance, so those Auth cleanup routines will suffice. + auth.cleanUp = async () => { + // If we're in an emulated environment, the emulator will clean up for us. + //if (emulatorUrl) { + // await resetEmulator(); + //} + }; + return auth; +} + export async function cleanUpTestInstance(auth: Auth): Promise { await auth.signOut(); await (auth as IntegrationTestAuth).cleanUp(); diff --git a/packages/auth/test/integration/flows/firebaseserverapp.test.ts b/packages/auth/test/integration/flows/firebaseserverapp.test.ts new file mode 100644 index 00000000000..42b14eb2edc --- /dev/null +++ b/packages/auth/test/integration/flows/firebaseserverapp.test.ts @@ -0,0 +1,391 @@ +/** + * @license + * Copyright 2024 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. + */ + +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +// eslint-disable-next-line import/no-extraneous-dependencies +import { + Auth, + OperationType, + createUserWithEmailAndPassword, + getAdditionalUserInfo, + onAuthStateChanged, + signInAnonymously, + signOut, + updateProfile +} from '@firebase/auth'; +import { isBrowser } from '@firebase/util'; +import { initializeServerApp } from '@firebase/app'; + +import { + cleanUpTestInstance, + getTestInstance, + getTestInstanceForServerApp, + randomEmail +} from '../../helpers/integration/helpers'; + +import { getAppConfig } from '../../helpers/integration/settings'; + +use(chaiAsPromised); + +const signInWaitDuration = 200; + +describe('Integration test: Auth FirebaseServerApp tests', () => { + let auth: Auth; + let serverAppAuth: Auth | null; + + beforeEach(() => { + auth = getTestInstance(); + }); + + afterEach(async () => { + if (serverAppAuth) { + await signOut(serverAppAuth); + serverAppAuth = null; + } + await cleanUpTestInstance(auth); + }); + + it('signs in with anonymous user', async () => { + if (isBrowser()) { + return; + } + const userCred = await signInAnonymously(auth); + expect(auth.currentUser).to.eq(userCred.user); + expect(userCred.operationType).to.eq(OperationType.SIGN_IN); + const user = userCred.user; + expect(user).to.equal(auth.currentUser); + expect(user.isAnonymous).to.be.true; + expect(user.uid).to.be.a('string'); + expect(user.emailVerified).to.be.false; + expect(user.providerData.length).to.equal(0); + + const authIdToken = await user.getIdToken(); + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp(auth.app, firebaseServerAppSettings); + serverAppAuth = getTestInstanceForServerApp(serverApp); + + console.log('auth.emulatorConfig ', auth.emulatorConfig); + console.log('serverAuth.emulatorConfig ', serverAppAuth.emulatorConfig); + + let numberServerLogins = 0; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + + // Note, the serverAuthUser does not fully equal the standard Auth user + // since the serverAuthUser does not have a refresh token. + expect(user.uid).to.be.equal(serverAuthUser.uid); + expect(user.isAnonymous).to.be.equal(serverAuthUser.isAnonymous); + expect(user.emailVerified).to.be.equal(serverAuthUser.emailVerified); + expect(user.providerData.length).to.eq( + serverAuthUser.providerData.length + ); + } + }); + + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(numberServerLogins).to.equal(1); + }); + + it('getToken operations fullfilled or rejected', async () => { + if (isBrowser()) { + return; + } + const userCred = await signInAnonymously(auth); + expect(auth.currentUser).to.eq(userCred.user); + expect(userCred.operationType).to.eq(OperationType.SIGN_IN); + const user = userCred.user; + expect(user).to.equal(auth.currentUser); + expect(user.isAnonymous).to.be.true; + expect(user.uid).to.be.a('string'); + + const authIdToken = await user.getIdToken(); + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp( + getAppConfig(), + firebaseServerAppSettings + ); + serverAppAuth = getTestInstanceForServerApp(serverApp); + let numberServerLogins = 0; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + expect(user.uid).to.be.equal(serverAuthUser.uid); + expect(serverAppAuth).to.not.be.null; + expect(serverAuthUser.getIdToken); + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.equal(serverAuthUser); + } + } + }); + + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(numberServerLogins).to.equal(1); + expect(serverAppAuth.currentUser).to.not.be.null; + if (serverAppAuth.currentUser) { + const idToken = await serverAppAuth.currentUser.getIdToken( + /*forceRefresh=*/ false + ); + expect(idToken).to.not.be.null; + await expect(serverAppAuth.currentUser.getIdToken(/*forceRefresh=*/ true)) + .to.be.rejected; + } + }); + + it('invalid token does not sign in user', async () => { + if (isBrowser()) { + return; + } + const authIdToken = '{ invalid token }'; + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp( + getAppConfig(), + firebaseServerAppSettings + ); + serverAppAuth = getTestInstanceForServerApp(serverApp); + expect(serverAppAuth.currentUser).to.be.null; + + let numberServerLogins = 0; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + } + }); + + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(numberServerLogins).to.equal(0); + expect(serverAppAuth.currentUser).to.be.null; + }); + + it('signs in with email crednetial user', async () => { + if (isBrowser()) { + return; + } + const email = randomEmail(); + const password = 'password'; + const userCred = await createUserWithEmailAndPassword( + auth, + email, + password + ); + const user = userCred.user; + expect(auth.currentUser).to.eq(userCred.user); + expect(userCred.operationType).to.eq(OperationType.SIGN_IN); + + const additionalUserInfo = getAdditionalUserInfo(userCred)!; + expect(additionalUserInfo.isNewUser).to.be.true; + expect(additionalUserInfo.providerId).to.eq('password'); + expect(user.isAnonymous).to.be.false; + expect(user.email).to.equal(email); + + const authIdToken = await user.getIdToken(); + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp( + getAppConfig(), + firebaseServerAppSettings + ); + serverAppAuth = getTestInstanceForServerApp(serverApp); + let numberServerLogins = 0; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + expect(serverAppAuth).to.not.be.null; + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.equal(serverAuthUser); + } + expect(user.uid).to.be.equal(serverAuthUser.uid); + expect(serverAuthUser.refreshToken).to.be.empty; + expect(user.isAnonymous).to.be.equal(serverAuthUser.isAnonymous); + expect(user.emailVerified).to.be.equal(serverAuthUser.emailVerified); + expect(user.providerData.length).to.eq( + serverAuthUser.providerData.length + ); + expect(user.email).to.equal(serverAuthUser.email); + } + }); + + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(numberServerLogins).to.equal(1); + }); + + it('can reload user', async () => { + if (isBrowser()) { + return; + } + const userCred = await signInAnonymously(auth); + expect(auth.currentUser).to.eq(userCred.user); + + const user = userCred.user; + expect(user).to.equal(auth.currentUser); + expect(user.uid).to.be.a('string'); + + const authIdToken = await user.getIdToken(); + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp( + getAppConfig(), + firebaseServerAppSettings + ); + serverAppAuth = getTestInstanceForServerApp(serverApp); + let numberServerLogins = 0; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + expect(user.uid).to.be.equal(serverAuthUser.uid); + expect(serverAppAuth).to.not.be.null; + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.equal(serverAuthUser); + } + } + }); + + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(serverAppAuth.currentUser).to.not.be.null; + if (serverAppAuth.currentUser) { + await serverAppAuth.currentUser.reload(); + } + expect(numberServerLogins).to.equal(1); + }); + + it('can update server based user profile', async () => { + if (isBrowser()) { + return; + } + const userCred = await signInAnonymously(auth); + expect(auth.currentUser).to.eq(userCred.user); + + const user = userCred.user; + expect(user).to.equal(auth.currentUser); + expect(user.uid).to.be.a('string'); + expect(user.displayName).to.be.null; + + const authIdToken = await user.getIdToken(); + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp( + getAppConfig(), + firebaseServerAppSettings + ); + serverAppAuth = getTestInstanceForServerApp(serverApp); + let numberServerLogins = 0; + const newDisplayName = 'newName'; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + expect(serverAppAuth).to.not.be.null; + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.equal(serverAuthUser); + } + expect(user.uid).to.be.equal(serverAuthUser.uid); + expect(user.displayName).to.be.null; + void updateProfile(serverAuthUser, { + displayName: newDisplayName + }); + } + }); + + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(serverAppAuth.currentUser).to.not.be.null; + + if (serverAppAuth.currentUser) { + await serverAppAuth.currentUser.reload(); + } + + expect(numberServerLogins).to.equal(1); + expect(serverAppAuth).to.not.be.null; + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.not.be.null; + expect(serverAppAuth.currentUser?.displayName).to.not.be.null; + expect(serverAppAuth.currentUser?.displayName).to.equal(newDisplayName); + } + }); + + it('can sign out of main auth and still use server auth', async () => { + if (isBrowser()) { + return; + } + const userCred = await signInAnonymously(auth); + expect(auth.currentUser).to.eq(userCred.user); + + const user = userCred.user; + expect(user).to.equal(auth.currentUser); + expect(user.uid).to.be.a('string'); + expect(user.displayName).to.be.null; + + const authIdToken = await user.getIdToken(); + const firebaseServerAppSettings = { authIdToken }; + + const serverApp = initializeServerApp( + getAppConfig(), + firebaseServerAppSettings + ); + serverAppAuth = getTestInstanceForServerApp(serverApp); + let numberServerLogins = 0; + onAuthStateChanged(serverAppAuth, serverAuthUser => { + if (serverAuthUser) { + numberServerLogins++; + expect(serverAppAuth).to.not.be.null; + expect(user.uid).to.be.equal(serverAuthUser.uid); + expect(user.displayName).to.be.null; + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.equal(serverAuthUser); + } + } + }); + + await signOut(auth); + await new Promise(resolve => { + setTimeout(resolve, signInWaitDuration); + }); + + expect(serverAppAuth.currentUser).to.not.be.null; + + if (serverAppAuth.currentUser) { + await serverAppAuth.currentUser.reload(); + } + + expect(numberServerLogins).to.equal(1); + expect(serverAppAuth).to.not.be.null; + if (serverAppAuth) { + expect(serverAppAuth.currentUser).to.not.be.null; + } + }); +});