Skip to content

Commit df41e56

Browse files
committed
Fix redirect
1 parent b49a56e commit df41e56

File tree

11 files changed

+104
-53
lines changed

11 files changed

+104
-53
lines changed

common/api-review/auth-exp.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export function getIdTokenResult(externUser: externs.User, forceRefresh?: boolea
156156
export function getMultiFactorResolver(auth: externs.Auth, errorExtern: externs.MultiFactorError): externs.MultiFactorResolver;
157157

158158
// @public (undocumented)
159-
export function getRedirectResult(authExtern: externs.Auth, resolverExtern?: externs.PopupRedirectResolver): Promise<externs.UserCredential | null>;
159+
export function getRedirectResult(authExtern: externs.Auth, resolverExtern?: externs.PopupRedirectResolver, bypassAuthState?: boolean): Promise<externs.UserCredential | null>;
160160

161161
// @public (undocumented)
162162
export class GithubAuthProvider extends OAuthProvider {
@@ -176,6 +176,7 @@ export class GithubAuthProvider extends OAuthProvider {
176176

177177
// @public (undocumented)
178178
export class GoogleAuthProvider extends OAuthProvider {
179+
constructor();
179180
// (undocumented)
180181
static credential(idToken?: string | null, accessToken?: string | null): externs.OAuthCredential;
181182
// (undocumented)

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

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export class AuthImpl implements Auth, _FirebaseService {
5858
private idTokenSubscription = new Subscription<externs.User>(this);
5959
private redirectUser: User | null = null;
6060
private isProactiveRefreshEnabled = false;
61+
private redirectInitializerError: Error | null = null;
6162

6263
// Any network calls will set this to true and prevent subsequent emulator
6364
// initialization
@@ -114,7 +115,13 @@ export class AuthImpl implements Auth, _FirebaseService {
114115
}
115116

116117
this._isInitialized = true;
117-
this.notifyAuthListeners();
118+
});
119+
120+
// After initialization completes, throw any error caused by redirect flow
121+
this._initializationPromise.then(() => {
122+
if (this.redirectInitializerError) {
123+
throw this.redirectInitializerError;
124+
}
118125
});
119126

120127
return this._initializationPromise;
@@ -147,27 +154,34 @@ export class AuthImpl implements Auth, _FirebaseService {
147154

148155
// Update current Auth state. Either a new login or logout.
149156
await this._updateCurrentUser(user);
150-
// Notify external Auth changes of Auth change event.
151-
this.notifyAuthListeners();
152157
}
153158

154159
private async initializeCurrentUser(popupRedirectResolver?: externs.PopupRedirectResolver): Promise<void> {
155160
// First check to see if we have a pending redirect event.
161+
let storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null;
156162
if (popupRedirectResolver) {
157163
await this.getOrInitRedirectPersistenceManager();
158-
const result = await this._popupRedirectResolver!._completeRedirectFn(this, popupRedirectResolver);
159-
if (result) {
160-
return;
161-
};
164+
const redirectUserEventId = this.redirectUser?._redirectEventId;
165+
const storedUserEventId = storedUser?._redirectEventId;
166+
const result = await this.tryRedirectSignIn(popupRedirectResolver);
167+
168+
// If the stored user (i.e. the old "currentUser") has a redirectId that
169+
// matches the redirect user, then we want to initially sign in with the
170+
// new user object from result.
171+
if ((!redirectUserEventId || (redirectUserEventId === storedUserEventId)) && result?.user) {
172+
storedUser = result.user as User
173+
}
162174
}
163175

164-
const storedUser = (await this.assertedPersistence.getCurrentUser()) as User | null;
176+
// If no user in persistence, there is no current user. Set to null.
165177
if (!storedUser) {
166-
return this.directlySetCurrentUser(storedUser);
178+
return this.directlySetCurrentUser(null);
167179
}
168180

169181
if (!storedUser._redirectEventId) {
170182
// This isn't a redirect user, we can reload and bail
183+
// This will also catch the redirected user, if available, as that method
184+
// strips the _redirectEventId
171185
return this.reloadAndSetCurrentUserOrClear(storedUser);
172186
}
173187

@@ -189,6 +203,36 @@ export class AuthImpl implements Auth, _FirebaseService {
189203
return this.reloadAndSetCurrentUserOrClear(storedUser);
190204
}
191205

206+
private async tryRedirectSignIn(redirectResolver: externs.PopupRedirectResolver): Promise<externs.UserCredential | null> {
207+
// The redirect user needs to be checked (and signed in if available)
208+
// during auth initialization. All of the normal sign in and link/reauth
209+
// flows call back into auth and push things onto the promise queue. We
210+
// need to await the result of the redirect sign in *inside the promise
211+
// queue*. This presents a problem: we run into deadlock. See:
212+
// ┌> [Initialization] ─────┐
213+
// ┌> [<other queue tasks>] │
214+
// └─ [getRedirectResult] <─┘
215+
// where [] are tasks on the queue and arrows denote awaits
216+
// Initialization will never complete because it's waiting on something
217+
// that's waiting for initialization to complete!
218+
//
219+
// Instead, this method calls getRedirectResult() (stored in
220+
// _completeRedirectFn) with an optional parameter that instructs all of
221+
// the underlying auth operations to skip anything that mutates auth state.
222+
223+
let result: externs.UserCredential | null = null;
224+
try {
225+
// We know this._popupRedirectResolver is set since redirectResolver
226+
// is passed in. The _completeRedirectFn expects the unwrapped extern.
227+
result = await this._popupRedirectResolver!._completeRedirectFn(this, redirectResolver, true);
228+
} catch (e) {
229+
this.redirectInitializerError = e;
230+
await this._setRedirectUser(null);
231+
}
232+
233+
return result;
234+
}
235+
192236
private async reloadAndSetCurrentUserOrClear(user: User): Promise<void> {
193237
try {
194238
await _reloadWithoutSaving(user);
@@ -236,10 +280,10 @@ export class AuthImpl implements Auth, _FirebaseService {
236280
}
237281
);
238282

239-
return this._updateCurrentUser(user && user._clone(), false);
283+
return this._updateCurrentUser(user && user._clone());
240284
}
241285

242-
async _updateCurrentUser(user: externs.User | null, isInternal = true): Promise<void> {
286+
async _updateCurrentUser(user: externs.User | null): Promise<void> {
243287
if (this._deleted) {
244288
return;
245289
}
@@ -251,20 +295,10 @@ export class AuthImpl implements Auth, _FirebaseService {
251295
);
252296
}
253297

254-
const action = async (): Promise<void> => {
298+
return this.queue(async () => {
255299
await this.directlySetCurrentUser(user as User | null);
256300
this.notifyAuthListeners();
257-
};
258-
259-
// Bypass the queue if this is an internal invocation and initialization is
260-
// incomplete. This state only occurs if there's a pending redirect
261-
// operation. That redirect operation was invoked from within the queue,
262-
// meaning that we're safe to directly set the user
263-
if (isInternal && !this._isInitialized) {
264-
return action();
265-
}
266-
267-
return this.queue(action);
301+
});
268302
}
269303

270304
async signOut(): Promise<void> {
@@ -273,7 +307,7 @@ export class AuthImpl implements Auth, _FirebaseService {
273307
await this._setRedirectUser(null);
274308
}
275309

276-
return this._updateCurrentUser(null, false);
310+
return this._updateCurrentUser(null);
277311
}
278312

279313
setPersistence(persistence: externs.Persistence): Promise<void> {
@@ -372,11 +406,7 @@ export class AuthImpl implements Auth, _FirebaseService {
372406

373407
async _persistUserIfCurrent(user: User): Promise<void> {
374408
if (user === this.currentUser) {
375-
if (!this._isInitialized) {
376-
return this.directlySetCurrentUser(user);
377-
} else {
378-
return this.queue(async () => this.directlySetCurrentUser(user));
379-
}
409+
return this.queue(async () => this.directlySetCurrentUser(user));
380410
}
381411
}
382412

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export class GoogleAuthProvider extends OAuthProvider {
2929
static readonly PROVIDER_ID = externs.ProviderId.GOOGLE;
3030
readonly providerId = GoogleAuthProvider.PROVIDER_ID;
3131

32+
constructor() {
33+
super(externs.ProviderId.GOOGLE);
34+
this.addScope('profile');
35+
}
36+
3237
static credential(
3338
idToken?: string | null,
3439
accessToken?: string | null

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import { _castAuth } from '../auth/auth_impl';
2929

3030
export async function _signInWithCredential(
3131
auth: Auth,
32-
credential: AuthCredential
32+
credential: AuthCredential,
33+
bypassAuthState = false
3334
): Promise<UserCredential> {
3435
const operationType = OperationType.SIGN_IN;
3536
const response = await _processCredentialSavingMfaContextIfNecessary(
@@ -42,7 +43,10 @@ export async function _signInWithCredential(
4243
operationType,
4344
response
4445
);
45-
await auth._updateCurrentUser(userCredential.user);
46+
47+
if (!bypassAuthState) {
48+
await auth._updateCurrentUser(userCredential.user);
49+
}
4650
return userCredential;
4751
}
4852

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface IdpTaskParams {
3939
postBody?: string;
4040
pendingToken?: string;
4141
user?: User;
42+
bypassAuthState?: boolean;
4243
}
4344

4445
export type IdpTask = (params: IdpTaskParams) => Promise<UserCredential>;
@@ -84,18 +85,19 @@ class IdpCredential extends AuthCredential {
8485
export function _signIn(params: IdpTaskParams): Promise<UserCredential> {
8586
return _signInWithCredential(
8687
params.auth,
87-
new IdpCredential(params)
88+
new IdpCredential(params),
89+
params.bypassAuthState
8890
) as Promise<UserCredential>;
8991
}
9092

9193
export function _reauth(params: IdpTaskParams): Promise<UserCredential> {
9294
const { auth, user } = params;
9395
assert(user, AuthErrorCode.INTERNAL_ERROR, { appName: auth.name });
94-
return _reauthenticate(user, new IdpCredential(params));
96+
return _reauthenticate(user, new IdpCredential(params), params.bypassAuthState);
9597
}
9698

9799
export async function _link(params: IdpTaskParams): Promise<UserCredential> {
98100
const { auth, user } = params;
99101
assert(user, AuthErrorCode.INTERNAL_ERROR, { appName: auth.name });
100-
return _linkUser(user, new IdpCredential(params));
102+
return _linkUser(user, new IdpCredential(params), params.bypassAuthState);
101103
}

packages-exp/auth-exp/src/core/user/invalidation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ import { AuthErrorCode } from '../errors';
2222

2323
export async function _logoutIfInvalidated<T>(
2424
user: User,
25-
promise: Promise<T>
25+
promise: Promise<T>,
26+
bypassAuthState = false
2627
): Promise<T> {
28+
if (bypassAuthState) {
29+
return promise;
30+
}
2731
try {
2832
return await promise;
2933
} catch (e) {

packages-exp/auth-exp/src/core/user/link_unlink.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ export async function unlink(
6060
*/
6161
export async function _link(
6262
user: User,
63-
credential: AuthCredential
63+
credential: AuthCredential,
64+
bypassAuthState = false
6465
): Promise<UserCredential> {
6566
const response = await _logoutIfInvalidated(
6667
user,
67-
credential._linkToIdToken(user.auth, await user.getIdToken())
68+
credential._linkToIdToken(user.auth, await user.getIdToken()),
69+
bypassAuthState
6870
);
6971
return UserCredentialImpl._forOperation(
7072
user,

packages-exp/auth-exp/src/core/user/reauthenticate.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import { UserCredentialImpl } from './user_credential_impl';
2828

2929
export async function _reauthenticate(
3030
user: User,
31-
credential: AuthCredential
31+
credential: AuthCredential,
32+
bypassAuthState = false,
3233
): Promise<UserCredentialImpl> {
3334
const appName = user.auth.name;
3435
const operationType = OperationType.REAUTHENTICATE;
@@ -41,7 +42,8 @@ export async function _reauthenticate(
4142
operationType,
4243
credential,
4344
user
44-
)
45+
),
46+
bypassAuthState,
4547
);
4648
assert(response.idToken, AuthErrorCode.INTERNAL_ERROR, { appName });
4749
const parsed = _parseToken(response.idToken);

packages-exp/auth-exp/src/model/popup_redirect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,5 @@ export interface PopupRedirectResolver extends externs.PopupRedirectResolver {
9494
_redirectPersistence: externs.Persistence;
9595

9696
// This is needed so that auth does not have a hard dependency on redirect
97-
_completeRedirectFn: (auth: externs.Auth, resolver: externs.PopupRedirectResolver) => Promise<externs.UserCredential | null>;
97+
_completeRedirectFn: (auth: externs.Auth, resolver: externs.PopupRedirectResolver, bypassAuthState: boolean) => Promise<externs.UserCredential | null>;
9898
}

packages-exp/auth-exp/src/platform_browser/strategies/abstract_popup_redirect_operation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,16 @@ export abstract class AbstractPopupRedirectOperation
5050
private pendingPromise: PendingPromise | null = null;
5151
private eventManager: EventManager | null = null;
5252
readonly filter: AuthEventType[];
53+
// private readonly bypassAuthState: boolean;
5354

5455
abstract eventId: string | null;
5556

5657
constructor(
5758
protected readonly auth: Auth,
5859
filter: AuthEventType | AuthEventType[],
5960
protected readonly resolver: PopupRedirectResolver,
60-
protected user?: User
61+
protected user?: User,
62+
private readonly bypassAuthState = false,
6163
) {
6264
this.filter = Array.isArray(filter) ? filter : [filter];
6365
}
@@ -91,7 +93,8 @@ export abstract class AbstractPopupRedirectOperation
9193
sessionId: sessionId!,
9294
tenantId: tenantId || undefined,
9395
postBody: postBody || undefined,
94-
user: this.user
96+
user: this.user,
97+
bypassAuthState: this.bypassAuthState
9598
};
9699

97100
try {

packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,20 @@ export async function linkWithRedirect(
9898

9999
export async function getRedirectResult(
100100
authExtern: externs.Auth,
101-
resolverExtern?: externs.PopupRedirectResolver
101+
resolverExtern?: externs.PopupRedirectResolver,
102+
bypassAuthState = false,
102103
): Promise<externs.UserCredential | null> {
103104
const auth = _castAuth(authExtern);
104105
const resolver = _withDefaultResolver(auth, resolverExtern);
105-
const action = new RedirectAction(auth, resolver);
106-
console.log('Executing redirect action');
106+
const action = new RedirectAction(auth, resolver, bypassAuthState);
107107
const result = await action.execute();
108108

109-
if (result) {
109+
if (result && !bypassAuthState) {
110110
delete result.user._redirectEventId;
111-
console.log('Persisting user');
112111
await auth._persistUserIfCurrent(result.user as User);
113-
console.log('Updating redirect user');
114112
await auth._setRedirectUser(null, resolverExtern);
115-
console.log('Finished');
116113
}
117114

118-
console.log('Finished with total');
119115
return result;
120116
}
121117

@@ -137,7 +133,7 @@ const redirectOutcomeMap: Map<
137133
class RedirectAction extends AbstractPopupRedirectOperation {
138134
eventId = null;
139135

140-
constructor(auth: Auth, resolver: PopupRedirectResolver) {
136+
constructor(auth: Auth, resolver: PopupRedirectResolver, bypassAuthState = false) {
141137
super(
142138
auth,
143139
[
@@ -146,7 +142,9 @@ class RedirectAction extends AbstractPopupRedirectOperation {
146142
AuthEventType.REAUTH_VIA_REDIRECT,
147143
AuthEventType.UNKNOWN
148144
],
149-
resolver
145+
resolver,
146+
undefined,
147+
bypassAuthState
150148
);
151149
}
152150

0 commit comments

Comments
 (0)