Skip to content

Commit dc70589

Browse files
authored
Track AuthEvent with ID to remove duplicate events (#1718)
1 parent 37873b6 commit dc70589

File tree

5 files changed

+179
-2
lines changed

5 files changed

+179
-2
lines changed

packages/auth/src/authevent.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ fireauth.AuthEvent.prototype.getEventId = function() {
118118
};
119119

120120

121+
/** @return {string} The event unique identifier. */
122+
fireauth.AuthEvent.prototype.getUid = function() {
123+
var components = [];
124+
components.push(this.type_);
125+
if (this.eventId_) {
126+
components.push(this.eventId_);
127+
}
128+
if (this.sessionId_) {
129+
components.push(this.sessionId_);
130+
}
131+
if (this.tenantId_) {
132+
components.push(this.tenantId_);
133+
}
134+
return components.join('-');
135+
};
136+
137+
121138
/** @return {?string} The url response of Auth event. */
122139
fireauth.AuthEvent.prototype.getUrlResponse = function() {
123140
return this.urlResponse_;

packages/auth/src/autheventmanager.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ goog.require('goog.array');
5050
* @constructor
5151
*/
5252
fireauth.AuthEventManager = function(authDomain, apiKey, appName) {
53+
/**
54+
* @private {!Object<string, boolean>} The map of processed auth event IDs.
55+
*/
56+
this.processedEvents_ = {};
57+
/** @private {number} The last saved processed event time in milliseconds. */
58+
this.lastProcessedEventTime_ = 0;
5359
/** @private {string} The Auth domain. */
5460
this.authDomain_ = authDomain;
5561
/** @private {string} The browser API key. */
@@ -111,6 +117,14 @@ fireauth.AuthEventManager = function(authDomain, apiKey, appName) {
111117
};
112118

113119

120+
/**
121+
* @const {number} The number of milliseconds since the last processed
122+
* event before the event duplication cache is cleared. This is currently
123+
* 10 minutes.
124+
*/
125+
fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION = 10 * 60 * 1000;
126+
127+
114128
/**
115129
* @return {!fireauth.RedirectAuthEventProcessor} The redirect event processor.
116130
*/
@@ -167,6 +181,7 @@ fireauth.AuthEventManager.prototype.reset = function() {
167181
fireauth.AuthEventManager.instantiateOAuthSignInHandler(
168182
this.authDomain_, this.apiKey_, this.appName_,
169183
firebase.SDK_VERSION || null);
184+
this.processedEvents_ = {};
170185
};
171186

172187

@@ -341,6 +356,48 @@ fireauth.AuthEventManager.prototype.unsubscribe = function(handler) {
341356
};
342357

343358

359+
/**
360+
* @param {?fireauth.AuthEvent} authEvent External Auth event to check.
361+
* @return {boolean} Whether the event was previously processed.
362+
* @private
363+
*/
364+
fireauth.AuthEventManager.prototype.hasProcessedAuthEvent_ =
365+
function(authEvent) {
366+
// Prevent duplicate event tracker from growing too large.
367+
if (goog.now() - this.lastProcessedEventTime_ >=
368+
fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION) {
369+
this.processedEvents_ = {};
370+
this.lastProcessedEventTime_ = 0;
371+
}
372+
if (authEvent && authEvent.getUid() &&
373+
this.processedEvents_.hasOwnProperty(authEvent.getUid())) {
374+
// If event is already processed, ignore it.
375+
return true;
376+
}
377+
return false;
378+
};
379+
380+
381+
/**
382+
* Saves the provided event uid to prevent processing duplication.
383+
* @param {?fireauth.AuthEvent} authEvent External Auth event to track in
384+
* processed list of events.
385+
* @private
386+
*/
387+
fireauth.AuthEventManager.prototype.saveProcessedAuthEvent_ =
388+
function(authEvent) {
389+
if (authEvent &&
390+
(authEvent.getSessionId() || authEvent.getEventId())) {
391+
// Save processed event ID. We keep the cache for 10 minutes to prevent it
392+
// from growing too large.
393+
this.processedEvents_[
394+
/** @type {string} */ (authEvent.getUid())] = true;
395+
// Save last processing time.
396+
this.lastProcessedEventTime_ = goog.now();
397+
}
398+
};
399+
400+
344401
/**
345402
* Handles external Auth event detected by the OAuth sign-in handler.
346403
* @param {?fireauth.AuthEvent} authEvent External Auth event detected by
@@ -356,6 +413,10 @@ fireauth.AuthEventManager.prototype.handleAuthEvent_ = function(authEvent) {
356413
if (!authEvent) {
357414
throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT);
358415
}
416+
if (this.hasProcessedAuthEvent_(authEvent)) {
417+
// If event is already processed, ignore it.
418+
return false;
419+
}
359420
// Initialize event processed status to false. When set to false, the event is
360421
// not clear to delete in the OAuth helper iframe as the owner of this event
361422
// could be a user in another tab.
@@ -368,6 +429,9 @@ fireauth.AuthEventManager.prototype.handleAuthEvent_ = function(authEvent) {
368429
var eventManager = this.typeToManager_[authEvent.getType()];
369430
if (eventManager) {
370431
eventManager.processAuthEvent(authEvent, potentialHandler);
432+
// Prevent events with event IDs or session IDs from duplicate
433+
// processing.
434+
this.saveProcessedAuthEvent_(authEvent);
371435
}
372436
// Event has been processed, free to clear in OAuth helper.
373437
processed = true;

packages/auth/test/authevent_test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ function testAuthEvent_error() {
214214

215215

216216
function testAuthEvent() {
217+
var unknownEvent = new fireauth.AuthEvent(
218+
fireauth.AuthEvent.Type.UNKNOWN,
219+
null,
220+
null,
221+
null,
222+
new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR));
223+
assertEquals('unknown', unknownEvent.getUid());
224+
217225
assertEquals(
218226
fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP,
219227
authEvent.getType());
@@ -225,6 +233,7 @@ function testAuthEvent() {
225233
assertNull(authEvent.getEventId());
226234
assertNull(authEvent.getError());
227235
assertFalse(authEvent.hasError());
236+
assertEquals('signInViaPopup-SESSION_ID', authEvent.getUid());
228237

229238
assertEquals(
230239
fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT,
@@ -235,6 +244,7 @@ function testAuthEvent() {
235244
authEvent2.getError());
236245
assertTrue(authEvent2.hasError());
237246
assertNull(authEvent2.getPostBody());
247+
assertEquals('signInViaRedirect-12345678', authEvent2.getUid());
238248
}
239249

240250

packages/auth/test/autheventmanager_test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ function testAuthEventManager_subscribeAndUnsubscribe() {
723723

724724
function testAuthEventManager_testEventToProcessor() {
725725
var recordedHandler;
726+
clock = new goog.testing.MockClock(true);
726727
asyncTestCase.waitForSignals(1);
727728
// This test is not environment specific.
728729
stubs.replace(
@@ -738,6 +739,9 @@ function testAuthEventManager_testEventToProcessor() {
738739
recordedHandler = handler;
739740
asyncTestCase.signal();
740741
},
742+
'removeAuthEventListener': function(handler) {
743+
recordedHandler = null;
744+
},
741745
'initializeAndWait': function() { return goog.Promise.resolve(); },
742746
'shouldBeInitializedEarly': function() {
743747
return false;
@@ -917,6 +921,76 @@ function testAuthEventManager_testEventToProcessor() {
917921
3,
918922
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
919923
getCallCount());
924+
925+
// Duplicate events with event IDs or session IDs should be ignored.
926+
assertFalse(recordedHandler(signInViaPopupEvent));
927+
assertFalse(recordedHandler(signInViaRedirectEvent));
928+
assertFalse(recordedHandler(linkViaPopupEvent));
929+
assertFalse(recordedHandler(linkViaRedirectEvent));
930+
assertFalse(recordedHandler(reauthViaPopupEvent));
931+
assertFalse(recordedHandler(reauthViaRedirectEvent));
932+
assertEquals(
933+
4,
934+
fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent.
935+
getCallCount());
936+
assertEquals(
937+
3,
938+
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
939+
getCallCount());
940+
// Unknown events are allowed to be duplicated.
941+
assertTrue(recordedHandler(unknownEvent));
942+
assertEquals(
943+
5,
944+
fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent.
945+
getCallCount());
946+
assertEquals(
947+
3,
948+
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
949+
getCallCount());
950+
951+
// Reset should clear processed events.
952+
manager.reset();
953+
manager.initialize();
954+
assertTrue(recordedHandler(signInViaPopupEvent));
955+
assertEquals(
956+
5,
957+
fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent.
958+
getCallCount());
959+
assertEquals(
960+
4,
961+
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
962+
getCallCount());
963+
assertFalse(recordedHandler(signInViaPopupEvent));
964+
965+
// Simulate 1 millisecond before cachebuster triggers.
966+
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION - 1);
967+
// Event uid should still be saved.
968+
assertFalse(recordedHandler(signInViaPopupEvent));
969+
// Simulate one more millisecond to clear cache.
970+
clock.tick(1);
971+
// Event uid should be cleared.
972+
assertTrue(recordedHandler(signInViaPopupEvent));
973+
// This should be cached until next time cache is cleared.
974+
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION - 1);
975+
assertFalse(recordedHandler(signInViaPopupEvent));
976+
clock.tick(1);
977+
assertTrue(recordedHandler(signInViaPopupEvent));
978+
979+
// Halfway through timeout duration.
980+
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION / 2);
981+
// Trigger second event.
982+
assertTrue(recordedHandler(linkViaPopupEvent));
983+
// Halfway through timeout.
984+
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION / 2);
985+
// Both events still cached (second event should reset the counter).
986+
assertFalse(recordedHandler(signInViaPopupEvent));
987+
assertFalse(recordedHandler(linkViaPopupEvent));
988+
// Trigger timeout (half timeout duration).
989+
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION / 2);
990+
// Cache should be cleared from both events (full timeout duration from last
991+
// event).
992+
assertTrue(recordedHandler(signInViaPopupEvent));
993+
assertTrue(recordedHandler(linkViaPopupEvent));
920994
}
921995

922996

packages/auth/test/authuser_test.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8784,12 +8784,18 @@ function testUser_linkWithPopup_timeout() {
87848784
actualOnInit();
87858785
return goog.Promise.resolve();
87868786
});
8787+
oAuthSignInHandlerInstance.startPopupTimeout(
8788+
ignoreArgument, ignoreArgument, ignoreArgument)
8789+
.$does(function(popupWin, onError, delay) {
8790+
// Do nothing on first call to simulate timeout.
8791+
return new goog.Promise(function(resolve, reject) {});
8792+
}).$once();
87878793
oAuthSignInHandlerInstance.startPopupTimeout(
87888794
ignoreArgument, ignoreArgument, ignoreArgument)
87898795
.$does(function(popupWin, onError, delay) {
87908796
recordedHandler(expectedAuthEvent);
87918797
return new goog.Promise(function(resolve, reject) {});
8792-
}).$times(2);
8798+
}).$once();
87938799
mockControl.$replayAll();
87948800
// Set the backend user info with no linked providers.
87958801
stubs.replace(
@@ -8962,12 +8968,18 @@ function testUser_reauthenticateWithPopup_timeout() {
89628968
});
89638969
oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false);
89648970
oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false);
8971+
oAuthSignInHandlerInstance.startPopupTimeout(
8972+
ignoreArgument, ignoreArgument, ignoreArgument)
8973+
.$does(function(popupWin, onError, delay) {
8974+
// Do nothing on first call to simulate timeout.
8975+
return new goog.Promise(function(resolve, reject) {});
8976+
}).$once();
89658977
oAuthSignInHandlerInstance.startPopupTimeout(
89668978
ignoreArgument, ignoreArgument, ignoreArgument)
89678979
.$does(function(popupWin, onError, delay) {
89688980
recordedHandler(expectedAuthEvent);
89698981
return new goog.Promise(function(resolve, reject) {});
8970-
}).$times(2);
8982+
}).$once();
89718983
mockControl.$replayAll();
89728984
// The expected popup window object.
89738985
var expectedPopup = {

0 commit comments

Comments
 (0)