Skip to content

Commit 97fdf68

Browse files
authored
Nh/refresh access token (#4147)
* Add a timer to refresh the access_token before it expires
1 parent 7c92d70 commit 97fdf68

9 files changed

Lines changed: 125 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
* Bug causing classes to be replaced by classes already in Gradle's classpath (#3568).
1313
* NullPointerException when notifying a single object that it changed (#4086).
1414

15+
### Enhancements
16+
17+
* [ObjectServer] Add a timer to refresh periodically the access_token.
18+
1519
## 2.3.0
1620

17-
### Object Server API Changes
21+
### Object Server API Changes
1822

1923
* Realm Sync v1.0.0 has been released, and Realm Mobile Platform is no longer considered in beta.
2024
* Breaking change: Location of Realm files are now placed in `getFilesDir()/<userIdentifier>` instead of `getFilesDir()/`.

realm/realm-library/src/androidTestObjectServer/java/io/realm/AuthenticateRequestTests.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import static org.junit.Assert.assertEquals;
2424
import static org.junit.Assert.assertFalse;
25+
import static org.junit.Assert.assertTrue;
2526
import static org.junit.Assert.fail;
2627
import static org.mockito.Matchers.any;
2728
import static org.mockito.Mockito.when;
@@ -39,10 +40,10 @@ public void setUp() {
3940
@Test
4041
public void realmLogin() throws URISyntaxException, JSONException {
4142
Token t = SyncTestUtils.createTestUser().getSyncUser().getUserToken();
42-
AuthenticateRequest request = AuthenticateRequest.realmLogin(t, new URI("realm://objectserver/" + t.value() + "/default"));
43+
AuthenticateRequest request = AuthenticateRequest.realmLogin(t, new URI("realm://objectserver/" + t.identity() + "/default"));
4344

4445
JSONObject obj = new JSONObject(request.toJson());
45-
assertEquals("/" + t.value() + "/default", obj.get("path"));
46+
assertEquals("/" + t.identity() + "/default", obj.get("path"));
4647
assertEquals(t.value(), obj.get("data"));
4748
assertEquals("realm", obj.get("provider"));
4849
}
@@ -60,10 +61,10 @@ public void userLogin() throws URISyntaxException, JSONException {
6061
@Test
6162
public void userRefresh() throws URISyntaxException, JSONException {
6263
Token t = SyncTestUtils.createTestUser().getSyncUser().getUserToken();
63-
AuthenticateRequest request = AuthenticateRequest.userRefresh(t);
64+
AuthenticateRequest request = AuthenticateRequest.userRefresh(t, new URI("realm://objectserver/" + t.identity() + "/default"));
6465

6566
JSONObject obj = new JSONObject(request.toJson());
66-
assertFalse(obj.has("path"));
67+
assertTrue(obj.has("path"));
6768
assertEquals(t.value(), obj.get("data"));
6869
assertEquals("realm", obj.get("provider"));
6970
}

realm/realm-library/src/main/java/io/realm/RealmResults.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ public void remove() {
789789
throw new UnsupportedOperationException("remove() is not supported by RealmResults iterators.");
790790
}
791791

792-
protected void checkRealmIsStable() {
792+
void checkRealmIsStable() {
793793
long version = table.getVersion();
794794
// Any change within a write transaction will immediately update the table version. This means that we
795795
// cannot depend on the tableVersion heuristic in that case.

realm/realm-library/src/objectServer/java/io/realm/SyncUser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ public String toJson() {
361361
* Returns {@code true} if the user is logged into the Realm Object Server. If this method returns {@code true} it
362362
* implies that the user has valid credentials that have not expired.
363363
* <p>
364-
* The user might still be have been logged out by the Realm Object Server which will not be detected before the
364+
* The user might still have been logged out by the Realm Object Server which will not be detected before the
365365
* user tries to actively synchronize a Realm. If a logged out user tries to synchronize a Realm, an error will be
366366
* reported to the {@link SyncSession.ErrorHandler} defined by
367367
* {@link SyncConfiguration.Builder#errorHandler(SyncSession.ErrorHandler)}.

realm/realm-library/src/objectServer/java/io/realm/internal/network/AuthenticateRequest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ public static AuthenticateRequest userLogin(SyncCredentials credentials) {
5656
/**
5757
* Generates a request for refreshing a user token.
5858
*/
59-
public static AuthenticateRequest userRefresh(Token userToken) {
59+
public static AuthenticateRequest userRefresh(Token userToken, URI serverUrl) {
6060
return new AuthenticateRequest("realm",
6161
userToken.value(),
6262
SyncManager.APP_ID,
63-
null,
63+
serverUrl.getPath(),
6464
Collections.<String, Object>emptyMap()
6565
);
6666
}

realm/realm-library/src/objectServer/java/io/realm/internal/network/AuthenticationServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public interface AuthenticationServer {
4848
* Before it expires, the client should try to refresh the token, effectively keeping the user logged in on the
4949
* Object Server. Failing to do so will cause a "soft logout", where the User will have limited access rights.
5050
*/
51-
AuthenticateResponse refreshUser(Token userToken, URL authenticationUrl);
51+
AuthenticateResponse refreshUser(Token userToken, URI serverUrl, URL authenticationUrl);
5252

5353
/**
5454
* Logs out the user on the Object Server by invalidating the refresh token. Each device should be given their

realm/realm-library/src/objectServer/java/io/realm/internal/network/ExponentialBackoffTask.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ protected boolean shouldAbortTask(T response) {
4444
}
4545
}
4646

47-
// Callback when task is have succeeded
47+
// Callback when task have succeeded
4848
protected abstract void onSuccess(T response);
4949

5050
// Callback when task has failed

realm/realm-library/src/objectServer/java/io/realm/internal/network/OkHttpAuthenticationServer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ public AuthenticateResponse loginToRealm(Token refreshToken, URI serverUrl, URL
6767
}
6868

6969
@Override
70-
public AuthenticateResponse refreshUser(Token userToken, URL authenticationUrl) {
70+
public AuthenticateResponse refreshUser(Token userToken, URI serverUrl, URL authenticationUrl) {
7171
try {
72-
String requestBody = AuthenticateRequest.userRefresh(userToken).toJson();
72+
String requestBody = AuthenticateRequest.userRefresh(userToken, serverUrl).toJson();
7373
return authenticate(authenticationUrl, requestBody);
7474
} catch (Exception e) {
7575
return AuthenticateResponse.from(new ObjectServerError(ErrorCode.UNKNOWN, e));

realm/realm-library/src/objectServer/java/io/realm/internal/objectserver/ObjectServerSession.java

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import java.net.URI;
2020
import java.util.HashMap;
2121
import java.util.concurrent.Future;
22+
import java.util.concurrent.ScheduledFuture;
23+
import java.util.concurrent.ScheduledThreadPoolExecutor;
24+
import java.util.concurrent.TimeUnit;
2225

2326
import io.realm.ErrorCode;
2427
import io.realm.ObjectServerError;
@@ -99,14 +102,18 @@ public final class ObjectServerSession {
99102
private long nativeSessionPointer;
100103
private final ObjectServerUser user;
101104
RealmAsyncTask networkRequest;
105+
private RealmAsyncTask refreshTokenTask;
106+
private RealmAsyncTask refreshTokenNetworkRequest;
102107
NetworkStateReceiver.ConnectionListener networkListener;
103108
private SyncPolicy syncPolicy;
104109

105110
// Keeping track of current FSM state
106111
private SessionState currentStateDescription;
107112
private FsmState currentState;
108113
private SyncSession userSession;
109-
private SyncSession publicSession;
114+
115+
private final static ScheduledThreadPoolExecutor REFRESH_TOKENS_EXECUTOR = new ScheduledThreadPoolExecutor(1);
116+
private final static long REFRESH_MARGIN_DELAY = TimeUnit.SECONDS.toMillis(10);
110117

111118
/**
112119
* Creates a new Object Server Session.
@@ -166,6 +173,8 @@ public synchronized void start() {
166173
* Stops the session. The session can no longer be used.
167174
*/
168175
public synchronized void stop() {
176+
// tries to stop any scheduled access_token refresh
177+
clearScheduledAccessTokenRefresh();
169178
currentState.onStop();
170179
}
171180

@@ -238,6 +247,25 @@ void stopNativeSession() {
238247
nativeUnbind(nativeSessionPointer);
239248
nativeSessionPointer = 0;
240249
}
250+
clearScheduledAccessTokenRefresh();
251+
}
252+
253+
// It is an error to call this function before calling Client::bind() state
254+
private boolean updateSessionAccessToken(String userToken) {
255+
if (nativeSessionPointer != 0 && isBound()) {
256+
nativeRefresh(nativeSessionPointer, userToken);
257+
return true;
258+
}
259+
return false;
260+
}
261+
262+
private void clearScheduledAccessTokenRefresh() {
263+
if (refreshTokenTask != null) {
264+
refreshTokenTask.cancel();
265+
}
266+
if (refreshTokenNetworkRequest != null) {
267+
refreshTokenNetworkRequest.cancel();
268+
}
241269
}
242270

243271
void removeAccessToken() {
@@ -260,6 +288,10 @@ void authenticateRealm(final Runnable onSuccess, final SyncSession.ErrorHandler
260288
if (networkRequest != null) {
261289
networkRequest.cancel();
262290
}
291+
// clear any previously scheduled refresh access_token
292+
// since we're going to obtain a new refresh_token
293+
clearScheduledAccessTokenRefresh();
294+
263295
// Authenticate in a background thread. This allows incremental backoff and retries in a safe manner.
264296
Future<?> task = SyncManager.NETWORK_POOL_EXECUTOR.submit(new ExponentialBackoffTask<AuthenticateResponse>() {
265297
@Override
@@ -279,6 +311,8 @@ protected void onSuccess(AuthenticateResponse response) {
279311
configuration.shouldDeleteRealmOnLogout()
280312
);
281313
user.addRealm(configuration.getServerUrl(), desc);
314+
// schedule a token refresh before it expires
315+
scheduleRefreshAccessToken(response.getAccessToken().expiresMs());
282316
onSuccess.run();
283317
}
284318

@@ -290,6 +324,78 @@ protected void onError(AuthenticateResponse response) {
290324
networkRequest = new RealmAsyncTaskImpl(task, SyncManager.NETWORK_POOL_EXECUTOR);
291325
}
292326

327+
private void scheduleRefreshAccessToken(long expireDateInMs) {
328+
// calculate the delay time before which we should refresh the access_token,
329+
// we adjust to 10 second to proactively refresh the access_token before the session
330+
// hit the expire date on the token
331+
long refreshAfter = expireDateInMs - System.currentTimeMillis() - REFRESH_MARGIN_DELAY;
332+
if (refreshAfter < 0) {
333+
// Token already expired
334+
RealmLog.debug("Expires time already reached for the access token, refreshing now");
335+
refreshAccessToken();
336+
337+
} else {
338+
RealmLog.debug("Scheduling an access_token refresh in " + (refreshAfter) + " milliseconds");
339+
if (refreshTokenTask != null) {
340+
refreshTokenTask.cancel();
341+
}
342+
343+
ScheduledFuture<?> task = REFRESH_TOKENS_EXECUTOR.schedule(new Runnable() {
344+
@Override
345+
public void run() {
346+
refreshAccessToken();
347+
}
348+
}, refreshAfter, TimeUnit.MILLISECONDS);
349+
refreshTokenTask = new RealmAsyncTaskImpl(task, REFRESH_TOKENS_EXECUTOR);
350+
}
351+
}
352+
353+
// Authenticate by getting access tokens for the specific Realm
354+
private void refreshAccessToken() {
355+
// Authenticate in a background thread. This allows incremental backoff and retries in a safe manner.
356+
if (refreshTokenNetworkRequest != null) {
357+
refreshTokenNetworkRequest.cancel();
358+
}
359+
Future<?> task = SyncManager.NETWORK_POOL_EXECUTOR.submit(new ExponentialBackoffTask<AuthenticateResponse>() {
360+
@Override
361+
protected AuthenticateResponse execute() {
362+
return authServer.refreshUser(user.getUserToken(), configuration.getServerUrl(), user.getAuthenticationUrl());
363+
}
364+
365+
@Override
366+
protected void onSuccess(AuthenticateResponse response) {
367+
synchronized (ObjectServerSession.this) {
368+
RealmLog.debug("Access Token refreshed successfully");
369+
if (updateSessionAccessToken(response.getAccessToken().value())) {
370+
RealmLog.debug("Token applied");
371+
// only schedule an update if the token was updated.
372+
// The callback might return will the session state is not BOUND
373+
// in this case we'll wait for the new session state to transition to
374+
// BOUND, which will schedule a refresh in the process
375+
376+
// this will also avoid updating a stopped session
377+
378+
// replaced the user old access_token
379+
ObjectServerUser.AccessDescription desc = new ObjectServerUser.AccessDescription(
380+
response.getAccessToken(),
381+
configuration.getPath(),
382+
configuration.shouldDeleteRealmOnLogout()
383+
);
384+
user.addRealm(configuration.getServerUrl(), desc);
385+
// schedule the next refresh
386+
scheduleRefreshAccessToken(response.getAccessToken().expiresMs());
387+
}
388+
}
389+
}
390+
391+
@Override
392+
protected void onError(AuthenticateResponse response) {
393+
RealmLog.error("Unrecoverable error, while refreshing the access Token (" + response.getError().toString() + ") reschedule will not happen");
394+
}
395+
});
396+
refreshTokenNetworkRequest = new RealmAsyncTaskImpl(task, SyncManager.NETWORK_POOL_EXECUTOR);
397+
}
398+
293399
/**
294400
* Checks if a user has valid credentials for accessing this Realm.
295401
*

0 commit comments

Comments
 (0)