Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add refresh token handling http client #2033

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
71 changes: 71 additions & 0 deletions lib/src/utils/matrix_refresh_token_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import 'package:http/http.dart' hide Client;

import 'package:matrix/matrix.dart';

/// a [BaseClient] implementation handling [Client.onSoftLogout]
///
/// This client wrapper takes matrix [Client] as parameter and handles token
/// rotation.
///
/// Before dispatching any request, it will check whether the [Client] supports
/// token refresh by checking [Client.accessTokenExpiresAt]. Token rotation is
/// done when :
/// - refresh is supported ([Client.accessTokenExpiresAt])
/// - we are actually initialized ([Client.onSync.value])
/// - the request is to the homeserver rather than e.g. IDP ([BaseRequest.url])
/// - the request is authenticated ([BaseRequest.headers])
/// - we're logged in ([Client.isLogged])
///
/// In this case, [Client.ensureNotSoftLoggedOut] is awaited before running
/// [BaseClient.send]. If the [Client.bearerToken] was changed meanwhile,
/// the [BaseRequest] is being adjusted.
class MatrixRefreshTokenClient extends BaseClient {
MatrixRefreshTokenClient({
required this.inner,
required this.client,
});

/// the matrix [Client] to handle token rotation for
final Client client;

/// the inner [BaseClient] to dispatch requests with
final BaseClient inner;

@override
Future<StreamedResponse> send(BaseRequest request) async {
Request? req;
if ( // only refresh if
// refresh is supported
client.accessTokenExpiresAt != null &&
// 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);
}
}
Loading