Implement proactive access token refresh (#842)

Closes #840
This commit is contained in:
Yavor Georgiev 2017-02-03 16:40:13 +01:00 committed by GitHub
parent f22efa7117
commit bb0dc575c9
6 changed files with 55 additions and 25 deletions

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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() {

View File

@ -380,13 +380,13 @@ void SyncClass<T>::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<T, UserClass<T>>(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);
}
});

View File

@ -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}};