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: dio client interceptor #5

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
2c83cc7
feat: dio client interceptor
thorvalld Mar 15, 2024
cb8cfeb
refactor: added exports
thorvalld Mar 15, 2024
c4339e7
test: WIP draft test case
thorvalld Mar 15, 2024
f0e346e
test: refactored dio interceptor test and dropped mockito
thorvalld Mar 19, 2024
e865d7a
test: documentation added
thorvalld Mar 19, 2024
1e2d511
refactor: minor comments formatting
thorvalld Mar 19, 2024
3833f87
refactor: dio interception logic
thorvalld Mar 19, 2024
e6c0142
fix: dio interception implementation
thorvalld Mar 21, 2024
4c59426
feat: added uuid generation pckg
thorvalld Mar 21, 2024
d26f23f
refactor: adjusted test scenarios
thorvalld Mar 21, 2024
49975b0
refactor: added try catch block
thorvalld Mar 21, 2024
0d5db05
refactor: dropped malforomed data test
thorvalld Mar 21, 2024
d594b4e
fix: clockskew adjusted
thorvalld Mar 21, 2024
cdce087
test: added test case for clockskew
thorvalld Mar 21, 2024
fdd1ae1
refactor: WIP interception onRequest Method
thorvalld Mar 22, 2024
66056b1
refactor: interceptor can pass a dio as arg and added injectable time…
thorvalld Mar 26, 2024
9525cd3
refactor: dio interceptor
thorvalld Mar 27, 2024
8d81410
refactor: injectable getTime()
thorvalld Apr 2, 2024
950643a
refactor: updated getTime()
thorvalld Apr 2, 2024
db49ac1
refactor: now variable using getTime() insyteam of _getTime()
thorvalld Apr 2, 2024
bc057aa
chore: Support
Soap-141 Apr 3, 2024
188a0bb
Merge pull request #6 from nventive/dev/thla/support
thorvalld Apr 4, 2024
ee107b0
refactor: adjusted test case and bug fix
thorvalld Apr 5, 2024
0c73a49
refactor: dropped unused third party libraries
thorvalld Apr 5, 2024
c97c9ae
chore: package readme
thorvalld Apr 5, 2024
eb81213
refactor: readme adjusted
thorvalld Apr 8, 2024
a0e5d4e
refactor: dropped dart_tool files
thorvalld Apr 8, 2024
9c9bad2
refactor: added assertion to test case #2
thorvalld Apr 8, 2024
5dd6fa7
refactor: test case # 2
thorvalld Apr 8, 2024
0e7ae55
refactor: added test cases
thorvalld Apr 9, 2024
0ea224c
refactor: dropped unnecessary recheck for dates
thorvalld Apr 9, 2024
9ca1fba
fix: typo
thorvalld Apr 9, 2024
63980e8
refactor: dropped unnecessary clockskew validation
thorvalld Apr 9, 2024
defa2f4
refactor: improvements and refactored for reusability
thorvalld Apr 12, 2024
f939086
refactor: minor variables refactoring
thorvalld Apr 12, 2024
64192fc
refactor: ropped use of getDateHeader in interceptor
thorvalld Apr 15, 2024
dfec5c5
refactor: refactored test cases
thorvalld Apr 16, 2024
351fd06
fix: test case when auto-retry disabled
thorvalld Apr 16, 2024
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
12 changes: 8 additions & 4 deletions lib/requests_signature_dart.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
library requests_signature_dart;

// Export exceptions
/// Export exceptions
export 'src/core/exception/requests_signature_exception.dart';

// Export core implementation
/// Export core implementation
export 'src/core/implementation/hash_algorithm_signature_body_signer.dart';
export 'src/core/implementation/signature_body_parameters.dart';
export 'src/core/implementation/signature_body_source_builder.dart';
export 'src/core/implementation/signature_body_source_components.dart';
export 'src/core/implementation/signature_body_source_parameters.dart';

// Export interfaces
/// Export interfaces
export 'src/core/interface/signature_body_signer.dart';
export 'src/core/interface/signature_body_source_builder.dart';

// Export default constants
/// Export default constants
export 'src/core/default_constants.dart';

/// Export client implementation
/// Client: Dio
export 'src/client/dio_interceptor.dart';
26 changes: 26 additions & 0 deletions lib/src/client/dio_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:dio/dio.dart';
import 'package:requests_signature_dart/src/client/dio_interceptor.dart';
import 'package:requests_signature_dart/src/client/requests_signature_options.dart';
import 'package:requests_signature_dart/src/core/interface/signature_body_signer.dart';
import 'package:requests_signature_dart/src/core/interface/signature_body_source_builder.dart';

/// Extension methods for Dio.
extension DioExtension on Dio {
/// Adds [RequestsSignatureInterceptor] as an interceptor in the Dio client.
///
/// [options] - The [RequestsSignatureOptions] to use. If not provided, will retrieve from the container.
///
/// [signatureBodySourceBuilder] - The [ISignatureBodySourceBuilder]. If not provided, will try to retrieve from the container.
///
/// [signatureBodySigner] - The [ISignatureBodySigner]. If not provided, will try to retrieve from the container.
void addRequestsSignatureInterceptor(
{RequestsSignatureOptions? options,
ISignatureBodySourceBuilder? signatureBodySourceBuilder,
ISignatureBodySigner? signatureBodySigner}) {
interceptors.add(RequestsSignatureInterceptor(
options!,
signatureBodySourceBuilder: signatureBodySourceBuilder!,
signatureBodySigner: signatureBodySigner!,
));
}
}
110 changes: 110 additions & 0 deletions lib/src/client/dio_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:dio/dio.dart';
import 'package:requests_signature_dart/requests_signature_dart.dart';

import 'dart:convert';
import 'package:requests_signature_dart/src/client/requests_signature_options.dart';

import 'dart:typed_data';

import 'package:uuid/uuid.dart';

/// Interceptor for signing outgoing requests with request signature.
///
/// This interceptor signs the outgoing requests with a request signature
/// before forwarding them to the inner Dio client for processing.
class RequestsSignatureInterceptor extends Interceptor {
Uuid _uuid = const Uuid();
final RequestsSignatureOptions _options;
final ISignatureBodySourceBuilder _signatureBodySourceBuilder;
final ISignatureBodySigner _signatureBodySigner;
int _clockSkew = 0;

/// Constructs a new [RequestsSignatureInterceptor].
///
/// The [options] parameter specifies the signature options.
///
/// Optionally, you can provide custom implementations for
/// [signatureBodySourceBuilder] and [signatureBodySigner].
RequestsSignatureInterceptor(
this._options, {
ISignatureBodySourceBuilder? signatureBodySourceBuilder,
ISignatureBodySigner? signatureBodySigner,
}) : _signatureBodySourceBuilder =
signatureBodySourceBuilder ?? SignatureBodySourceBuilder(),
_signatureBodySigner =
signatureBodySigner ?? HashAlgorithmSignatureBodySigner();

@override
Future onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
try {
// Sign the request before it is sent
await _signRequest(options);
// Proceed with the request
return handler.next(options);
} catch (e) {
throw RequestsSignatureException(e.toString());
}
}

/// Signs the outgoing request with a request signature.
Future<void> _signRequest(RequestOptions options) async {
options.headers
.remove(_options.headerName); // Remove existing signature header

final signatureBodySourceComponents =
_options.signatureBodySourceComponents.isNotEmpty
? _options.signatureBodySourceComponents
: DefaultConstants.signatureBodySourceComponents;

Uint8List? body;
// Encode request body if required by signature components
if (options.data != null &&
signatureBodySourceComponents
.contains(SignatureBodySourceComponents.body)) {
body = utf8.encode(options.data.toString());
}

// Build parameters for constructing the signature body
final signatureBodySourceParameters = SignatureBodySourceParameters(
options.method,
options.uri,
options.headers
.map((key, value) => MapEntry(key.toString(), value.toString())),
_uuid.v4(), // Generate a nonce
_getTimestamp(),
_options.clientId!,
signatureBodySourceComponents,
body: body);

// Build the signature body source based on parameters
final signatureBodySource =
await _signatureBodySourceBuilder.build(signatureBodySourceParameters);

// Construct parameters for creating the signature
final signatureBodyParameters =
SignatureBodyParameters(signatureBodySource, _options.clientSecret!);

// Generate the signature using the signer
final signatureBody =
await _signatureBodySigner.sign(signatureBodyParameters);

// Format the signature header based on the specified pattern
final signatureHeader = _options.signaturePattern
.replaceAll("{ClientId}", _options.clientId!)
.replaceAll("{Nonce}", signatureBodySourceParameters.nonce)
.replaceAll(
"{Timestamp}", signatureBodySourceParameters.timestamp.toString())
.replaceAll("{SignatureBody}", signatureBody);

// Add the signature header to the request headers
options.headers[_options.headerName] = signatureHeader;
}

// Get the current timestamp in seconds since epoch
int _getTime() {
return DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000;
thorvalld marked this conversation as resolved.
Show resolved Hide resolved
}

int _getTimestamp() => _getTime() + _clockSkew;
thorvalld marked this conversation as resolved.
Show resolved Hide resolved
}
50 changes: 50 additions & 0 deletions lib/src/client/requests_signature_options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'dart:core';

import 'package:requests_signature_dart/src/core/default_constants.dart';

/// Options for signing requests.
class RequestsSignatureOptions {
/// Client id.
String? clientId;

/// Client secret.
String? clientSecret;

/// Header name.
String headerName = DefaultConstants.headerName;

/// Header signature pattern.
String signaturePattern = DefaultConstants.signaturePattern;

/// Allowed lag of time in either direction (past/future)
/// where the request is still considered valid.
///
/// Defaults to [DefaultConstants.clockSkew] (5 minutes).
Duration clockSkew = DefaultConstants.clockSkew;

/// Indicates whether to disable auto-retries on clock skew detection.
bool disableAutoRetryOnClockSkew = false;
thorvalld marked this conversation as resolved.
Show resolved Hide resolved

/// Ordered list of signature body source components used to compute
/// the value that will be signed and create the signature body.
List<String> signatureBodySourceComponents = [];

/// Constructor for RequestsSignatureOptions.
RequestsSignatureOptions({
required this.clientId,
required this.clientSecret,
String? headerName,
String? signaturePattern,
Duration? clockSkew,
bool? disableAutoRetryOnClockSkew,
List<String>? signatureBodySourceComponents,
}) {
this.headerName = headerName ?? this.headerName;
this.signaturePattern = signaturePattern ?? this.signaturePattern;
this.clockSkew = clockSkew ?? this.clockSkew;
this.disableAutoRetryOnClockSkew =
disableAutoRetryOnClockSkew ?? this.disableAutoRetryOnClockSkew;
this.signatureBodySourceComponents =
signatureBodySourceComponents ?? this.signatureBodySourceComponents;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
import 'package:cryptography/dart.dart';
import 'package:requests_signature_dart/src/core/implementation/signature_body_parameters.dart';
import 'package:requests_signature_dart/src/core/interface/signature_body_signer.dart';

/// A class for signing message bodies using a hash algorithm.
///
Expand All @@ -19,7 +20,7 @@ import 'package:requests_signature_dart/src/core/implementation/signature_body_p
/// final signature = await signer.sign(parameters);
/// print(signature);
/// ```
class HashAlgorithmSignatureBodySigner {
class HashAlgorithmSignatureBodySigner implements ISignatureBodySigner {
/// Function that builds the hash algorithm using provided [SignatureBodyParameters].
late DartHmac _hashAlgorithm;

Expand Down
16 changes: 16 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
Expand Down Expand Up @@ -417,6 +425,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
version: "4.3.3"
vm_service:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies:
dio: ^5.4.1
http: ^1.2.0
mockito: ^5.4.4
uuid: ^4.3.3

dev_dependencies:
test: ^1.25.2
Expand Down
72 changes: 72 additions & 0 deletions test/client/dio_interceptor_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:dio/dio.dart';
import 'package:requests_signature_dart/src/client/requests_signature_options.dart';
import 'package:test/test.dart';
import 'package:requests_signature_dart/requests_signature_dart.dart';

void main() {
test('Interceptor adds signature header to GET request', () async {
// Arrange
final options = RequestsSignatureOptions(
clientId: 'test_client_id',
clientSecret: 'test_client_secret',
headerName: 'X-Signature',
signaturePattern: '{ClientId}:{Nonce}:{Timestamp}:{SignatureBody}',
);

final interceptor = RequestsSignatureInterceptor(options);

final dio = Dio();
dio.interceptors.add(interceptor);

// Act
final response = await dio.get('https://google.ca');

// Assert
expect(response.statusCode, 200);
expect(response.requestOptions.headers, contains('X-Signature'));
});

test('Interceptor adds signature header to POST request', () async {
// Arrange
final options = RequestsSignatureOptions(
clientId: 'test_client_id',
clientSecret: 'test_client_secret',
headerName: 'X-Signature',
signaturePattern: '{ClientId}:{Nonce}:{Timestamp}:{SignatureBody}',
);

final interceptor = RequestsSignatureInterceptor(options);

final dio = Dio();
dio.interceptors.add(interceptor);

// Act
final response =
await dio.post('https://example.com', data: {'key': 'value'});

// Assert
expect(response.statusCode, 200);
expect(response.requestOptions.headers, contains('X-Signature'));
});

test('Interceptor handles invalid request URL', () async {
// Arrange
final options = RequestsSignatureOptions(
clientId: 'test_client_id',
clientSecret: 'test_client_secret',
headerName: 'X-Signature',
signaturePattern: '{ClientId}:{Nonce}:{Timestamp}:{SignatureBody}',
);

final interceptor = RequestsSignatureInterceptor(options);

final dio = Dio();
dio.interceptors.add(interceptor);

// Act & Assert
expect(
() async => await dio.get('invalid_url'), // Request with invalid URL
throwsA(isA<DioException>()), // Expect DioException due to invalid URL
);
});
}