From bb0dc575c97f8b78addade152571f3e13b7512ae Mon Sep 17 00:00:00 2001 From: Yavor Georgiev Date: Fri, 3 Feb 2017 16:40:13 +0100 Subject: [PATCH] Implement proactive access token refresh (#842) Closes #840 --- CHANGELOG.md | 11 ++++++++++ lib/browser/index.js | 4 ++-- lib/browser/rpc.js | 8 ++++---- lib/user-methods.js | 49 ++++++++++++++++++++++++++++++-------------- src/js_sync.hpp | 4 ++-- src/rpc.cpp | 4 ++-- 6 files changed, 55 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dca33ff..6ad53602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +vNext Release notes (TBA) +============================================================= +### Breaking changes +* None + +### Enhancements +* None + +### Bug fixes +* Proactively refresh sync user tokens to avoid a reconnect delay (#840) + 1.0.1 Release notes (2017-2-2) ============================================================= ### Breaking changes diff --git a/lib/browser/index.js b/lib/browser/index.js index 2a6a31a8..efe9957b 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -179,9 +179,9 @@ Object.defineProperties(Realm, { for (let i = 0, len = debugHosts.length; i < len; i++) { try { - let authenticateRealmCallback = staticUserMethods._authenticateRealm.bind(User); + let refreshAccessTokenCallback = staticUserMethods._refreshAccessToken.bind(User); // The session ID refers to the Realm constructor object in the RPC server. - Realm[keys.id] = rpc.createSession(authenticateRealmCallback, debugHosts[i] + ':' + debugPort); + Realm[keys.id] = rpc.createSession(refreshAccessTokenCallback, debugHosts[i] + ':' + debugPort); break; } catch (e) { // Only throw exception after all hosts have been tried. diff --git a/lib/browser/rpc.js b/lib/browser/rpc.js index a8044b2a..faadb04b 100644 --- a/lib/browser/rpc.js +++ b/lib/browser/rpc.js @@ -25,7 +25,7 @@ const {id: idKey, realm: _realmKey} = keys; let registeredCallbacks = []; const typeConverters = {}; -// Callbacks that are registered initially (currently only authenticateRealm) will +// Callbacks that are registered initially (currently only refreshAccessToken) will // carry this symbol so they are not wiped in clearTestState. const persistentCallback = Symbol("persistentCallback"); @@ -49,9 +49,9 @@ export function registerTypeConverter(type, handler) { typeConverters[type] = handler; } -export function createSession(authenticateRealm, host) { - authenticateRealm[persistentCallback] = true; - sessionId = sendRequest('create_session', { authenticateRealm: serialize(undefined, authenticateRealm) }, host); +export function createSession(refreshAccessToken, host) { + refreshAccessToken[persistentCallback] = true; + sessionId = sendRequest('create_session', { refreshAccessToken: serialize(undefined, refreshAccessToken) }, host); sessionHost = host; return sessionId; diff --git a/lib/user-methods.js b/lib/user-methods.js index 626a7f81..c0469b1a 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -40,7 +40,13 @@ function auth_url(server) { return server + 'auth'; } -function authenticateRealm(user, fileUrl, realmUrl) { +function scheduleAccessTokenRefresh(user, localRealmPath, realmUrl, expirationDate) { + const refreshBuffer = 10 * 1000; + const timeout = expirationDate - Date.now() - refreshBuffer; + setTimeout(() => refreshAccessToken(user, localRealmPath, realmUrl), timeout); +} + +function refreshAccessToken(user, localRealmPath, realmUrl) { let parsedRealmUrl = url_parse(realmUrl); const url = auth_url(user.server); const options = { @@ -54,23 +60,36 @@ function authenticateRealm(user, fileUrl, realmUrl) { headers: postHeaders }; performFetch(url, options) - .then((response) => { + // in case something lower in the HTTP stack breaks, try again in 10 seconds + .catch(() => setTimeout(() => refreshAccessToken(user, localRealmPath, realmUrl), 10 * 1000)) + .then((response) => response.json().then((json) => { return { response, json }; })) + .then((responseAndJson) => { + const response = responseAndJson.response; + const json = responseAndJson.json; if (response.status != 200) { //FIXME: propagate error to session error handler + /* + let session = user._sessionForOnDiskPath(localRealmPath); + let errorHandler = session._errorHandler; + if (errorHandler) { + errorHandler(session, new AuthError(json)); + } + */ } else { - return response.json().then(function(body) { - // Look up a fresh instance of the user. - // We do this because in React Native Remote Debugging - // `Realm.clearTestState()` will have invalidated the user object - let newUser = user.constructor.all[user.identity]; - if (newUser) { - let session = newUser._sessionForOnDiskPath(fileUrl); - if (session) { - parsedRealmUrl.set('pathname', body.access_token.token_data.path); - session._refreshAccessToken(body.access_token.token, parsedRealmUrl.href); - } + // Look up a fresh instance of the user. + // We do this because in React Native Remote Debugging + // `Realm.clearTestState()` will have invalidated the user object + let newUser = user.constructor.all[user.identity]; + if (newUser) { + let session = newUser._sessionForOnDiskPath(localRealmPath); + if (session && session.state !== 'invalid') { + parsedRealmUrl.set('pathname', json.access_token.token_data.path); + session._refreshAccessToken(json.access_token.token, parsedRealmUrl.href); + + const tokenExpirationDate = new Date(json.access_token.token_data.expires * 1000); + scheduleAccessTokenRefresh(newUser, localRealmPath, realmUrl, tokenExpirationDate); } - }); + } } }); } @@ -146,7 +165,7 @@ module.exports = { }, callback); }, - _authenticateRealm: authenticateRealm + _refreshAccessToken: refreshAccessToken }, instance: { openManagementRealm() { diff --git a/src/js_sync.hpp b/src/js_sync.hpp index 35d1de1d..bb30192f 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -380,13 +380,13 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr } else { ObjectType user_constructor = Object::validated_get_object(protected_ctx, protected_sync, std::string("User")); - FunctionType authenticate = Object::validated_get_function(protected_ctx, user_constructor, std::string("_authenticateRealm")); + FunctionType refreshAccessToken = Object::validated_get_function(protected_ctx, user_constructor, std::string("_refreshAccessToken")); ValueType arguments[3]; arguments[0] = create_object>(protected_ctx, new SharedUser(config.user)); arguments[1] = Value::from_string(protected_ctx, path.c_str()); arguments[2] = Value::from_string(protected_ctx, config.realm_url.c_str()); - Function::call(protected_ctx, authenticate, 3, arguments); + Function::call(protected_ctx, refreshAccessToken, 3, arguments); } }); diff --git a/src/rpc.cpp b/src/rpc.cpp index c133f100..45570ace 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -113,11 +113,11 @@ RPCServer::RPCServer() { jsc::String realm_string = "Realm"; JSObjectRef realm_constructor = jsc::Object::validated_get_constructor(m_context, JSContextGetGlobalObject(m_context), realm_string); - JSValueRef authenticateRealm = deserialize_json_value(dict["authenticateRealm"]); + JSValueRef refreshAccessTokenCallback = deserialize_json_value(dict["refreshAccessToken"]); JSObjectRef sync_constructor = (JSObjectRef)jsc::Object::get_property(m_context, realm_constructor, "Sync"); JSObjectRef user_constructor = (JSObjectRef)jsc::Object::get_property(m_context, sync_constructor, "User"); - jsc::Object::set_property(m_context, user_constructor, "_authenticateRealm", authenticateRealm); + jsc::Object::set_property(m_context, user_constructor, "_refreshAccessToken", refreshAccessTokenCallback); m_session_id = store_object(realm_constructor); return (json){{"result", m_session_id}};