Skip to content

Commit c1e9229

Browse files
authored
Add the internal auth interface including proactive refresh (#3655)
* Proactive refresh and internal SDK * Fix tests for node * Formatting * Remove unused file * PR feedback * Formatting
1 parent c7ef26f commit c1e9229

File tree

10 files changed

+582
-10
lines changed

10 files changed

+582
-10
lines changed

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
6262
private authStateSubscription = new Subscription<T>(this);
6363
private idTokenSubscription = new Subscription<T>(this);
6464
private redirectUser: T | null = null;
65+
private isProactiveRefreshEnabled = false;
6566
_isInitialized = false;
67+
_initializationPromise: Promise<void> | null = null;
6668
_popupRedirectResolver: PopupRedirectResolver | null = null;
6769
readonly name: string;
6870

@@ -86,7 +88,7 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
8688
persistenceHierarchy: Persistence[],
8789
popupRedirectResolver?: externs.PopupRedirectResolver
8890
): Promise<void> {
89-
return this.queue(async () => {
91+
this._initializationPromise = this.queue(async () => {
9092
if (popupRedirectResolver) {
9193
this._popupRedirectResolver = _getInstance(popupRedirectResolver);
9294
}
@@ -101,6 +103,8 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
101103
this._isInitialized = true;
102104
this.notifyAuthListeners();
103105
});
106+
107+
return this._initializationPromise;
104108
}
105109

106110
_createUser(params: UserParameters): T {
@@ -271,6 +275,20 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
271275
return `${this.config.authDomain}:${this.config.apiKey}:${this.name}`;
272276
}
273277

278+
_startProactiveRefresh(): void {
279+
this.isProactiveRefreshEnabled = true;
280+
if (this.currentUser) {
281+
this.currentUser._startProactiveRefresh();
282+
}
283+
}
284+
285+
_stopProactiveRefresh(): void {
286+
this.isProactiveRefreshEnabled = false;
287+
if (this.currentUser) {
288+
this.currentUser._stopProactiveRefresh();
289+
}
290+
}
291+
274292
private notifyAuthListeners(): void {
275293
if (!this._isInitialized) {
276294
return;
@@ -313,6 +331,13 @@ export class AuthImplCompat<T extends User> implements Auth, _FirebaseService {
313331
* because the queue shouldn't rely on another queued callback.
314332
*/
315333
private async directlySetCurrentUser(user: T | null): Promise<void> {
334+
if (this.currentUser && this.currentUser !== user) {
335+
this.currentUser._stopProactiveRefresh();
336+
if (user && this.isProactiveRefreshEnabled) {
337+
user._startProactiveRefresh();
338+
}
339+
}
340+
316341
this.currentUser = user;
317342

318343
if (user) {
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
import * as sinon from 'sinon';
20+
21+
import { testAuth, testUser } from '../../../test/helpers/mock_auth';
22+
import { Auth } from '../../model/auth';
23+
import { User } from '../../model/user';
24+
import { AuthInternal } from './firebase_internal';
25+
26+
describe('src/core/auth/firebase_internal', () => {
27+
let auth: Auth;
28+
let authInternal: AuthInternal;
29+
beforeEach(async () => {
30+
auth = await testAuth();
31+
authInternal = new AuthInternal(auth);
32+
});
33+
34+
afterEach(() => {
35+
sinon.restore();
36+
});
37+
38+
context('getUid', () => {
39+
it('returns null if currentUser is undefined', () => {
40+
expect(authInternal.getUid()).to.be.null;
41+
});
42+
43+
it('returns the uid of the user if set', async () => {
44+
const user = testUser(auth, 'uid');
45+
await auth.updateCurrentUser(user);
46+
expect(authInternal.getUid()).to.eq('uid');
47+
});
48+
});
49+
50+
context('getToken', () => {
51+
it('returns null if currentUser is undefined', async () => {
52+
expect(await authInternal.getToken()).to.be.null;
53+
});
54+
55+
it('returns the id token of the current user correctly', async () => {
56+
const user = testUser(auth, 'uid');
57+
await auth.updateCurrentUser(user);
58+
user.stsTokenManager.accessToken = 'access-token';
59+
user.stsTokenManager.expirationTime = Date.now() + 1000 * 60 * 60 * 24;
60+
expect(await authInternal.getToken()).to.eql({
61+
accessToken: 'access-token'
62+
});
63+
});
64+
});
65+
66+
context('token listeners', () => {
67+
let isProactiveRefresh = false;
68+
let user: User;
69+
70+
beforeEach(async () => {
71+
user = testUser(auth, 'uid', undefined, true);
72+
await auth.updateCurrentUser(user);
73+
sinon.stub(user.stsTokenManager, 'getToken').returns(
74+
Promise.resolve({
75+
accessToken: 'access-token',
76+
refreshToken: 'refresh-tken',
77+
wasRefreshed: true
78+
})
79+
);
80+
sinon
81+
.stub(user, '_startProactiveRefresh')
82+
.callsFake(() => (isProactiveRefresh = true));
83+
sinon
84+
.stub(user, '_stopProactiveRefresh')
85+
.callsFake(() => (isProactiveRefresh = false));
86+
});
87+
88+
context('addAuthTokenListener', () => {
89+
it('gets called with the token, starts proactive refresh', done => {
90+
// The listener always fires first time. Ignore that one
91+
let firstCall = true;
92+
authInternal.addAuthTokenListener(token => {
93+
if (firstCall) {
94+
firstCall = false;
95+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
96+
user.getIdToken(true);
97+
return;
98+
}
99+
100+
expect(token).to.eq('access-token');
101+
expect(isProactiveRefresh).to.be.true;
102+
done();
103+
});
104+
});
105+
106+
it('gets called on subsequent updates', async () => {
107+
let tokenCount = 0;
108+
authInternal.addAuthTokenListener(() => {
109+
tokenCount++;
110+
});
111+
112+
await user.getIdToken(true);
113+
await user.getIdToken(true);
114+
await user.getIdToken(true);
115+
await user.getIdToken(true);
116+
117+
expect(tokenCount).to.eq(5);
118+
});
119+
});
120+
121+
context('removeAuthTokenListener', () => {
122+
it('listeners no longer receive token updates', async () => {
123+
let tokenCount = 0;
124+
function listener(): void {
125+
tokenCount++;
126+
}
127+
authInternal.addAuthTokenListener(listener);
128+
129+
await user.getIdToken(true);
130+
expect(tokenCount).to.eq(2);
131+
authInternal.removeAuthTokenListener(listener);
132+
await user.getIdToken(true);
133+
await user.getIdToken(true);
134+
await user.getIdToken(true);
135+
expect(tokenCount).to.eq(2);
136+
});
137+
138+
it('toggles proactive refresh when listeners fall to 0', () => {
139+
function listenerA(): void {}
140+
141+
authInternal.addAuthTokenListener(listenerA);
142+
expect(isProactiveRefresh).to.be.true;
143+
authInternal.removeAuthTokenListener(listenerA);
144+
expect(isProactiveRefresh).to.be.false;
145+
});
146+
147+
it('toggles proactive refresh when single listener subbed twice', () => {
148+
function listenerA(): void {}
149+
150+
authInternal.addAuthTokenListener(listenerA);
151+
authInternal.addAuthTokenListener(listenerA);
152+
expect(isProactiveRefresh).to.be.true;
153+
authInternal.removeAuthTokenListener(listenerA);
154+
expect(isProactiveRefresh).to.be.false;
155+
});
156+
157+
it('toggles proactive refresh properly multiple listeners', () => {
158+
function listenerA(): void {}
159+
function listenerB(): void {}
160+
161+
authInternal.addAuthTokenListener(listenerA);
162+
authInternal.addAuthTokenListener(listenerB);
163+
expect(isProactiveRefresh).to.be.true;
164+
authInternal.removeAuthTokenListener(listenerA);
165+
expect(isProactiveRefresh).to.be.true;
166+
authInternal.removeAuthTokenListener(listenerB);
167+
expect(isProactiveRefresh).to.be.false;
168+
169+
authInternal.addAuthTokenListener(listenerB);
170+
expect(isProactiveRefresh).to.be.true;
171+
});
172+
});
173+
});
174+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { Unsubscribe } from '@firebase/util';
19+
20+
import { Auth } from '../../model/auth';
21+
22+
declare module '@firebase/component' {
23+
interface NameServiceMapping {
24+
'auth-internal-exp': AuthInternal;
25+
}
26+
}
27+
28+
interface TokenListener {
29+
(tok: string | null): unknown;
30+
}
31+
32+
export class AuthInternal {
33+
private readonly internalListeners: Map<
34+
TokenListener,
35+
Unsubscribe
36+
> = new Map();
37+
38+
constructor(private readonly auth: Auth) {}
39+
40+
getUid(): string | null {
41+
return this.auth.currentUser?.uid || null;
42+
}
43+
44+
async getToken(
45+
forceRefresh?: boolean
46+
): Promise<{ accessToken: string } | null> {
47+
await this.auth._initializationPromise;
48+
if (!this.auth.currentUser) {
49+
return null;
50+
}
51+
52+
const accessToken = await this.auth.currentUser.getIdToken(forceRefresh);
53+
return { accessToken };
54+
}
55+
56+
addAuthTokenListener(listener: TokenListener): void {
57+
if (this.internalListeners.has(listener)) {
58+
return;
59+
}
60+
61+
const unsubscribe = this.auth._onIdTokenChanged(user => {
62+
listener(user?.stsTokenManager.accessToken || null);
63+
});
64+
this.internalListeners.set(listener, unsubscribe);
65+
this.updateProactiveRefresh();
66+
}
67+
68+
removeAuthTokenListener(listener: TokenListener): void {
69+
const unsubscribe = this.internalListeners.get(listener);
70+
if (!unsubscribe) {
71+
return;
72+
}
73+
74+
this.internalListeners.delete(listener);
75+
unsubscribe();
76+
this.updateProactiveRefresh();
77+
}
78+
79+
private updateProactiveRefresh(): void {
80+
if (this.internalListeners.size > 0) {
81+
this.auth._startProactiveRefresh();
82+
} else {
83+
this.auth._stopProactiveRefresh();
84+
}
85+
}
86+
}

packages-exp/auth-exp/src/core/auth/register.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@ import { AuthErrorCode } from '../errors';
2424
import { assert } from '../util/assert';
2525
import { _getClientVersion, ClientPlatform } from '../util/version';
2626
import {
27+
_castAuth,
2728
AuthImpl,
2829
DEFAULT_API_HOST,
2930
DEFAULT_API_SCHEME,
3031
DEFAULT_TOKEN_API_HOST
3132
} from './auth_impl';
33+
import { AuthInternal } from './firebase_internal';
3234

3335
export const _AUTH_COMPONENT_NAME = 'auth-exp';
36+
export const _AUTH_INTERNAL_COMPONENT_NAME = 'auth-internal-exp';
3437

3538
function getVersionForPlatform(
3639
clientPlatform: ClientPlatform
@@ -70,6 +73,20 @@ export function registerAuth(clientPlatform: ClientPlatform): void {
7073
ComponentType.PUBLIC
7174
)
7275
);
76+
77+
_registerComponent(
78+
new Component(
79+
_AUTH_INTERNAL_COMPONENT_NAME,
80+
container => {
81+
const auth = _castAuth(
82+
container.getProvider(_AUTH_COMPONENT_NAME).getImmediate()!
83+
);
84+
return (auth => new AuthInternal(auth))(auth);
85+
},
86+
ComponentType.PRIVATE
87+
)
88+
);
89+
7390
registerVersion(
7491
_AUTH_COMPONENT_NAME,
7592
version,

0 commit comments

Comments
 (0)