diff --git a/lib/screens/common_widgets/auth/oauth1_fields.dart b/lib/screens/common_widgets/auth/oauth1_fields.dart index 7de37043..b8537eeb 100644 --- a/lib/screens/common_widgets/auth/oauth1_fields.dart +++ b/lib/screens/common_widgets/auth/oauth1_fields.dart @@ -1,3 +1,4 @@ +import 'package:apidash/utils/utils.dart'; import 'package:apidash/widgets/widgets.dart'; import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/widgets/widgets.dart'; @@ -188,7 +189,13 @@ class _OAuth1FieldsState extends State { ); } - void _updateOAuth1() { + void _updateOAuth1() async { + final String? credentialsFilePath = + await getApplicationSupportDirectoryFilePath( + "oauth1_credentials", "json"); + if (credentialsFilePath == null) { + return; + } widget.updateAuth?.call( widget.authData?.copyWith( type: APIAuthType.oauth1, @@ -204,6 +211,7 @@ class _OAuth1FieldsState extends State { timestamp: _timestampController.text.trim(), nonce: _nonceController.text.trim(), realm: _realmController.text.trim(), + credentialsFilePath: credentialsFilePath, ), ) ?? AuthModel( @@ -220,6 +228,7 @@ class _OAuth1FieldsState extends State { timestamp: _timestampController.text.trim(), nonce: _nonceController.text.trim(), realm: _realmController.text.trim(), + credentialsFilePath: credentialsFilePath, ), ), ); diff --git a/packages/better_networking/lib/models/auth/auth_oauth1_model.dart b/packages/better_networking/lib/models/auth/auth_oauth1_model.dart index 33d4567e..646c5897 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth1_model.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth1_model.dart @@ -9,6 +9,7 @@ class AuthOAuth1Model with _$AuthOAuth1Model { const factory AuthOAuth1Model({ required String consumerKey, required String consumerSecret, + required String credentialsFilePath, String? accessToken, String? tokenSecret, @Default("hmacSha1") String signatureMethod, diff --git a/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart index 811cc121..035fb71f 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart @@ -23,6 +23,7 @@ AuthOAuth1Model _$AuthOAuth1ModelFromJson(Map json) { mixin _$AuthOAuth1Model { String get consumerKey => throw _privateConstructorUsedError; String get consumerSecret => throw _privateConstructorUsedError; + String get credentialsFilePath => throw _privateConstructorUsedError; String? get accessToken => throw _privateConstructorUsedError; String? get tokenSecret => throw _privateConstructorUsedError; String get signatureMethod => throw _privateConstructorUsedError; @@ -55,6 +56,7 @@ abstract class $AuthOAuth1ModelCopyWith<$Res> { $Res call({ String consumerKey, String consumerSecret, + String credentialsFilePath, String? accessToken, String? tokenSecret, String signatureMethod, @@ -86,6 +88,7 @@ class _$AuthOAuth1ModelCopyWithImpl<$Res, $Val extends AuthOAuth1Model> $Res call({ Object? consumerKey = null, Object? consumerSecret = null, + Object? credentialsFilePath = null, Object? accessToken = freezed, Object? tokenSecret = freezed, Object? signatureMethod = null, @@ -108,6 +111,10 @@ class _$AuthOAuth1ModelCopyWithImpl<$Res, $Val extends AuthOAuth1Model> ? _value.consumerSecret : consumerSecret // ignore: cast_nullable_to_non_nullable as String, + credentialsFilePath: null == credentialsFilePath + ? _value.credentialsFilePath + : credentialsFilePath // ignore: cast_nullable_to_non_nullable + as String, accessToken: freezed == accessToken ? _value.accessToken : accessToken // ignore: cast_nullable_to_non_nullable @@ -170,6 +177,7 @@ abstract class _$$AuthOAuth1ModelImplCopyWith<$Res> $Res call({ String consumerKey, String consumerSecret, + String credentialsFilePath, String? accessToken, String? tokenSecret, String signatureMethod, @@ -200,6 +208,7 @@ class __$$AuthOAuth1ModelImplCopyWithImpl<$Res> $Res call({ Object? consumerKey = null, Object? consumerSecret = null, + Object? credentialsFilePath = null, Object? accessToken = freezed, Object? tokenSecret = freezed, Object? signatureMethod = null, @@ -222,6 +231,10 @@ class __$$AuthOAuth1ModelImplCopyWithImpl<$Res> ? _value.consumerSecret : consumerSecret // ignore: cast_nullable_to_non_nullable as String, + credentialsFilePath: null == credentialsFilePath + ? _value.credentialsFilePath + : credentialsFilePath // ignore: cast_nullable_to_non_nullable + as String, accessToken: freezed == accessToken ? _value.accessToken : accessToken // ignore: cast_nullable_to_non_nullable @@ -277,6 +290,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model { const _$AuthOAuth1ModelImpl({ required this.consumerKey, required this.consumerSecret, + required this.credentialsFilePath, this.accessToken, this.tokenSecret, this.signatureMethod = "hmacSha1", @@ -298,6 +312,8 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model { @override final String consumerSecret; @override + final String credentialsFilePath; + @override final String? accessToken; @override final String? tokenSecret; @@ -326,7 +342,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model { @override String toString() { - return 'AuthOAuth1Model(consumerKey: $consumerKey, consumerSecret: $consumerSecret, accessToken: $accessToken, tokenSecret: $tokenSecret, signatureMethod: $signatureMethod, parameterLocation: $parameterLocation, version: $version, realm: $realm, callbackUrl: $callbackUrl, verifier: $verifier, nonce: $nonce, timestamp: $timestamp, includeBodyHash: $includeBodyHash)'; + return 'AuthOAuth1Model(consumerKey: $consumerKey, consumerSecret: $consumerSecret, credentialsFilePath: $credentialsFilePath, accessToken: $accessToken, tokenSecret: $tokenSecret, signatureMethod: $signatureMethod, parameterLocation: $parameterLocation, version: $version, realm: $realm, callbackUrl: $callbackUrl, verifier: $verifier, nonce: $nonce, timestamp: $timestamp, includeBodyHash: $includeBodyHash)'; } @override @@ -338,6 +354,8 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model { other.consumerKey == consumerKey) && (identical(other.consumerSecret, consumerSecret) || other.consumerSecret == consumerSecret) && + (identical(other.credentialsFilePath, credentialsFilePath) || + other.credentialsFilePath == credentialsFilePath) && (identical(other.accessToken, accessToken) || other.accessToken == accessToken) && (identical(other.tokenSecret, tokenSecret) || @@ -365,6 +383,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model { runtimeType, consumerKey, consumerSecret, + credentialsFilePath, accessToken, tokenSecret, signatureMethod, @@ -399,6 +418,7 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model { const factory _AuthOAuth1Model({ required final String consumerKey, required final String consumerSecret, + required final String credentialsFilePath, final String? accessToken, final String? tokenSecret, final String signatureMethod, @@ -420,6 +440,8 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model { @override String get consumerSecret; @override + String get credentialsFilePath; + @override String? get accessToken; @override String? get tokenSecret; diff --git a/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart b/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart index dd6b66bc..a5f436b1 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart @@ -11,6 +11,7 @@ _$AuthOAuth1ModelImpl _$$AuthOAuth1ModelImplFromJson( ) => _$AuthOAuth1ModelImpl( consumerKey: json['consumerKey'] as String, consumerSecret: json['consumerSecret'] as String, + credentialsFilePath: json['credentialsFilePath'] as String, accessToken: json['accessToken'] as String?, tokenSecret: json['tokenSecret'] as String?, signatureMethod: json['signatureMethod'] as String? ?? "hmacSha1", @@ -29,6 +30,7 @@ Map _$$AuthOAuth1ModelImplToJson( ) => { 'consumerKey': instance.consumerKey, 'consumerSecret': instance.consumerSecret, + 'credentialsFilePath': instance.credentialsFilePath, 'accessToken': instance.accessToken, 'tokenSecret': instance.tokenSecret, 'signatureMethod': instance.signatureMethod, diff --git a/packages/better_networking/lib/utils/auth/handle_auth.dart b/packages/better_networking/lib/utils/auth/handle_auth.dart index 4d8c8699..ca781fe2 100644 --- a/packages/better_networking/lib/utils/auth/handle_auth.dart +++ b/packages/better_networking/lib/utils/auth/handle_auth.dart @@ -7,6 +7,8 @@ import 'package:better_networking/better_networking.dart'; import 'package:better_networking/utils/auth/oauth2_utils.dart'; import 'package:flutter/foundation.dart'; +import 'oauth1_utils.dart'; + Future handleAuth( HttpRequestModel httpRequestModel, AuthModel? authData, @@ -157,8 +159,24 @@ Future handleAuth( } break; case APIAuthType.oauth1: - // TODO: Handle this case. - throw UnimplementedError(); + if (authData.oauth1 != null) { + final oauth1Model = authData.oauth1!; + + try { + final client = generateOAuth1AuthHeader( + oauth1Model, + httpRequestModel, + ); + + updatedHeaders.add( + NameValueModel(name: 'Authorization', value: client), + ); + updatedHeaderEnabledList.add(true); + } catch (e) { + throw Exception('OAuth 1.0 authentication failed: $e'); + } + } + break; case APIAuthType.oauth2: final oauth2 = authData.oauth2; diff --git a/packages/better_networking/lib/utils/auth/oauth1_utils.dart b/packages/better_networking/lib/utils/auth/oauth1_utils.dart new file mode 100644 index 00000000..54d17719 --- /dev/null +++ b/packages/better_networking/lib/utils/auth/oauth1_utils.dart @@ -0,0 +1,121 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:math' as math; +import 'package:crypto/crypto.dart'; + +import '../../models/models.dart'; + +/// Generates a simple OAuth 1.0a Authorization header directly from model +String generateOAuth1AuthHeader( + AuthOAuth1Model oauth1Model, + HttpRequestModel request, +) { + // Generate OAuth parameters + final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(); + final nonce = _generateNonce(); + + // Build OAuth parameters map + final oauthParams = { + 'oauth_consumer_key': oauth1Model.consumerKey, + 'oauth_signature_method': "HMAC-SHA1", + 'oauth_timestamp': timestamp, + 'oauth_nonce': nonce, + 'oauth_version': oauth1Model.version, + }; + + // Add oauth_token if available + if (oauth1Model.accessToken != null && oauth1Model.accessToken!.isNotEmpty) { + oauthParams['oauth_token'] = oauth1Model.accessToken!; + } + + // Create signature base string and signing key + final method = request.method.name.toUpperCase(); + final uri = Uri.parse(request.url); + final baseString = _createSignatureBaseString(method, uri, oauthParams); + final signingKey = + '${Uri.encodeComponent(oauth1Model.consumerSecret)}&' + '${Uri.encodeComponent(oauth1Model.tokenSecret ?? '')}'; + + // Generate signature using HMAC-SHA1 + final signature = _generateHmacSha1Signature(baseString, signingKey); + oauthParams['oauth_signature'] = signature; + + // Build Authorization header + final authParts = oauthParams.entries + .map((e) => '${e.key}="${Uri.encodeComponent(e.value)}"') + .join(','); + + return 'OAuth $authParts'; +} + +/// Helper function to clear saved OAuth 1.0 credentials +Future clearOAuth1Credentials(File credentialsFile) async { + if (await credentialsFile.exists()) { + try { + await credentialsFile.delete(); + log('Cleared OAuth 1.0 credentials'); + } catch (e) { + log('Error clearing OAuth 1.0 credentials: $e'); + } + } +} + +/// Generates a random nonce for OAuth 1.0a +String _generateNonce() { + const chars = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = math.Random.secure(); + return String.fromCharCodes( + Iterable.generate( + 16, + (_) => chars.codeUnitAt(random.nextInt(chars.length)), + ), + ); +} + +/// Creates the signature base string for OAuth 1.0a +String _createSignatureBaseString( + String method, + Uri uri, + Map parameters, +) { + // Combine OAuth parameters with query parameters + final allParameters = {...parameters}; + + // Add query parameters from the URI + uri.queryParameters.forEach((key, value) { + allParameters[key] = value; + }); + + // Sort parameters by key + final sortedKeys = allParameters.keys.toList()..sort(); + + // Create parameter string + final paramString = sortedKeys + .map( + (key) => + '${Uri.encodeComponent(key)}=${Uri.encodeComponent(allParameters[key]!)}', + ) + .join('&'); + + // Create base URI (without query parameters) + final baseUri = uri.replace(queryParameters: {}).toString(); + + // Create signature base string + return '${method.toUpperCase()}&' + '${Uri.encodeComponent(baseUri)}&' + '${Uri.encodeComponent(paramString)}'; +} + +/// Generates HMAC-SHA1 signature for OAuth 1.0a +String _generateHmacSha1Signature(String baseString, String key) { + final keyBytes = utf8.encode(key); + final messageBytes = utf8.encode(baseString); + + final hmac = Hmac(sha1, keyBytes); + final digest = hmac.convert(messageBytes); + + return base64Encode(digest.bytes); +} diff --git a/pubspec.lock b/pubspec.lock index 1910e0f1..660db639 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -55,38 +55,6 @@ packages: relative: true source: path version: "0.0.1" - app_links: - dependency: transitive - description: - name: app_links - sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" - url: "https://pub.dev" - source: hosted - version: "6.4.0" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" archive: dependency: transitive description: @@ -782,14 +750,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - gtk: - dependency: transitive - description: - name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" - source: hosted - version: "2.1.0" har: dependency: transitive description: @@ -1198,6 +1158,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + oauth1: + dependency: transitive + description: + name: oauth1 + sha256: "1d424e3c24017a6c5714acb12e0dd76c2fdff96db6d6ef0aab58c925ffc28ae0" + url: "https://pub.dev" + source: hosted + version: "2.1.0" oauth2: dependency: transitive description: @@ -2034,38 +2002,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - webview_flutter: - dependency: transitive - description: - name: webview_flutter - sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba - url: "https://pub.dev" - source: hosted - version: "4.13.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: "9573ad97890d199ac3ab32399aa33a5412163b37feb573eb5b0a76b35e9ffe41" - url: "https://pub.dev" - source: hosted - version: "4.8.2" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "71523b9048cf510cfa1fd4e0a3fa5e476a66e0884d5df51d59d5023dba237107" - url: "https://pub.dev" - source: hosted - version: "3.22.1" win32: dependency: transitive description: