Skip to content

Commit c7a965c

Browse files
feat: add refresh token handling http client
- implement a `MatrixRefreshTokenClient` to await token refresh before dispatching http requests - improve documentation about soft logout logic - fix dartdoc locations about soft logout Fixes: #2027 Signed-off-by: The one with the braid <info@braid.business>
1 parent 352b3fa commit c7a965c

File tree

3 files changed

+112
-7
lines changed

3 files changed

+112
-7
lines changed

lib/matrix.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export 'src/utils/matrix_file.dart';
6363
export 'src/utils/matrix_id_string_extension.dart';
6464
export 'src/utils/matrix_localizations.dart';
6565
export 'src/utils/native_implementations.dart';
66+
export 'src/utils/matrix_refresh_token_client.dart';
6667
export 'src/utils/room_enums.dart';
6768
export 'src/utils/room_member_change_type.dart';
6869
export 'src/utils/push_notification.dart';

lib/src/client.dart

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ class Client extends MatrixApi {
9292

9393
ShareKeysWith shareKeysWith;
9494

95+
/// Implement your https://spec.matrix.org/v1.9/client-server-api/#soft-logout
96+
/// logic here.
97+
/// Set this to [Client.refreshAccessToken] for the easiest way to handle the
98+
/// most common reason for soft logouts.
99+
///
100+
/// Please ensure to also provide a [MatrixRefreshTokenClient] as
101+
/// [httpClient] in order to handle soft logout on non-sync calls.
102+
///
103+
/// You may want to wrap the default
104+
/// [Client.refreshAccessToken] implementation with retry logic in case
105+
/// you run into situations where the token refresh may fail due to bad
106+
/// network connectivity.
107+
/// You can also perform a new login here by passing the existing deviceId.
95108
Future<void> Function(Client client)? onSoftLogout;
96109

97110
DateTime? get accessTokenExpiresAt => _accessTokenExpiresAt;
@@ -151,13 +164,19 @@ class Client extends MatrixApi {
151164
)? customImageResizer;
152165

153166
/// Create a client
154-
/// [clientName] = unique identifier of this client
167+
///
168+
/// [clientName]: unique identifier of this client
169+
///
155170
/// [databaseBuilder]: A function that creates the database instance, that will be used.
171+
///
156172
/// [legacyDatabaseBuilder]: Use this for your old database implementation to perform an automatic migration
173+
///
157174
/// [databaseDestroyer]: A function that can be used to destroy a database instance, for example by deleting files from disk.
175+
///
158176
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
159177
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
160178
/// KeyVerificationMethod.emoji: Compare emojis
179+
///
161180
/// [importantStateEvents]: A set of all the important state events to load when the client connects.
162181
/// To speed up performance only a set of state events is loaded on startup, those that are
163182
/// needed to display a room list. All the remaining state events are automatically post-loaded
@@ -171,24 +190,43 @@ class Client extends MatrixApi {
171190
/// - m.room.canonical_alias
172191
/// - m.room.tombstone
173192
/// - *some* m.room.member events, where needed
193+
///
194+
/// [httpClient]: The inner [Client] used to dispatch any HTTP requests
195+
/// performed by the SDK. The [Client] you pass here will by default be
196+
/// wrapped with a [FixedTimeoutHttpClient] with the specified
197+
/// [defaultNetworkRequestTimeout]. In case you do not wish this wrapper,
198+
/// you can later override the [httpClient] by using the
199+
/// [Client.httpClient] setter.
200+
///
201+
/// In case your homeserver supports refresh tokens, please ensure you
202+
/// provide a [MatrixRefreshTokenClient].
203+
///
174204
/// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
175205
/// in a room for the room list.
206+
///
176207
/// Set [requestHistoryOnLimitedTimeline] to controll the automatic behaviour if the client
177208
/// receives a limited timeline flag for a room.
209+
///
178210
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
179211
/// if there is no other displayname available. If not then this will return "Unknown user".
212+
///
180213
/// If [formatLocalpart] is true, then the localpart of an mxid will
181214
/// be formatted in the way, that all "_" characters are becomming white spaces and
182215
/// the first character of each word becomes uppercase.
216+
///
183217
/// If your client supports more login types like login with token or SSO, then add this to
184218
/// [supportedLoginTypes]. Set a custom [syncFilter] if you like. By default the app
185219
/// will use lazy_load_members.
220+
///
186221
/// Set [nativeImplementations] to [NativeImplementationsIsolate] in order to
187222
/// enable the SDK to compute some code in background.
223+
///
188224
/// Set [timelineEventTimeout] to the preferred time the Client should retry
189225
/// sending events on connection problems or to `Duration.zero` to disable it.
226+
///
190227
/// Set [customImageResizer] to your own implementation for a more advanced
191228
/// and faster image resizing experience.
229+
///
192230
/// Set [enableDehydratedDevices] to enable experimental support for enabling MSC3814 dehydrated devices.
193231
Client(
194232
this.clientName, {
@@ -222,12 +260,6 @@ class Client extends MatrixApi {
222260
this.shareKeysWith = ShareKeysWith.crossVerifiedIfEnabled,
223261
this.enableDehydratedDevices = false,
224262
this.receiptsPublicByDefault = true,
225-
226-
/// Implement your https://spec.matrix.org/v1.9/client-server-api/#soft-logout
227-
/// logic here.
228-
/// Set this to `refreshAccessToken()` for the easiest way to handle the
229-
/// most common reason for soft logouts.
230-
/// You can also perform a new login here by passing the existing deviceId.
231263
this.onSoftLogout,
232264

233265
/// Experimental feature which allows to send a custom refresh token
@@ -2438,6 +2470,7 @@ class Client extends MatrixApi {
24382470
),
24392471
);
24402472
if (e.error == MatrixError.M_UNKNOWN_TOKEN) {
2473+
// due to race conditions via QUIC, still handle soft_logout here
24412474
if (e.raw.tryGet<bool>('soft_logout') == true) {
24422475
Logs().w(
24432476
'The user has been soft logged out! Calling client.onSoftLogout() if present.',
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:http/http.dart' hide Client;
2+
3+
import 'package:matrix/matrix.dart';
4+
5+
/// a [BaseClient] implementation handling [Client.onSoftLogout]
6+
///
7+
/// This client wrapper takes matrix [Client] as parameter and handles token
8+
/// rotation.
9+
///
10+
/// Before dispatching any request, it will check whether the [Client] supports
11+
/// token refresh by checking [Client.accessTokenExpiresAt]. Token rotation is
12+
/// done when :
13+
/// - refresh is supported ([Client.accessTokenExpiresAt])
14+
/// - we are actually initialized ([Client.onSync.value])
15+
/// - the request is to the homeserver rather than e.g. IDP ([BaseRequest.url])
16+
/// - the request is authenticated ([BaseRequest.headers])
17+
/// - we're logged in ([Client.isLogged])
18+
///
19+
/// In this case, [Client.ensureNotSoftLoggedOut] is awaited before running
20+
/// [BaseClient.send]. If the [Client.bearerToken] was changed meanwhile,
21+
/// the [BaseRequest] is being adjusted.
22+
class MatrixRefreshTokenClient extends BaseClient {
23+
MatrixRefreshTokenClient({
24+
required this.inner,
25+
required this.client,
26+
});
27+
28+
/// the matrix [Client] to handle token rotation for
29+
final Client client;
30+
31+
/// the inner [BaseClient] to dispatch requests with
32+
final BaseClient inner;
33+
34+
@override
35+
Future<StreamedResponse> send(BaseRequest request) async {
36+
Request? req;
37+
if ( // only refresh if
38+
// refresh is supported
39+
client.accessTokenExpiresAt != null &&
40+
// we are actually initialized
41+
client.onSync.value != null &&
42+
// the request is to the homeserver rather than e.g. IDP
43+
request.url.host == client.homeserver?.host &&
44+
// the request is authenticated
45+
request.headers
46+
.map((k, v) => MapEntry(k.toLowerCase(), v))
47+
.containsKey('authorization') &&
48+
// and last but not least we're logged in
49+
client.isLogged()) {
50+
try {
51+
await client.ensureNotSoftLoggedOut();
52+
} catch (e) {
53+
Logs().w('Could not rotate token before dispatching HTTP request.', e);
54+
}
55+
// in every case ensure we run with the latest bearer token to avoid
56+
// race conditions
57+
finally {
58+
final headers = request.headers;
59+
// hours wasted : unknown :facepalm:
60+
headers.removeWhere((k, _) => k.toLowerCase() == 'authorization');
61+
headers['Authorization'] = 'Bearer ${client.bearerToken!}';
62+
req = Request(request.method, request.url);
63+
req.headers.addAll(headers);
64+
if (request is Request) {
65+
req.bodyBytes = request.bodyBytes;
66+
}
67+
}
68+
}
69+
return inner.send(req ?? request);
70+
}
71+
}

0 commit comments

Comments
 (0)