Skip to content

Commit 262557d

Browse files
committed
WIP: MSC2918 initial implementation
1 parent e9ce87e commit 262557d

File tree

3 files changed

+111
-4
lines changed

3 files changed

+111
-4
lines changed

src/matrix/SessionContainer.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {Reconnector, ConnectionStatus} from "./net/Reconnector.js";
2121
import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay.js";
2222
import {MediaRepository} from "./net/MediaRepository.js";
2323
import {RequestScheduler} from "./net/RequestScheduler.js";
24+
import {TokenRefresher} from "./net/TokenRefresher.js";
2425
import {HomeServerError, ConnectionError, AbortError} from "./error.js";
2526
import {Sync, SyncStatus} from "./Sync.js";
2627
import {Session} from "./Session.js";
@@ -105,7 +106,13 @@ export class SessionContainer {
105106
accessToken: loginData.access_token,
106107
lastUsed: clock.now()
107108
};
108-
await this._platform.sessionInfoStorage.add(sessionInfo);
109+
110+
if (loginData.refresh_token) {
111+
sessionInfo.accessTokenExpiresAt = clock.now() + loginData.expires_in * 1000;
112+
sessionInfo.refreshToken = loginData.refresh_token;
113+
}
114+
115+
await this._platform.sessionInfoStorage.add(sessionInfo);
109116
} catch (err) {
110117
this._error = err;
111118
if (err instanceof HomeServerError) {
@@ -143,13 +150,31 @@ export class SessionContainer {
143150
retryDelay: new ExponentialRetryDelay(clock.createTimeout),
144151
createMeasure: clock.createMeasure
145152
});
153+
154+
let accessToken;
155+
if (sessionInfo.refreshToken) {
156+
this._tokenRefresher = new TokenRefresher({
157+
accessToken: sessionInfo.accessToken,
158+
accessTokenExpiresAt: sessionInfo.accessTokenExpiresAt,
159+
refreshToken: sessionInfo.refreshToken,
160+
anticipation: 10 * 1000, // Refresh 10 seconds before the expiration
161+
clock,
162+
});
163+
accessToken = this._tokenRefresher.accessToken;
164+
} else {
165+
accessToken = new ObservableValue(sessionInfo.accessToken);
166+
}
167+
146168
const hsApi = new HomeServerApi({
147169
homeServer: sessionInfo.homeServer,
148-
accessToken: sessionInfo.accessToken,
170+
accessToken,
149171
request: this._platform.request,
150172
reconnector: this._reconnector,
151173
createTimeout: clock.createTimeout
152174
});
175+
if (this._tokenRefresher) {
176+
await this._tokenRefresher.start(hsApi);
177+
}
153178
this._sessionId = sessionInfo.id;
154179
this._storage = await this._platform.storageFactory.create(sessionInfo.id);
155180
// no need to pass access token to session

src/matrix/net/HomeServerApi.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export class HomeServerApi {
101101
return `${this._homeserver}/_matrix/client/r0${csPath}`;
102102
}
103103

104+
_unstableUrl(feature, csPath) {
105+
return `${this._homeserver}/_matrix/client/unstable/${feature}${csPath}`;
106+
}
107+
104108
_baseRequest(method, url, queryParams, body, options, accessToken) {
105109
const queryString = encodeQueryParams(queryParams);
106110
url = `${url}?${queryString}`;
@@ -157,7 +161,7 @@ export class HomeServerApi {
157161
}
158162

159163
_authedRequest(method, url, queryParams, body, options) {
160-
return this._baseRequest(method, url, queryParams, body, options, this._accessToken);
164+
return this._baseRequest(method, url, queryParams, body, options, this._accessToken.get());
161165
}
162166

163167
_post(csPath, queryParams, body, options) {
@@ -196,7 +200,9 @@ export class HomeServerApi {
196200
}
197201

198202
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
199-
return this._unauthedRequest("POST", this._url("/login"), null, {
203+
return this._unauthedRequest("POST", this._url("/login"), {
204+
"org.matrix.msc2918.refresh_token": "true"
205+
}, {
200206
"type": "m.login.password",
201207
"identifier": {
202208
"type": "m.id.user",
@@ -207,6 +213,12 @@ export class HomeServerApi {
207213
}, options);
208214
}
209215

216+
refreshToken(token, options = null) {
217+
return this._unauthedRequest("POST", this._unstableUrl("org.matrix.msc2918.refresh_token", "/refresh"), null, {
218+
"refresh_token": token
219+
}, options);
220+
}
221+
210222
createFilter(userId, filter, options = null) {
211223
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
212224
}

src/matrix/net/TokenRefresher.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ObservableValue } from "../../observable/ObservableValue.js";
2+
3+
export class TokenRefresher {
4+
constructor({
5+
refreshToken,
6+
accessToken,
7+
accessTokenExpiresAt,
8+
anticipation,
9+
clock,
10+
}) {
11+
this._refreshToken = new ObservableValue(refreshToken);
12+
this._accessToken = new ObservableValue(accessToken);
13+
this._accessTokenExpiresAt = new ObservableValue(accessTokenExpiresAt);
14+
this._anticipation = anticipation;
15+
this._clock = clock;
16+
}
17+
18+
async start(hsApi) {
19+
this._hsApi = hsApi;
20+
if (this.needsRenewing) {
21+
await this.renew();
22+
}
23+
24+
this._renewingLoop();
25+
}
26+
27+
get needsRenewing() {
28+
const remaining = this._accessTokenExpiresAt.get() - this._clock.now();
29+
const anticipated = remaining - this._anticipation;
30+
return anticipated < 0;
31+
}
32+
33+
async _renewingLoop() {
34+
while (true) {
35+
const remaining =
36+
this._accessTokenExpiresAt.get() - this._clock.now();
37+
const anticipated = remaining - this._anticipation;
38+
39+
if (anticipated > 0) {
40+
this._timeout = this._clock.createTimeout(anticipated);
41+
await this._timeout.elapsed();
42+
}
43+
44+
await this.renew();
45+
}
46+
}
47+
48+
async renew() {
49+
const response = await this._hsApi
50+
.refreshToken(this._refreshToken.get())
51+
.response();
52+
53+
if (response["refresh_token"]) {
54+
this._refreshToken.set(response["refresh_token"]);
55+
}
56+
57+
this._accessToken.set(response["access_token"]);
58+
this._accessTokenExpiresAt.set(
59+
this._clock.now() + response["expires_in"] * 1000
60+
);
61+
}
62+
63+
get accessToken() {
64+
return this._accessToken;
65+
}
66+
67+
get refreshToken() {
68+
return this._refreshToken;
69+
}
70+
}

0 commit comments

Comments
 (0)