Skip to content

Commit

Permalink
feat: add refresh token handling http client
Browse files Browse the repository at this point in the history
- 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 <[email protected]>
  • Loading branch information
TheOneWithTheBraid committed Feb 13, 2025
1 parent 352b3fa commit 65e065c
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 7 deletions.
1 change: 1 addition & 0 deletions lib/matrix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export 'src/utils/matrix_file.dart';
export 'src/utils/matrix_id_string_extension.dart';
export 'src/utils/matrix_localizations.dart';
export 'src/utils/native_implementations.dart';
export 'src/utils/matrix_refresh_token_client.dart';
export 'src/utils/room_enums.dart';
export 'src/utils/room_member_change_type.dart';
export 'src/utils/push_notification.dart';
Expand Down
47 changes: 40 additions & 7 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ class Client extends MatrixApi {

ShareKeysWith shareKeysWith;

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

DateTime? get accessTokenExpiresAt => _accessTokenExpiresAt;
Expand Down Expand Up @@ -151,13 +164,19 @@ class Client extends MatrixApi {
)? customImageResizer;

/// Create a client
/// [clientName] = unique identifier of this client
///
/// [clientName]: unique identifier of this client
///
/// [databaseBuilder]: A function that creates the database instance, that will be used.
///
/// [legacyDatabaseBuilder]: Use this for your old database implementation to perform an automatic migration
///
/// [databaseDestroyer]: A function that can be used to destroy a database instance, for example by deleting files from disk.
///
/// [verificationMethods]: A set of all the verification methods this client can handle. Includes:
/// KeyVerificationMethod.numbers: Compare numbers. Most basic, should be supported
/// KeyVerificationMethod.emoji: Compare emojis
///
/// [importantStateEvents]: A set of all the important state events to load when the client connects.
/// To speed up performance only a set of state events is loaded on startup, those that are
/// needed to display a room list. All the remaining state events are automatically post-loaded
Expand All @@ -171,24 +190,43 @@ class Client extends MatrixApi {
/// - m.room.canonical_alias
/// - m.room.tombstone
/// - *some* m.room.member events, where needed
///
/// [httpClient]: The inner [Client] used to dispatch any HTTP requests
/// performed by the SDK. The [Client] you pass here will by default be
/// wrapped with a [FixedTimeoutHttpClient] with the specified
/// [defaultNetworkRequestTimeout]. In case you do not wish this wrapper,
/// you can later override the [httpClient] by using the
/// [Client.httpClient] setter.
///
/// In case your homeserver supports refresh tokens, please ensure you
/// provide a [MatrixRefreshTokenClient].
///
/// [roomPreviewLastEvents]: The event types that should be used to calculate the last event
/// in a room for the room list.
///
/// Set [requestHistoryOnLimitedTimeline] to controll the automatic behaviour if the client
/// receives a limited timeline flag for a room.
///
/// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
/// if there is no other displayname available. If not then this will return "Unknown user".
///
/// If [formatLocalpart] is true, then the localpart of an mxid will
/// be formatted in the way, that all "_" characters are becomming white spaces and
/// the first character of each word becomes uppercase.
///
/// If your client supports more login types like login with token or SSO, then add this to
/// [supportedLoginTypes]. Set a custom [syncFilter] if you like. By default the app
/// will use lazy_load_members.
///
/// Set [nativeImplementations] to [NativeImplementationsIsolate] in order to
/// enable the SDK to compute some code in background.
///
/// Set [timelineEventTimeout] to the preferred time the Client should retry
/// sending events on connection problems or to `Duration.zero` to disable it.
///
/// Set [customImageResizer] to your own implementation for a more advanced
/// and faster image resizing experience.
///
/// Set [enableDehydratedDevices] to enable experimental support for enabling MSC3814 dehydrated devices.
Client(
this.clientName, {
Expand Down Expand Up @@ -222,12 +260,6 @@ class Client extends MatrixApi {
this.shareKeysWith = ShareKeysWith.crossVerifiedIfEnabled,
this.enableDehydratedDevices = false,
this.receiptsPublicByDefault = true,

/// Implement your https://spec.matrix.org/v1.9/client-server-api/#soft-logout
/// logic here.
/// Set this to `refreshAccessToken()` for the easiest way to handle the
/// most common reason for soft logouts.
/// You can also perform a new login here by passing the existing deviceId.
this.onSoftLogout,

/// Experimental feature which allows to send a custom refresh token
Expand Down Expand Up @@ -2438,6 +2470,7 @@ class Client extends MatrixApi {
),
);
if (e.error == MatrixError.M_UNKNOWN_TOKEN) {
// due to race conditions via QUIC, still handle soft_logout here
if (e.raw.tryGet<bool>('soft_logout') == true) {
Logs().w(
'The user has been soft logged out! Calling client.onSoftLogout() if present.',
Expand Down
49 changes: 49 additions & 0 deletions lib/src/utils/matrix_refresh_token_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:http/http.dart' hide Client;

import 'package:matrix/matrix.dart';

class MatrixRefreshTokenClient extends BaseClient {
MatrixRefreshTokenClient({
required this.inner,
required this.client,
});

final Client client;
final BaseClient inner;

@override
Future<StreamedResponse> send(BaseRequest request) async {
Request? req;
if ( // only refresh if
// we are actually initialized
client.onSync.value != null &&
// the request is to the homeserver rather than e.g. IDP
request.url.host == client.homeserver?.host &&
// the request is authenticated
request.headers
.map((k, v) => MapEntry(k.toLowerCase(), v))
.containsKey('authorization') &&
// and last but not least we're logged in
client.isLogged()) {
try {
await client.ensureNotSoftLoggedOut();
} catch (e) {
Logs().w('Could not rotate token before dispatching HTTP request.', e);
}
// in every case ensure we run with the latest bearer token to avoid
// race conditions
finally {
final headers = request.headers;
// hours wasted : unknown :facepalm:
headers.removeWhere((k, _) => k.toLowerCase() == 'authorization');
headers['Authorization'] = 'Bearer ${client.bearerToken!}';
req = Request(request.method, request.url);
req.headers.addAll(headers);
if (request is Request) {
req.bodyBytes = request.bodyBytes;
}
}
}
return inner.send(req ?? request);
}
}

0 comments on commit 65e065c

Please sign in to comment.