Skip to content

Commit 2ac31a1

Browse files
sam-gcavolkovi
andauthored
A whole bunch of things to bring auth (compat) to parity (#3970)
* Handle anonymous auth re-login edge case * Formatting * Initial fixes * Fix redirect * clean up additional user info * Formatting * PR feedback * Fix some tests * Fix tests & write some new ones * Fix broken build * Formatting * Formatting * PR feedback * Formatting * PR feedback Co-authored-by: avolkovi <[email protected]>
1 parent 54a46f8 commit 2ac31a1

37 files changed

+465
-60
lines changed

packages-exp/auth-compat-exp/src/user_credential.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ export async function convertConfirmationResult(
116116
auth: externs.Auth,
117117
confirmationResultPromise: Promise<externs.ConfirmationResult>
118118
): Promise<compat.ConfirmationResult> {
119-
const { verificationId, confirm } = await confirmationResultPromise;
119+
const confirmationResultExp = await confirmationResultPromise;
120120
return {
121-
verificationId,
121+
verificationId: confirmationResultExp.verificationId,
122122
confirm: (verificationCode: string) =>
123-
convertCredential(auth, confirm(verificationCode))
123+
convertCredential(auth, confirmationResultExp.confirm(verificationCode))
124124
};
125125
}

packages-exp/auth-exp/src/api/account_management/profile.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ describe('api/account_management/updateProfile', () => {
3333
const request = {
3434
idToken: 'my-token',
3535
36-
password: 'my-password'
36+
password: 'my-password',
37+
returnSecureToken: true
3738
};
3839

3940
let auth: TestAuth;

packages-exp/auth-exp/src/api/account_management/profile.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface UpdateProfileRequest {
2323
idToken: string;
2424
displayName?: string | null;
2525
photoUrl?: string | null;
26+
returnSecureToken: boolean;
2627
}
2728

2829
export interface UpdateProfileResponse extends IdTokenResponse {

packages-exp/auth-exp/src/api/authentication/custom_token.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ use(chaiAsPromised);
3232

3333
describe('api/authentication/signInWithCustomToken', () => {
3434
const request = {
35-
token: 'my-token'
35+
token: 'my-token',
36+
returnSecureToken: true
3637
};
3738

3839
let auth: TestAuth;

packages-exp/auth-exp/src/api/authentication/custom_token.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Auth } from '@firebase/auth-types-exp';
2121

2222
export interface SignInWithCustomTokenRequest {
2323
token: string;
24+
returnSecureToken: boolean;
2425
}
2526

2627
export interface SignInWithCustomTokenResponse extends IdTokenResponse {}

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

+77-10
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export class AuthImpl implements Auth, _FirebaseService {
6060
private idTokenSubscription = new Subscription<externs.User>(this);
6161
private redirectUser: User | null = null;
6262
private isProactiveRefreshEnabled = false;
63+
private redirectInitializerError: Error | null = null;
6364

6465
// Any network calls will set this to true and prevent subsequent emulator
6566
// initialization
@@ -109,17 +110,21 @@ export class AuthImpl implements Auth, _FirebaseService {
109110
return;
110111
}
111112

112-
await this.initializeCurrentUser();
113+
await this.initializeCurrentUser(popupRedirectResolver);
113114

114115
if (this._deleted) {
115116
return;
116117
}
117118

118119
this._isInitialized = true;
119-
this.notifyAuthListeners();
120120
});
121121

122-
return this._initializationPromise;
122+
// After initialization completes, throw any error caused by redirect flow
123+
return this._initializationPromise.then(() => {
124+
if (this.redirectInitializerError) {
125+
throw this.redirectInitializerError;
126+
}
127+
});
123128
}
124129

125130
/**
@@ -149,18 +154,40 @@ export class AuthImpl implements Auth, _FirebaseService {
149154

150155
// Update current Auth state. Either a new login or logout.
151156
await this._updateCurrentUser(user);
152-
// Notify external Auth changes of Auth change event.
153-
this.notifyAuthListeners();
154157
}
155158

156-
private async initializeCurrentUser(): Promise<void> {
157-
const storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null;
159+
private async initializeCurrentUser(
160+
popupRedirectResolver?: externs.PopupRedirectResolver
161+
): Promise<void> {
162+
// First check to see if we have a pending redirect event.
163+
let storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null;
164+
if (popupRedirectResolver) {
165+
await this.getOrInitRedirectPersistenceManager();
166+
const redirectUserEventId = this.redirectUser?._redirectEventId;
167+
const storedUserEventId = storedUser?._redirectEventId;
168+
const result = await this.tryRedirectSignIn(popupRedirectResolver);
169+
170+
// If the stored user (i.e. the old "currentUser") has a redirectId that
171+
// matches the redirect user, then we want to initially sign in with the
172+
// new user object from result.
173+
// TODO(samgho): More thoroughly test all of this
174+
if (
175+
(!redirectUserEventId || redirectUserEventId === storedUserEventId) &&
176+
result?.user
177+
) {
178+
storedUser = result.user as User;
179+
}
180+
}
181+
182+
// If no user in persistence, there is no current user. Set to null.
158183
if (!storedUser) {
159-
return this.directlySetCurrentUser(storedUser);
184+
return this.directlySetCurrentUser(null);
160185
}
161186

162187
if (!storedUser._redirectEventId) {
163188
// This isn't a redirect user, we can reload and bail
189+
// This will also catch the redirected user, if available, as that method
190+
// strips the _redirectEventId
164191
return this.reloadAndSetCurrentUserOrClear(storedUser);
165192
}
166193

@@ -182,6 +209,42 @@ export class AuthImpl implements Auth, _FirebaseService {
182209
return this.reloadAndSetCurrentUserOrClear(storedUser);
183210
}
184211

212+
private async tryRedirectSignIn(
213+
redirectResolver: externs.PopupRedirectResolver
214+
): Promise<externs.UserCredential | null> {
215+
// The redirect user needs to be checked (and signed in if available)
216+
// during auth initialization. All of the normal sign in and link/reauth
217+
// flows call back into auth and push things onto the promise queue. We
218+
// need to await the result of the redirect sign in *inside the promise
219+
// queue*. This presents a problem: we run into deadlock. See:
220+
// ┌> [Initialization] ─────┐
221+
// ┌> [<other queue tasks>] │
222+
// └─ [getRedirectResult] <─┘
223+
// where [] are tasks on the queue and arrows denote awaits
224+
// Initialization will never complete because it's waiting on something
225+
// that's waiting for initialization to complete!
226+
//
227+
// Instead, this method calls getRedirectResult() (stored in
228+
// _completeRedirectFn) with an optional parameter that instructs all of
229+
// the underlying auth operations to skip anything that mutates auth state.
230+
231+
let result: externs.UserCredential | null = null;
232+
try {
233+
// We know this._popupRedirectResolver is set since redirectResolver
234+
// is passed in. The _completeRedirectFn expects the unwrapped extern.
235+
result = await this._popupRedirectResolver!._completeRedirectFn(
236+
this,
237+
redirectResolver,
238+
true
239+
);
240+
} catch (e) {
241+
this.redirectInitializerError = e;
242+
await this._setRedirectUser(null);
243+
}
244+
245+
return result;
246+
}
247+
185248
private async reloadAndSetCurrentUserOrClear(user: User): Promise<void> {
186249
try {
187250
await _reloadWithoutSaving(user);
@@ -243,6 +306,7 @@ export class AuthImpl implements Auth, _FirebaseService {
243306
{ appName: this.name }
244307
);
245308
}
309+
246310
return this.queue(async () => {
247311
await this.directlySetCurrentUser(user as User | null);
248312
this.notifyAuthListeners();
@@ -335,8 +399,11 @@ export class AuthImpl implements Auth, _FirebaseService {
335399
}
336400

337401
async _redirectUserForId(id: string): Promise<User | null> {
338-
// Make sure we've cleared any pending ppersistence actions
339-
await this.queue(async () => {});
402+
// Make sure we've cleared any pending persistence actions if we're not in
403+
// the initializer
404+
if (this._isInitialized) {
405+
await this.queue(async () => {});
406+
}
340407

341408
if (this._currentUser?._redirectEventId === id) {
342409
return this._currentUser;

packages-exp/auth-exp/src/core/auth/firebase_internal.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ describe('core/auth/firebase_internal', () => {
5656
const user = testUser(auth, 'uid');
5757
await auth._updateCurrentUser(user);
5858
user.stsTokenManager.accessToken = 'access-token';
59+
user.stsTokenManager.refreshToken = 'refresh-token';
5960
user.stsTokenManager.expirationTime = Date.now() + 1000 * 60 * 60 * 24;
6061
expect(await authInternal.getToken()).to.eql({
6162
accessToken: 'access-token'

packages-exp/auth-exp/src/core/auth/initialize.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ describe('core/auth/initialize', () => {
106106
): void {
107107
cb(true);
108108
}
109+
async _completeRedirectFn(
110+
_auth: externs.Auth,
111+
_resolver: externs.PopupRedirectResolver,
112+
_bypassAuthState: boolean
113+
): Promise<externs.UserCredential | null> {
114+
return null;
115+
}
109116
}
110117

111118
const fakePopupRedirectResolver: externs.PopupRedirectResolver = FakePopupRedirectResolver;

packages-exp/auth-exp/src/core/providers/google.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ describe('core/providers/google', () => {
3939
expect(cred.signInMethod).to.eq(SignInMethod.GOOGLE);
4040
});
4141

42+
it('adds the profile scope by default', () => {
43+
const provider = new GoogleAuthProvider();
44+
expect(provider.providerId).to.eq(ProviderId.GOOGLE);
45+
expect(provider.getScopes()).to.eql(['profile']);
46+
});
47+
4248
it('credentialFromResult creates the cred from a tagged result', async () => {
4349
const auth = await testAuth();
4450
const userCred = new UserCredentialImpl({

packages-exp/auth-exp/src/core/providers/google.ts

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class GoogleAuthProvider extends OAuthProvider {
7373

7474
constructor() {
7575
super(externs.ProviderId.GOOGLE);
76+
this.addScope('profile');
7677
}
7778

7879
/**

packages-exp/auth-exp/src/core/strategies/credential.test.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
4242
import {
4343
linkWithCredential,
4444
reauthenticateWithCredential,
45-
signInWithCredential
45+
signInWithCredential,
46+
_signInWithCredential
4647
} from './credential';
4748

4849
use(chaiAsPromised);
@@ -111,6 +112,15 @@ describe('core/strategies/credential', () => {
111112
expect(auth.currentUser).to.eq(user);
112113
});
113114

115+
it('does not update the current user if bypass is true', async () => {
116+
stub(authCredential, '_getIdTokenResponse').returns(
117+
Promise.resolve(idTokenResponse)
118+
);
119+
const { user } = await _signInWithCredential(auth, authCredential, true);
120+
expect(auth.currentUser).to.be.null;
121+
expect(user).not.to.be.null;
122+
});
123+
114124
it('should handle MFA', async () => {
115125
const serverResponse: IdTokenMfaResponse = {
116126
localId: 'uid',

packages-exp/auth-exp/src/core/strategies/credential.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ import { _castAuth } from '../auth/auth_impl';
3030
/** @internal */
3131
export async function _signInWithCredential(
3232
auth: Auth,
33-
credential: AuthCredential
33+
credential: AuthCredential,
34+
bypassAuthState = false
3435
): Promise<UserCredential> {
3536
const operationType = OperationType.SIGN_IN;
3637
const response = await _processCredentialSavingMfaContextIfNecessary(
@@ -43,7 +44,10 @@ export async function _signInWithCredential(
4344
operationType,
4445
response
4546
);
46-
await auth._updateCurrentUser(userCredential.user);
47+
48+
if (!bypassAuthState) {
49+
await auth._updateCurrentUser(userCredential.user);
50+
}
4751
return userCredential;
4852
}
4953

packages-exp/auth-exp/src/core/strategies/custom_token.test.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,15 @@ describe('core/strategies/signInWithCustomToken', () => {
5252
};
5353

5454
let auth: TestAuth;
55+
let signInRoute: mockFetch.Route;
5556

5657
beforeEach(async () => {
5758
auth = await testAuth();
5859
mockFetch.setUp();
59-
mockEndpoint(Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN, idTokenResponse);
60+
signInRoute = mockEndpoint(
61+
Endpoint.SIGN_IN_WITH_CUSTOM_TOKEN,
62+
idTokenResponse
63+
);
6064
mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
6165
users: [serverUser]
6266
});
@@ -78,6 +82,14 @@ describe('core/strategies/signInWithCustomToken', () => {
7882
expect(operationType).to.eq(OperationType.SIGN_IN);
7983
});
8084

85+
it('should send with a valid request', async () => {
86+
await signInWithCustomToken(auth, 'j.w.t');
87+
expect(signInRoute.calls[0].request).to.eql({
88+
token: 'j.w.t',
89+
returnSecureToken: true
90+
});
91+
});
92+
8193
it('should update the current user', async () => {
8294
const { user } = await signInWithCustomToken(auth, 'oh.no');
8395
expect(auth.currentUser).to.eq(user);

packages-exp/auth-exp/src/core/strategies/custom_token.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export async function signInWithCustomToken(
4343
customToken: string
4444
): Promise<externs.UserCredential> {
4545
const response: IdTokenResponse = await getIdTokenResponse(auth, {
46-
token: customToken
46+
token: customToken,
47+
returnSecureToken: true
4748
});
4849
const authInternal = _castAuth(auth);
4950
const cred = await UserCredentialImpl._fromIdTokenResponse(

0 commit comments

Comments
 (0)