Skip to content

Commit fc44b08

Browse files
authored
[ServerApp] Update feature branch Auth implementation to use the authIdToken (#7944)
Add support for logging-in users with the FirebaseServerApp's authIdToken. ### Testing Local project testing client-side created users, passing idTokens to serverApps, and logging in the user. Tested with multiple users and multiple instances of FirebaseServerApps w/ Auth. CI tests (added integration tests). ### API Changes N/A
1 parent fc0362f commit fc44b08

File tree

15 files changed

+618
-21
lines changed

15 files changed

+618
-21
lines changed

common/api-review/app.api.md

+4
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export interface FirebaseServerApp extends FirebaseApp {
7777
authIdTokenVerified: () => Promise<void>;
7878
installationTokenVerified: () => Promise<void>;
7979
name: string;
80+
readonly settings: FirebaseServerAppSettings;
8081
}
8182

8283
// @public
@@ -119,6 +120,9 @@ export function initializeServerApp(options: FirebaseOptions | FirebaseApp, conf
119120
// @internal (undocumented)
120121
export function _isFirebaseApp(obj: FirebaseApp | FirebaseOptions): obj is FirebaseApp;
121122

123+
// @internal (undocumented)
124+
export function _isFirebaseServerApp(obj: FirebaseApp | FirebaseServerApp): obj is FirebaseServerApp;
125+
122126
// @public
123127
export function onLog(logCallback: LogCallback | null, options?: LogOptions): void;
124128

docs-devsite/app.firebaseserverapp.md

+21
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface FirebaseServerApp extends FirebaseApp
2929
| [authIdTokenVerified](./app.firebaseserverapp.md#firebaseserverappauthidtokenverified) | () =&gt; Promise&lt;void&gt; | 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. |
3030
| [installationTokenVerified](./app.firebaseserverapp.md#firebaseserverappinstallationtokenverified) | () =&gt; Promise&lt;void&gt; | 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. |
3131
| [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. |
32+
| [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)<!-- -->. |
3233
3334
## FirebaseServerApp.appCheckTokenVerified
3435
@@ -81,3 +82,23 @@ There is no get for FirebaseServerApp, so the name is not relevant. However, it'
8182
```typescript
8283
name: string;
8384
```
85+
86+
## FirebaseServerApp.settings
87+
88+
The (read-only) configuration settings for this server app. These are the original parameters given in [initializeServerApp()](./app.md#initializeserverapp_30ab697)<!-- -->.
89+
90+
<b>Signature:</b>
91+
92+
```typescript
93+
readonly settings: FirebaseServerAppSettings;
94+
```
95+
96+
### Example
97+
98+
99+
```javascript
100+
const app = initializeServerApp(settings);
101+
console.log(app.settings.authIdToken === options.authIdToken); // true
102+
103+
```
104+

packages/app/src/firebaseServerApp.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class FirebaseServerAppImpl
8383
void deleteApp(serverApp);
8484
}
8585

86-
get serverAppConfig(): FirebaseServerAppSettings {
86+
get settings(): FirebaseServerAppSettings {
8787
this.checkDestroyed();
8888
return this._serverConfig;
8989
}

packages/app/src/internal.ts

+14
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ export function _isFirebaseApp(
155155
return (obj as FirebaseApp).options !== undefined;
156156
}
157157

158+
/**
159+
*
160+
* @param obj - an object of type FirebaseApp.
161+
*
162+
* @returns true if the provided object is of type FirebaseServerAppImpl.
163+
*
164+
* @internal
165+
*/
166+
export function _isFirebaseServerApp(
167+
obj: FirebaseApp | FirebaseServerApp
168+
): obj is FirebaseServerApp {
169+
return (obj as FirebaseServerApp).authIdTokenVerified !== undefined;
170+
}
171+
158172
/**
159173
* Test only
160174
*

packages/app/src/public-types.ts

+12
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,18 @@ export interface FirebaseServerApp extends FirebaseApp {
132132
* string will always be empty for FirebaseServerApp instances.
133133
*/
134134
name: string;
135+
136+
/**
137+
* The (read-only) configuration settings for this server app. These are the original
138+
* parameters given in {@link (initializeServerApp:1) | initializeServerApp()}.
139+
*
140+
* @example
141+
* ```javascript
142+
* const app = initializeServerApp(settings);
143+
* console.log(app.settings.authIdToken === options.authIdToken); // true
144+
* ```
145+
*/
146+
readonly settings: FirebaseServerAppSettings;
135147
}
136148

137149
/**

packages/auth/karma.conf.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ function getTestFiles(argv) {
6565
'src/**/*.test.ts',
6666
'test/helpers/**/*.test.ts',
6767
'test/integration/flows/anonymous.test.ts',
68-
'test/integration/flows/email.test.ts'
68+
'test/integration/flows/email.test.ts',
69+
'test/integration/flows/firebaseserverapp.test.ts'
6970
];
7071
}
7172
}

packages/auth/scripts/run_node_tests.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ let testConfig = [
4848
];
4949

5050
if (argv.integration) {
51-
testConfig = ['test/integration/flows/{email,anonymous}.test.ts'];
51+
testConfig = [
52+
'test/integration/flows/{email,anonymous,firebaseserverapp}.test.ts'
53+
];
5254
if (argv.local) {
5355
testConfig.push('test/integration/flows/*.local.test.ts');
5456
}

packages/auth/src/core/auth/auth_impl.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { _FirebaseService, FirebaseApp } from '@firebase/app';
18+
import {
19+
_isFirebaseServerApp,
20+
_FirebaseService,
21+
FirebaseApp
22+
} from '@firebase/app';
1923
import { Provider } from '@firebase/component';
2024
import { AppCheckInternalComponentName } from '@firebase/app-check-interop-types';
2125
import {
@@ -167,7 +171,11 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
167171
}
168172
}
169173

170-
await this.initializeCurrentUser(popupRedirectResolver);
174+
// Skip loading users from persistence in FirebaseServerApp Auth instances.
175+
if (!_isFirebaseServerApp(this.app)) {
176+
await this.initializeCurrentUser(popupRedirectResolver);
177+
}
178+
171179
this.lastNotifiedUid = this.currentUser?.uid || null;
172180

173181
if (this._deleted) {

packages/auth/src/core/auth/initialize.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { _getProvider, FirebaseApp } from '@firebase/app';
18+
import { _getProvider, _isFirebaseServerApp, FirebaseApp } from '@firebase/app';
1919
import { deepEqual } from '@firebase/util';
2020
import { Auth, Dependencies } from '../../model/public_types';
2121

2222
import { AuthErrorCode } from '../errors';
2323
import { PersistenceInternal } from '../persistence';
2424
import { _fail } from '../util/assert';
2525
import { _getInstance } from '../util/instantiator';
26-
import { AuthImpl } from './auth_impl';
26+
import { AuthImpl, _castAuth } from './auth_impl';
27+
import { UserImpl } from '../user/user_impl';
28+
import { getAccountInfo } from '../../api/account_management/account';
2729

2830
/**
2931
* Initializes an {@link Auth} instance with fine-grained control over
@@ -65,9 +67,40 @@ export function initializeAuth(app: FirebaseApp, deps?: Dependencies): Auth {
6567

6668
const auth = provider.initialize({ options: deps }) as AuthImpl;
6769

70+
if (_isFirebaseServerApp(app)) {
71+
if (app.settings.authIdToken !== undefined) {
72+
const idToken = app.settings.authIdToken;
73+
// Start the auth operation in the next tick to allow a moment for the customer's app to
74+
// attach an emulator, if desired.
75+
setTimeout(() => void _loadUserFromIdToken(auth, idToken), 0);
76+
}
77+
}
78+
6879
return auth;
6980
}
7081

82+
export async function _loadUserFromIdToken(
83+
auth: Auth,
84+
idToken: string
85+
): Promise<void> {
86+
try {
87+
const response = await getAccountInfo(auth, { idToken });
88+
const authInternal = _castAuth(auth);
89+
await authInternal._initializationPromise;
90+
const user = await UserImpl._fromGetAccountInfoResponse(
91+
authInternal,
92+
response,
93+
idToken
94+
);
95+
await authInternal._updateCurrentUser(user);
96+
} catch (err) {
97+
console.warn(
98+
'FirebaseServerApp could not login user with provided authIdToken: ',
99+
err
100+
);
101+
}
102+
}
103+
71104
export function _initializeAuthInstance(
72105
auth: AuthImpl,
73106
deps?: Dependencies

packages/auth/src/core/user/reload.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function mergeProviderData(
102102
return [...deduped, ...newData];
103103
}
104104

105-
function extractProviderData(providers: ProviderUserInfo[]): UserInfo[] {
105+
export function extractProviderData(providers: ProviderUserInfo[]): UserInfo[] {
106106
return providers.map(({ providerId, ...provider }) => {
107107
return {
108108
providerId,

packages/auth/src/core/user/token_manager.test.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,35 @@ describe('core/user/token_manager', () => {
144144
});
145145
});
146146

147-
it('returns null if the refresh token is missing', async () => {
148-
expect(await stsTokenManager.getToken(auth)).to.be.null;
147+
it('returns non-null if the refresh token is missing but token still valid', async () => {
148+
Object.assign(stsTokenManager, {
149+
accessToken: 'token',
150+
expirationTime: now + 100_000
151+
});
152+
const tokens = await stsTokenManager.getToken(auth, false);
153+
expect(tokens).to.eql('token');
154+
});
155+
156+
it('throws an error if the refresh token is missing and force refresh is true', async () => {
157+
Object.assign(stsTokenManager, {
158+
accessToken: 'token',
159+
expirationTime: now + 100_000
160+
});
161+
await expect(stsTokenManager.getToken(auth, true)).to.be.rejectedWith(
162+
FirebaseError,
163+
"Firebase: The user's credential is no longer valid. The user must sign in again. (auth/user-token-expired)"
164+
);
165+
});
166+
167+
it('throws an error if the refresh token is missing and token is no longer valid', async () => {
168+
Object.assign(stsTokenManager, {
169+
accessToken: 'old-access-token',
170+
expirationTime: now - 1
171+
});
172+
await expect(stsTokenManager.getToken(auth)).to.be.rejectedWith(
173+
FirebaseError,
174+
"Firebase: The user's credential is no longer valid. The user must sign in again. (auth/user-token-expired)"
175+
);
149176
});
150177

151178
it('throws an error if expired but refresh token is missing', async () => {

packages/auth/src/core/user/token_manager.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,22 @@ export class StsTokenManager {
7373
);
7474
}
7575

76+
updateFromIdToken(idToken: string): void {
77+
_assert(idToken.length !== 0, AuthErrorCode.INTERNAL_ERROR);
78+
const expiresIn = _tokenExpiresIn(idToken);
79+
this.updateTokensAndExpiration(idToken, null, expiresIn);
80+
}
81+
7682
async getToken(
7783
auth: AuthInternal,
7884
forceRefresh = false
7985
): Promise<string | null> {
80-
_assert(
81-
!this.accessToken || this.refreshToken,
82-
auth,
83-
AuthErrorCode.TOKEN_EXPIRED
84-
);
85-
8686
if (!forceRefresh && this.accessToken && !this.isExpired) {
8787
return this.accessToken;
8888
}
8989

90+
_assert(this.refreshToken, auth, AuthErrorCode.TOKEN_EXPIRED);
91+
9092
if (this.refreshToken) {
9193
await this.refresh(auth, this.refreshToken!);
9294
return this.accessToken;
@@ -113,7 +115,7 @@ export class StsTokenManager {
113115

114116
private updateTokensAndExpiration(
115117
accessToken: string,
116-
refreshToken: string,
118+
refreshToken: string | null,
117119
expiresInSec: number
118120
): void {
119121
this.refreshToken = refreshToken || null;

packages/auth/src/core/user/user_impl.ts

+58-3
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { IdTokenResult } from '../../model/public_types';
18+
import { IdTokenResult, UserInfo } from '../../model/public_types';
1919
import { NextFn } from '@firebase/util';
20-
2120
import {
2221
APIUserInfo,
22+
GetAccountInfoResponse,
2323
deleteAccount
2424
} from '../../api/account_management/account';
2525
import { FinalizeMfaResponse } from '../../api/authentication/mfa';
@@ -36,7 +36,7 @@ import { _assert } from '../util/assert';
3636
import { getIdTokenResult } from './id_token_result';
3737
import { _logoutIfInvalidated } from './invalidation';
3838
import { ProactiveRefresh } from './proactive_refresh';
39-
import { _reloadWithoutSaving, reload } from './reload';
39+
import { extractProviderData, _reloadWithoutSaving, reload } from './reload';
4040
import { StsTokenManager } from './token_manager';
4141
import { UserMetadata } from './user_metadata';
4242
import { ProviderId } from '../../model/enums';
@@ -333,4 +333,59 @@ export class UserImpl implements UserInternal {
333333
await _reloadWithoutSaving(user);
334334
return user;
335335
}
336+
337+
/**
338+
* Initialize a User from an idToken server response
339+
* @param auth
340+
* @param idTokenResponse
341+
*/
342+
static async _fromGetAccountInfoResponse(
343+
auth: AuthInternal,
344+
response: GetAccountInfoResponse,
345+
idToken: string
346+
): Promise<UserInternal> {
347+
const coreAccount = response.users[0];
348+
_assert(coreAccount.localId !== undefined, AuthErrorCode.INTERNAL_ERROR);
349+
350+
const providerData: UserInfo[] =
351+
coreAccount.providerUserInfo !== undefined
352+
? extractProviderData(coreAccount.providerUserInfo)
353+
: [];
354+
355+
const isAnonymous =
356+
!(coreAccount.email && coreAccount.passwordHash) && !providerData?.length;
357+
358+
const stsTokenManager = new StsTokenManager();
359+
stsTokenManager.updateFromIdToken(idToken);
360+
361+
// Initialize the Firebase Auth user.
362+
const user = new UserImpl({
363+
uid: coreAccount.localId,
364+
auth,
365+
stsTokenManager,
366+
isAnonymous
367+
});
368+
369+
// update the user with data from the GetAccountInfo response.
370+
const updates: Partial<UserInternal> = {
371+
uid: coreAccount.localId,
372+
displayName: coreAccount.displayName || null,
373+
photoURL: coreAccount.photoUrl || null,
374+
email: coreAccount.email || null,
375+
emailVerified: coreAccount.emailVerified || false,
376+
phoneNumber: coreAccount.phoneNumber || null,
377+
tenantId: coreAccount.tenantId || null,
378+
providerData,
379+
metadata: new UserMetadata(
380+
coreAccount.createdAt,
381+
coreAccount.lastLoginAt
382+
),
383+
isAnonymous:
384+
!(coreAccount.email && coreAccount.passwordHash) &&
385+
!providerData?.length
386+
};
387+
388+
Object.assign(user, updates);
389+
return user;
390+
}
336391
}

0 commit comments

Comments
 (0)