-
Notifications
You must be signed in to change notification settings - Fork 938
Auth middleware (beforeAuthStateChanged) #6068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -78,6 +78,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
private redirectPersistenceManager?: PersistenceUserManager; | ||
private authStateSubscription = new Subscription<User>(this); | ||
private idTokenSubscription = new Subscription<User>(this); | ||
private beforeStateQueue: Array<(user: User | null) => Promise<void>> = []; | ||
private redirectUser: UserInternal | null = null; | ||
private isProactiveRefreshEnabled = false; | ||
|
||
|
@@ -181,7 +182,8 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
} | ||
|
||
// Update current Auth state. Either a new login or logout. | ||
await this._updateCurrentUser(user); | ||
// Skip blocking callbacks, they should not apply to a change in another tab. | ||
await this._updateCurrentUser(user, /* skipBeforeStateCallbacks */ true); | ||
} | ||
|
||
private async initializeCurrentUser( | ||
|
@@ -223,6 +225,14 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
_assert(this._popupRedirectResolver, this, AuthErrorCode.ARGUMENT_ERROR); | ||
await this.getOrInitRedirectPersistenceManager(); | ||
|
||
// At this point in the flow, this is a redirect user. Run blocking | ||
// middleware callbacks before setting the user. | ||
try { | ||
await this._runBeforeStateCallbacks(storedUser); | ||
} catch(e) { | ||
return; | ||
} | ||
|
||
// If the redirect user's event ID matches the current user's event ID, | ||
// DO NOT reload the current user, otherwise they'll be cleared from storage. | ||
// This is important for the reauthenticateWithRedirect() flow. | ||
|
@@ -313,7 +323,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
return this._updateCurrentUser(user && user._clone(this)); | ||
} | ||
|
||
async _updateCurrentUser(user: User | null): Promise<void> { | ||
async _updateCurrentUser(user: User | null, skipBeforeStateCallbacks: boolean = false): Promise<void> { | ||
if (this._deleted) { | ||
return; | ||
} | ||
|
@@ -325,19 +335,41 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
); | ||
} | ||
|
||
if (!skipBeforeStateCallbacks) { | ||
await this._runBeforeStateCallbacks(user); | ||
} | ||
|
||
return this.queue(async () => { | ||
await this.directlySetCurrentUser(user as UserInternal | null); | ||
this.notifyAuthListeners(); | ||
}); | ||
} | ||
|
||
async _runBeforeStateCallbacks(user: User | null): Promise<void> { | ||
if (this.currentUser === user) { | ||
return; | ||
} | ||
try { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method should probably check first whether or not the user is actually changing (like Right now with this code, if you call auth.signOut() twice in a row, it will call the middleware twice in a row There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a check. |
||
for (const beforeStateCallback of this.beforeStateQueue) { | ||
await beforeStateCallback(user); | ||
} | ||
} catch (e) { | ||
throw this._errorFactory.create( | ||
AuthErrorCode.LOGIN_BLOCKED, { originalMessage: e.message }); | ||
} | ||
} | ||
|
||
async signOut(): Promise<void> { | ||
// Run first, to block _setRedirectUser() if any callbacks fail. | ||
await this._runBeforeStateCallbacks(null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will run twice since it's also in _updateCurrentUser There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like it needs to run before |
||
// Clear the redirect user when signOut is called | ||
if (this.redirectPersistenceManager || this._popupRedirectResolver) { | ||
await this._setRedirectUser(null); | ||
} | ||
|
||
return this._updateCurrentUser(null); | ||
// Prevent callbacks from being called again in _updateCurrentUser, as | ||
// they were already called in the first line. | ||
return this._updateCurrentUser(null, /* skipBeforeStateCallbacks */ true); | ||
} | ||
|
||
setPersistence(persistence: Persistence): Promise<void> { | ||
|
@@ -371,6 +403,32 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
); | ||
} | ||
|
||
beforeAuthStateChanged( | ||
callback: (user: User | null) => void | Promise<void> | ||
): Unsubscribe { | ||
// The callback could be sync or async. Wrap it into a | ||
// function that is always async. | ||
const wrappedCallback = | ||
(user: User | null): Promise<void> => new Promise((resolve, reject) => { | ||
try { | ||
const result = callback(user); | ||
// Either resolve with existing promise or wrap a non-promise | ||
// return value into a promise. | ||
resolve(result); | ||
} catch (e) { | ||
// Sync callback throws. | ||
reject(e); | ||
} | ||
}); | ||
this.beforeStateQueue.push(wrappedCallback); | ||
const index = this.beforeStateQueue.length - 1; | ||
return () => { | ||
// Unsubscribe. Replace with no-op. Do not remove from array, or it will disturb | ||
// indexing of other elements. | ||
this.beforeStateQueue[index] = () => Promise.resolve(); | ||
}; | ||
} | ||
|
||
onIdTokenChanged( | ||
nextOrObserver: NextOrObserver<User>, | ||
error?: ErrorFn, | ||
|
@@ -429,7 +487,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
// Make sure we've cleared any pending persistence actions if we're not in | ||
// the initializer | ||
if (this._isInitialized) { | ||
await this.queue(async () => {}); | ||
await this.queue(async () => { }); | ||
} | ||
|
||
if (this._currentUser?._redirectEventId === id) { | ||
|
@@ -500,7 +558,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { | |
completed?: CompleteFn | ||
): Unsubscribe { | ||
if (this._deleted) { | ||
return () => {}; | ||
return () => { }; | ||
} | ||
|
||
const cb = | ||
|
@@ -607,7 +665,7 @@ class Subscription<T> { | |
observer => (this.observer = observer) | ||
); | ||
|
||
constructor(readonly auth: AuthInternal) {} | ||
constructor(readonly auth: AuthInternal) { } | ||
|
||
get next(): NextFn<T | null> { | ||
_assert(this.observer, this.auth, AuthErrorCode.INTERNAL_ERROR); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we want to tackle the second argument, the optional
onAbort
callback in this PR?That is just a
() => void
that is called useful ifbeforeAuthStateChanged
is called multiple times and a subsequent callback rejects the state change. This gives developers a chance to walk back any side effects.Useful to me as I integrate with the web frameworks work, for authenticated server-context. onAbort would allow me the chance to destroy the just minted cookie, if the developer is using beforeAuthStateChanged too and rejected the sign in.