1616 */
1717
1818import { AppOptions , app } from './firebase-namespace-api' ;
19- import { credential , GoogleOAuthAccessToken } from './credential/index' ;
19+ import { credential } from './credential/index' ;
2020import { getApplicationDefault } from './credential/credential-internal' ;
2121import * as validator from './utils/validator' ;
2222import { deepCopy } from './utils/deep-copy' ;
@@ -39,6 +39,8 @@ import { RemoteConfig } from './remote-config/remote-config';
3939import Credential = credential . Credential ;
4040import Database = database . Database ;
4141
42+ const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000 ;
43+
4244/**
4345 * Type representing a callback which is called every time an app lifecycle event occurs.
4446 */
@@ -57,129 +59,80 @@ export interface FirebaseAccessToken {
5759 * Internals of a FirebaseApp instance.
5860 */
5961export class FirebaseAppInternals {
60- private isDeleted_ = false ;
6162 private cachedToken_ : FirebaseAccessToken ;
62- private cachedTokenPromise_ : Promise < FirebaseAccessToken > | null ;
6363 private tokenListeners_ : Array < ( token : string ) => void > ;
64- private tokenRefreshTimeout_ : NodeJS . Timer ;
6564
6665 constructor ( private credential_ : Credential ) {
6766 this . tokenListeners_ = [ ] ;
6867 }
6968
70- /**
71- * Gets an auth token for the associated app.
72- *
73- * @param {boolean } forceRefresh Whether or not to force a token refresh.
74- * @return {Promise<FirebaseAccessToken> } A Promise that will be fulfilled with the current or
75- * new token.
76- */
77- public getToken ( forceRefresh ?: boolean ) : Promise < FirebaseAccessToken > {
78- const expired = this . cachedToken_ && this . cachedToken_ . expirationTime < Date . now ( ) ;
79- if ( this . cachedTokenPromise_ && ! forceRefresh && ! expired ) {
80- return this . cachedTokenPromise_
81- . catch ( ( error ) => {
82- // Update the cached token promise to avoid caching errors. Set it to resolve with the
83- // cached token if we have one (and return that promise since the token has still not
84- // expired).
85- if ( this . cachedToken_ ) {
86- this . cachedTokenPromise_ = Promise . resolve ( this . cachedToken_ ) ;
87- return this . cachedTokenPromise_ ;
88- }
89-
90- // Otherwise, set the cached token promise to null so that it will force a refresh next
91- // time getToken() is called.
92- this . cachedTokenPromise_ = null ;
93-
94- // And re-throw the caught error.
95- throw error ;
96- } ) ;
97- } else {
98- // Clear the outstanding token refresh timeout. This is a noop if the timeout is undefined.
99- clearTimeout ( this . tokenRefreshTimeout_ ) ;
100-
101- // this.credential_ may be an external class; resolving it in a promise helps us
102- // protect against exceptions and upgrades the result to a promise in all cases.
103- this . cachedTokenPromise_ = Promise . resolve ( this . credential_ . getAccessToken ( ) )
104- . then ( ( result : GoogleOAuthAccessToken ) => {
105- // Since the developer can provide the credential implementation, we want to weakly verify
106- // the return type until the type is properly exported.
107- if ( ! validator . isNonNullObject ( result ) ||
108- typeof result . expires_in !== 'number' ||
109- typeof result . access_token !== 'string' ) {
110- throw new FirebaseAppError (
111- AppErrorCodes . INVALID_CREDENTIAL ,
112- `Invalid access token generated: "${ JSON . stringify ( result ) } ". Valid access ` +
113- 'tokens must be an object with the "expires_in" (number) and "access_token" ' +
114- '(string) properties.' ,
115- ) ;
116- }
117-
118- const token : FirebaseAccessToken = {
119- accessToken : result . access_token ,
120- expirationTime : Date . now ( ) + ( result . expires_in * 1000 ) ,
121- } ;
122-
123- const hasAccessTokenChanged = ( this . cachedToken_ && this . cachedToken_ . accessToken !== token . accessToken ) ;
124- const hasExpirationChanged = ( this . cachedToken_ && this . cachedToken_ . expirationTime !== token . expirationTime ) ;
125- if ( ! this . cachedToken_ || hasAccessTokenChanged || hasExpirationChanged ) {
126- this . cachedToken_ = token ;
127- this . tokenListeners_ . forEach ( ( listener ) => {
128- listener ( token . accessToken ) ;
129- } ) ;
130- }
131-
132- // Establish a timeout to proactively refresh the token every minute starting at five
133- // minutes before it expires. Once a token refresh succeeds, no further retries are
134- // needed; if it fails, retry every minute until the token expires (resulting in a total
135- // of four retries: at 4, 3, 2, and 1 minutes).
136- let refreshTimeInSeconds = ( result . expires_in - ( 5 * 60 ) ) ;
137- let numRetries = 4 ;
138-
139- // In the rare cases the token is short-lived (that is, it expires in less than five
140- // minutes from when it was fetched), establish the timeout to refresh it after the
141- // current minute ends and update the number of retries that should be attempted before
142- // the token expires.
143- if ( refreshTimeInSeconds <= 0 ) {
144- refreshTimeInSeconds = result . expires_in % 60 ;
145- numRetries = Math . floor ( result . expires_in / 60 ) - 1 ;
146- }
147-
148- // The token refresh timeout keeps the Node.js process alive, so only create it if this
149- // instance has not already been deleted.
150- if ( numRetries && ! this . isDeleted_ ) {
151- this . setTokenRefreshTimeout ( refreshTimeInSeconds * 1000 , numRetries ) ;
152- }
153-
154- return token ;
155- } )
156- . catch ( ( error ) => {
157- let errorMessage = ( typeof error === 'string' ) ? error : error . message ;
158-
159- errorMessage = 'Credential implementation provided to initializeApp() via the ' +
160- '"credential" property failed to fetch a valid Google OAuth2 access token with the ' +
161- `following error: "${ errorMessage } ".` ;
162-
163- if ( errorMessage . indexOf ( 'invalid_grant' ) !== - 1 ) {
164- errorMessage += ' There are two likely causes: (1) your server time is not properly ' +
165- 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' +
166- 'time on your server. To solve (2), make sure the key ID for your key file is still ' +
167- 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' +
168- 'not, generate a new key file at ' +
169- 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.' ;
170- }
171-
172- throw new FirebaseAppError ( AppErrorCodes . INVALID_CREDENTIAL , errorMessage ) ;
173- } ) ;
174-
175- return this . cachedTokenPromise_ ;
69+ public getToken ( forceRefresh = false ) : Promise < FirebaseAccessToken > {
70+ if ( forceRefresh || this . shouldRefresh ( ) ) {
71+ return this . refreshToken ( ) ;
17672 }
73+
74+ return Promise . resolve ( this . cachedToken_ ) ;
75+ }
76+
77+ private refreshToken ( ) : Promise < FirebaseAccessToken > {
78+ return Promise . resolve ( this . credential_ . getAccessToken ( ) )
79+ . then ( ( result ) => {
80+ // Since the developer can provide the credential implementation, we want to weakly verify
81+ // the return type until the type is properly exported.
82+ if ( ! validator . isNonNullObject ( result ) ||
83+ typeof result . expires_in !== 'number' ||
84+ typeof result . access_token !== 'string' ) {
85+ throw new FirebaseAppError (
86+ AppErrorCodes . INVALID_CREDENTIAL ,
87+ `Invalid access token generated: "${ JSON . stringify ( result ) } ". Valid access ` +
88+ 'tokens must be an object with the "expires_in" (number) and "access_token" ' +
89+ '(string) properties.' ,
90+ ) ;
91+ }
92+
93+ const token = {
94+ accessToken : result . access_token ,
95+ expirationTime : Date . now ( ) + ( result . expires_in * 1000 ) ,
96+ } ;
97+ if ( ! this . cachedToken_
98+ || this . cachedToken_ . accessToken !== token . accessToken
99+ || this . cachedToken_ . expirationTime !== token . expirationTime ) {
100+ this . cachedToken_ = token ;
101+ this . tokenListeners_ . forEach ( ( listener ) => {
102+ listener ( token . accessToken ) ;
103+ } ) ;
104+ }
105+
106+ return token ;
107+ } )
108+ . catch ( ( error ) => {
109+ let errorMessage = ( typeof error === 'string' ) ? error : error . message ;
110+
111+ errorMessage = 'Credential implementation provided to initializeApp() via the ' +
112+ '"credential" property failed to fetch a valid Google OAuth2 access token with the ' +
113+ `following error: "${ errorMessage } ".` ;
114+
115+ if ( errorMessage . indexOf ( 'invalid_grant' ) !== - 1 ) {
116+ errorMessage += ' There are two likely causes: (1) your server time is not properly ' +
117+ 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' +
118+ 'time on your server. To solve (2), make sure the key ID for your key file is still ' +
119+ 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' +
120+ 'not, generate a new key file at ' +
121+ 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.' ;
122+ }
123+
124+ throw new FirebaseAppError ( AppErrorCodes . INVALID_CREDENTIAL , errorMessage ) ;
125+ } ) ;
126+ }
127+
128+ private shouldRefresh ( ) : boolean {
129+ return ! this . cachedToken_ || ( this . cachedToken_ . expirationTime - Date . now ( ) ) <= TOKEN_EXPIRY_THRESHOLD_MILLIS ;
177130 }
178131
179132 /**
180133 * Adds a listener that is called each time a token changes.
181134 *
182- * @param { function(string) } listener The listener that will be called with each new token.
135+ * @param listener The listener that will be called with each new token.
183136 */
184137 public addAuthTokenListener ( listener : ( token : string ) => void ) : void {
185138 this . tokenListeners_ . push ( listener ) ;
@@ -191,42 +144,11 @@ export class FirebaseAppInternals {
191144 /**
192145 * Removes a token listener.
193146 *
194- * @param { function(string) } listener The listener to remove.
147+ * @param listener The listener to remove.
195148 */
196149 public removeAuthTokenListener ( listener : ( token : string ) => void ) : void {
197150 this . tokenListeners_ = this . tokenListeners_ . filter ( ( other ) => other !== listener ) ;
198151 }
199-
200- /**
201- * Deletes the FirebaseAppInternals instance.
202- */
203- public delete ( ) : void {
204- this . isDeleted_ = true ;
205-
206- // Clear the token refresh timeout so it doesn't keep the Node.js process alive.
207- clearTimeout ( this . tokenRefreshTimeout_ ) ;
208- }
209-
210- /**
211- * Establishes timeout to refresh the Google OAuth2 access token used by the SDK.
212- *
213- * @param {number } delayInMilliseconds The delay to use for the timeout.
214- * @param {number } numRetries The number of times to retry fetching a new token if the prior fetch
215- * failed.
216- */
217- private setTokenRefreshTimeout ( delayInMilliseconds : number , numRetries : number ) : void {
218- this . tokenRefreshTimeout_ = setTimeout ( ( ) => {
219- this . getToken ( /* forceRefresh */ true )
220- . catch ( ( ) => {
221- // Ignore the error since this might just be an intermittent failure. If we really cannot
222- // refresh the token, an error will be logged once the existing token expires and we try
223- // to fetch a fresh one.
224- if ( numRetries > 0 ) {
225- this . setTokenRefreshTimeout ( 60 * 1000 , numRetries - 1 ) ;
226- }
227- } ) ;
228- } , delayInMilliseconds ) ;
229- }
230152}
231153
232154/**
@@ -419,8 +341,6 @@ export class FirebaseApp implements app.App {
419341 this . checkDestroyed_ ( ) ;
420342 this . firebaseInternals_ . removeApp ( this . name_ ) ;
421343
422- this . INTERNAL . delete ( ) ;
423-
424344 return Promise . all ( Object . keys ( this . services_ ) . map ( ( serviceName ) => {
425345 const service = this . services_ [ serviceName ] ;
426346 if ( isStateful ( service ) ) {
0 commit comments