Skip to content

Track AuthEvent with ID to remove duplicate events #1718

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

Merged
merged 1 commit into from
Apr 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/auth/src/authevent.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,23 @@ fireauth.AuthEvent.prototype.getEventId = function() {
};


/** @return {string} The event unique identifier. */
fireauth.AuthEvent.prototype.getUid = function() {
var components = [];
components.push(this.type_);
if (this.eventId_) {
components.push(this.eventId_);
}
if (this.sessionId_) {
components.push(this.sessionId_);
}
if (this.tenantId_) {
components.push(this.tenantId_);
}
return components.join('-');
};


/** @return {?string} The url response of Auth event. */
fireauth.AuthEvent.prototype.getUrlResponse = function() {
return this.urlResponse_;
Expand Down
64 changes: 64 additions & 0 deletions packages/auth/src/autheventmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ goog.require('goog.array');
* @constructor
*/
fireauth.AuthEventManager = function(authDomain, apiKey, appName) {
/**
* @private {!Object<string, boolean>} The map of processed auth event IDs.
*/
this.processedEvents_ = {};
/** @private {number} The last saved processed event time in milliseconds. */
this.lastProcessedEventTime_ = 0;
/** @private {string} The Auth domain. */
this.authDomain_ = authDomain;
/** @private {string} The browser API key. */
Expand Down Expand Up @@ -111,6 +117,14 @@ fireauth.AuthEventManager = function(authDomain, apiKey, appName) {
};


/**
* @const {number} The number of milliseconds since the last processed
* event before the event duplication cache is cleared. This is currently
* 10 minutes.
*/
fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION = 10 * 60 * 1000;


/**
* @return {!fireauth.RedirectAuthEventProcessor} The redirect event processor.
*/
Expand Down Expand Up @@ -167,6 +181,7 @@ fireauth.AuthEventManager.prototype.reset = function() {
fireauth.AuthEventManager.instantiateOAuthSignInHandler(
this.authDomain_, this.apiKey_, this.appName_,
firebase.SDK_VERSION || null);
this.processedEvents_ = {};
};


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


/**
* @param {?fireauth.AuthEvent} authEvent External Auth event to check.
* @return {boolean} Whether the event was previously processed.
* @private
*/
fireauth.AuthEventManager.prototype.hasProcessedAuthEvent_ =
function(authEvent) {
// Prevent duplicate event tracker from growing too large.
if (goog.now() - this.lastProcessedEventTime_ >=
fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION) {
this.processedEvents_ = {};
this.lastProcessedEventTime_ = 0;
}
if (authEvent && authEvent.getUid() &&
this.processedEvents_.hasOwnProperty(authEvent.getUid())) {
// If event is already processed, ignore it.
return true;
}
return false;
};


/**
* Saves the provided event uid to prevent processing duplication.
* @param {?fireauth.AuthEvent} authEvent External Auth event to track in
* processed list of events.
* @private
*/
fireauth.AuthEventManager.prototype.saveProcessedAuthEvent_ =
function(authEvent) {
if (authEvent &&
(authEvent.getSessionId() || authEvent.getEventId())) {
// Save processed event ID. We keep the cache for 10 minutes to prevent it
// from growing too large.
this.processedEvents_[
/** @type {string} */ (authEvent.getUid())] = true;
// Save last processing time.
this.lastProcessedEventTime_ = goog.now();
}
};


/**
* Handles external Auth event detected by the OAuth sign-in handler.
* @param {?fireauth.AuthEvent} authEvent External Auth event detected by
Expand All @@ -356,6 +413,10 @@ fireauth.AuthEventManager.prototype.handleAuthEvent_ = function(authEvent) {
if (!authEvent) {
throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT);
}
if (this.hasProcessedAuthEvent_(authEvent)) {
// If event is already processed, ignore it.
return false;
}
// Initialize event processed status to false. When set to false, the event is
// not clear to delete in the OAuth helper iframe as the owner of this event
// could be a user in another tab.
Expand All @@ -368,6 +429,9 @@ fireauth.AuthEventManager.prototype.handleAuthEvent_ = function(authEvent) {
var eventManager = this.typeToManager_[authEvent.getType()];
if (eventManager) {
eventManager.processAuthEvent(authEvent, potentialHandler);
// Prevent events with event IDs or session IDs from duplicate
// processing.
this.saveProcessedAuthEvent_(authEvent);
}
// Event has been processed, free to clear in OAuth helper.
processed = true;
Expand Down
10 changes: 10 additions & 0 deletions packages/auth/test/authevent_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ function testAuthEvent_error() {


function testAuthEvent() {
var unknownEvent = new fireauth.AuthEvent(
fireauth.AuthEvent.Type.UNKNOWN,
null,
null,
null,
new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR));
assertEquals('unknown', unknownEvent.getUid());

assertEquals(
fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP,
authEvent.getType());
Expand All @@ -225,6 +233,7 @@ function testAuthEvent() {
assertNull(authEvent.getEventId());
assertNull(authEvent.getError());
assertFalse(authEvent.hasError());
assertEquals('signInViaPopup-SESSION_ID', authEvent.getUid());

assertEquals(
fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT,
Expand All @@ -235,6 +244,7 @@ function testAuthEvent() {
authEvent2.getError());
assertTrue(authEvent2.hasError());
assertNull(authEvent2.getPostBody());
assertEquals('signInViaRedirect-12345678', authEvent2.getUid());
}


Expand Down
74 changes: 74 additions & 0 deletions packages/auth/test/autheventmanager_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ function testAuthEventManager_subscribeAndUnsubscribe() {

function testAuthEventManager_testEventToProcessor() {
var recordedHandler;
clock = new goog.testing.MockClock(true);
asyncTestCase.waitForSignals(1);
// This test is not environment specific.
stubs.replace(
Expand All @@ -738,6 +739,9 @@ function testAuthEventManager_testEventToProcessor() {
recordedHandler = handler;
asyncTestCase.signal();
},
'removeAuthEventListener': function(handler) {
recordedHandler = null;
},
'initializeAndWait': function() { return goog.Promise.resolve(); },
'shouldBeInitializedEarly': function() {
return false;
Expand Down Expand Up @@ -917,6 +921,76 @@ function testAuthEventManager_testEventToProcessor() {
3,
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
getCallCount());

// Duplicate events with event IDs or session IDs should be ignored.
assertFalse(recordedHandler(signInViaPopupEvent));
assertFalse(recordedHandler(signInViaRedirectEvent));
assertFalse(recordedHandler(linkViaPopupEvent));
assertFalse(recordedHandler(linkViaRedirectEvent));
assertFalse(recordedHandler(reauthViaPopupEvent));
assertFalse(recordedHandler(reauthViaRedirectEvent));
assertEquals(
4,
fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent.
getCallCount());
assertEquals(
3,
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
getCallCount());
// Unknown events are allowed to be duplicated.
assertTrue(recordedHandler(unknownEvent));
assertEquals(
5,
fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent.
getCallCount());
assertEquals(
3,
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
getCallCount());

// Reset should clear processed events.
manager.reset();
manager.initialize();
assertTrue(recordedHandler(signInViaPopupEvent));
assertEquals(
5,
fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent.
getCallCount());
assertEquals(
4,
fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.
getCallCount());
assertFalse(recordedHandler(signInViaPopupEvent));

// Simulate 1 millisecond before cachebuster triggers.
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION - 1);
// Event uid should still be saved.
assertFalse(recordedHandler(signInViaPopupEvent));
// Simulate one more millisecond to clear cache.
clock.tick(1);
// Event uid should be cleared.
assertTrue(recordedHandler(signInViaPopupEvent));
// This should be cached until next time cache is cleared.
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION - 1);
assertFalse(recordedHandler(signInViaPopupEvent));
clock.tick(1);
assertTrue(recordedHandler(signInViaPopupEvent));

// Halfway through timeout duration.
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION / 2);
// Trigger second event.
assertTrue(recordedHandler(linkViaPopupEvent));
// Halfway through timeout.
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION / 2);
// Both events still cached (second event should reset the counter).
assertFalse(recordedHandler(signInViaPopupEvent));
assertFalse(recordedHandler(linkViaPopupEvent));
// Trigger timeout (half timeout duration).
clock.tick(fireauth.AuthEventManager.EVENT_DUPLICATION_CACHE_DURATION / 2);
// Cache should be cleared from both events (full timeout duration from last
// event).
assertTrue(recordedHandler(signInViaPopupEvent));
assertTrue(recordedHandler(linkViaPopupEvent));
}


Expand Down
16 changes: 14 additions & 2 deletions packages/auth/test/authuser_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8784,12 +8784,18 @@ function testUser_linkWithPopup_timeout() {
actualOnInit();
return goog.Promise.resolve();
});
oAuthSignInHandlerInstance.startPopupTimeout(
ignoreArgument, ignoreArgument, ignoreArgument)
.$does(function(popupWin, onError, delay) {
// Do nothing on first call to simulate timeout.
return new goog.Promise(function(resolve, reject) {});
}).$once();
oAuthSignInHandlerInstance.startPopupTimeout(
ignoreArgument, ignoreArgument, ignoreArgument)
.$does(function(popupWin, onError, delay) {
recordedHandler(expectedAuthEvent);
return new goog.Promise(function(resolve, reject) {});
}).$times(2);
}).$once();
mockControl.$replayAll();
// Set the backend user info with no linked providers.
stubs.replace(
Expand Down Expand Up @@ -8962,12 +8968,18 @@ function testUser_reauthenticateWithPopup_timeout() {
});
oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false);
oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false);
oAuthSignInHandlerInstance.startPopupTimeout(
ignoreArgument, ignoreArgument, ignoreArgument)
.$does(function(popupWin, onError, delay) {
// Do nothing on first call to simulate timeout.
return new goog.Promise(function(resolve, reject) {});
}).$once();
oAuthSignInHandlerInstance.startPopupTimeout(
ignoreArgument, ignoreArgument, ignoreArgument)
.$does(function(popupWin, onError, delay) {
recordedHandler(expectedAuthEvent);
return new goog.Promise(function(resolve, reject) {});
}).$times(2);
}).$once();
mockControl.$replayAll();
// The expected popup window object.
var expectedPopup = {
Expand Down