feat: implement oauth1

This commit is contained in:
Udhay-Adithya
2025-07-22 23:09:15 +05:30
parent 85030c72a6
commit bc779882f7
7 changed files with 185 additions and 76 deletions

View File

@@ -1,3 +1,4 @@
import 'package:apidash/utils/utils.dart';
import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/widgets/widgets.dart';
import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/widgets/widgets.dart'; import 'package:apidash_design_system/widgets/widgets.dart';
@@ -188,7 +189,13 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
); );
} }
void _updateOAuth1() { void _updateOAuth1() async {
final String? credentialsFilePath =
await getApplicationSupportDirectoryFilePath(
"oauth1_credentials", "json");
if (credentialsFilePath == null) {
return;
}
widget.updateAuth?.call( widget.updateAuth?.call(
widget.authData?.copyWith( widget.authData?.copyWith(
type: APIAuthType.oauth1, type: APIAuthType.oauth1,
@@ -204,6 +211,7 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
timestamp: _timestampController.text.trim(), timestamp: _timestampController.text.trim(),
nonce: _nonceController.text.trim(), nonce: _nonceController.text.trim(),
realm: _realmController.text.trim(), realm: _realmController.text.trim(),
credentialsFilePath: credentialsFilePath,
), ),
) ?? ) ??
AuthModel( AuthModel(
@@ -220,6 +228,7 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
timestamp: _timestampController.text.trim(), timestamp: _timestampController.text.trim(),
nonce: _nonceController.text.trim(), nonce: _nonceController.text.trim(),
realm: _realmController.text.trim(), realm: _realmController.text.trim(),
credentialsFilePath: credentialsFilePath,
), ),
), ),
); );

View File

@@ -9,6 +9,7 @@ class AuthOAuth1Model with _$AuthOAuth1Model {
const factory AuthOAuth1Model({ const factory AuthOAuth1Model({
required String consumerKey, required String consumerKey,
required String consumerSecret, required String consumerSecret,
required String credentialsFilePath,
String? accessToken, String? accessToken,
String? tokenSecret, String? tokenSecret,
@Default("hmacSha1") String signatureMethod, @Default("hmacSha1") String signatureMethod,

View File

@@ -23,6 +23,7 @@ AuthOAuth1Model _$AuthOAuth1ModelFromJson(Map<String, dynamic> json) {
mixin _$AuthOAuth1Model { mixin _$AuthOAuth1Model {
String get consumerKey => throw _privateConstructorUsedError; String get consumerKey => throw _privateConstructorUsedError;
String get consumerSecret => throw _privateConstructorUsedError; String get consumerSecret => throw _privateConstructorUsedError;
String get credentialsFilePath => throw _privateConstructorUsedError;
String? get accessToken => throw _privateConstructorUsedError; String? get accessToken => throw _privateConstructorUsedError;
String? get tokenSecret => throw _privateConstructorUsedError; String? get tokenSecret => throw _privateConstructorUsedError;
String get signatureMethod => throw _privateConstructorUsedError; String get signatureMethod => throw _privateConstructorUsedError;
@@ -55,6 +56,7 @@ abstract class $AuthOAuth1ModelCopyWith<$Res> {
$Res call({ $Res call({
String consumerKey, String consumerKey,
String consumerSecret, String consumerSecret,
String credentialsFilePath,
String? accessToken, String? accessToken,
String? tokenSecret, String? tokenSecret,
String signatureMethod, String signatureMethod,
@@ -86,6 +88,7 @@ class _$AuthOAuth1ModelCopyWithImpl<$Res, $Val extends AuthOAuth1Model>
$Res call({ $Res call({
Object? consumerKey = null, Object? consumerKey = null,
Object? consumerSecret = null, Object? consumerSecret = null,
Object? credentialsFilePath = null,
Object? accessToken = freezed, Object? accessToken = freezed,
Object? tokenSecret = freezed, Object? tokenSecret = freezed,
Object? signatureMethod = null, Object? signatureMethod = null,
@@ -108,6 +111,10 @@ class _$AuthOAuth1ModelCopyWithImpl<$Res, $Val extends AuthOAuth1Model>
? _value.consumerSecret ? _value.consumerSecret
: consumerSecret // ignore: cast_nullable_to_non_nullable : consumerSecret // ignore: cast_nullable_to_non_nullable
as String, as String,
credentialsFilePath: null == credentialsFilePath
? _value.credentialsFilePath
: credentialsFilePath // ignore: cast_nullable_to_non_nullable
as String,
accessToken: freezed == accessToken accessToken: freezed == accessToken
? _value.accessToken ? _value.accessToken
: accessToken // ignore: cast_nullable_to_non_nullable : accessToken // ignore: cast_nullable_to_non_nullable
@@ -170,6 +177,7 @@ abstract class _$$AuthOAuth1ModelImplCopyWith<$Res>
$Res call({ $Res call({
String consumerKey, String consumerKey,
String consumerSecret, String consumerSecret,
String credentialsFilePath,
String? accessToken, String? accessToken,
String? tokenSecret, String? tokenSecret,
String signatureMethod, String signatureMethod,
@@ -200,6 +208,7 @@ class __$$AuthOAuth1ModelImplCopyWithImpl<$Res>
$Res call({ $Res call({
Object? consumerKey = null, Object? consumerKey = null,
Object? consumerSecret = null, Object? consumerSecret = null,
Object? credentialsFilePath = null,
Object? accessToken = freezed, Object? accessToken = freezed,
Object? tokenSecret = freezed, Object? tokenSecret = freezed,
Object? signatureMethod = null, Object? signatureMethod = null,
@@ -222,6 +231,10 @@ class __$$AuthOAuth1ModelImplCopyWithImpl<$Res>
? _value.consumerSecret ? _value.consumerSecret
: consumerSecret // ignore: cast_nullable_to_non_nullable : consumerSecret // ignore: cast_nullable_to_non_nullable
as String, as String,
credentialsFilePath: null == credentialsFilePath
? _value.credentialsFilePath
: credentialsFilePath // ignore: cast_nullable_to_non_nullable
as String,
accessToken: freezed == accessToken accessToken: freezed == accessToken
? _value.accessToken ? _value.accessToken
: accessToken // ignore: cast_nullable_to_non_nullable : accessToken // ignore: cast_nullable_to_non_nullable
@@ -277,6 +290,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
const _$AuthOAuth1ModelImpl({ const _$AuthOAuth1ModelImpl({
required this.consumerKey, required this.consumerKey,
required this.consumerSecret, required this.consumerSecret,
required this.credentialsFilePath,
this.accessToken, this.accessToken,
this.tokenSecret, this.tokenSecret,
this.signatureMethod = "hmacSha1", this.signatureMethod = "hmacSha1",
@@ -298,6 +312,8 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
@override @override
final String consumerSecret; final String consumerSecret;
@override @override
final String credentialsFilePath;
@override
final String? accessToken; final String? accessToken;
@override @override
final String? tokenSecret; final String? tokenSecret;
@@ -326,7 +342,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
@override @override
String toString() { 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 @override
@@ -338,6 +354,8 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
other.consumerKey == consumerKey) && other.consumerKey == consumerKey) &&
(identical(other.consumerSecret, consumerSecret) || (identical(other.consumerSecret, consumerSecret) ||
other.consumerSecret == consumerSecret) && other.consumerSecret == consumerSecret) &&
(identical(other.credentialsFilePath, credentialsFilePath) ||
other.credentialsFilePath == credentialsFilePath) &&
(identical(other.accessToken, accessToken) || (identical(other.accessToken, accessToken) ||
other.accessToken == accessToken) && other.accessToken == accessToken) &&
(identical(other.tokenSecret, tokenSecret) || (identical(other.tokenSecret, tokenSecret) ||
@@ -365,6 +383,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
runtimeType, runtimeType,
consumerKey, consumerKey,
consumerSecret, consumerSecret,
credentialsFilePath,
accessToken, accessToken,
tokenSecret, tokenSecret,
signatureMethod, signatureMethod,
@@ -399,6 +418,7 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model {
const factory _AuthOAuth1Model({ const factory _AuthOAuth1Model({
required final String consumerKey, required final String consumerKey,
required final String consumerSecret, required final String consumerSecret,
required final String credentialsFilePath,
final String? accessToken, final String? accessToken,
final String? tokenSecret, final String? tokenSecret,
final String signatureMethod, final String signatureMethod,
@@ -420,6 +440,8 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model {
@override @override
String get consumerSecret; String get consumerSecret;
@override @override
String get credentialsFilePath;
@override
String? get accessToken; String? get accessToken;
@override @override
String? get tokenSecret; String? get tokenSecret;

View File

@@ -11,6 +11,7 @@ _$AuthOAuth1ModelImpl _$$AuthOAuth1ModelImplFromJson(
) => _$AuthOAuth1ModelImpl( ) => _$AuthOAuth1ModelImpl(
consumerKey: json['consumerKey'] as String, consumerKey: json['consumerKey'] as String,
consumerSecret: json['consumerSecret'] as String, consumerSecret: json['consumerSecret'] as String,
credentialsFilePath: json['credentialsFilePath'] as String,
accessToken: json['accessToken'] as String?, accessToken: json['accessToken'] as String?,
tokenSecret: json['tokenSecret'] as String?, tokenSecret: json['tokenSecret'] as String?,
signatureMethod: json['signatureMethod'] as String? ?? "hmacSha1", signatureMethod: json['signatureMethod'] as String? ?? "hmacSha1",
@@ -29,6 +30,7 @@ Map<String, dynamic> _$$AuthOAuth1ModelImplToJson(
) => <String, dynamic>{ ) => <String, dynamic>{
'consumerKey': instance.consumerKey, 'consumerKey': instance.consumerKey,
'consumerSecret': instance.consumerSecret, 'consumerSecret': instance.consumerSecret,
'credentialsFilePath': instance.credentialsFilePath,
'accessToken': instance.accessToken, 'accessToken': instance.accessToken,
'tokenSecret': instance.tokenSecret, 'tokenSecret': instance.tokenSecret,
'signatureMethod': instance.signatureMethod, 'signatureMethod': instance.signatureMethod,

View File

@@ -7,6 +7,8 @@ import 'package:better_networking/better_networking.dart';
import 'package:better_networking/utils/auth/oauth2_utils.dart'; import 'package:better_networking/utils/auth/oauth2_utils.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'oauth1_utils.dart';
Future<HttpRequestModel> handleAuth( Future<HttpRequestModel> handleAuth(
HttpRequestModel httpRequestModel, HttpRequestModel httpRequestModel,
AuthModel? authData, AuthModel? authData,
@@ -157,8 +159,24 @@ Future<HttpRequestModel> handleAuth(
} }
break; break;
case APIAuthType.oauth1: case APIAuthType.oauth1:
// TODO: Handle this case. if (authData.oauth1 != null) {
throw UnimplementedError(); 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: case APIAuthType.oauth2:
final oauth2 = authData.oauth2; final oauth2 = authData.oauth2;

View File

@@ -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 = <String, String>{
'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<void> 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<String, String> parameters,
) {
// Combine OAuth parameters with query parameters
final allParameters = <String, String>{...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);
}

View File

@@ -55,38 +55,6 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" 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: archive:
dependency: transitive dependency: transitive
description: description:
@@ -782,14 +750,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
har: har:
dependency: transitive dependency: transitive
description: description:
@@ -1198,6 +1158,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "2.0.2"
oauth1:
dependency: transitive
description:
name: oauth1
sha256: "1d424e3c24017a6c5714acb12e0dd76c2fdff96db6d6ef0aab58c925ffc28ae0"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
oauth2: oauth2:
dependency: transitive dependency: transitive
description: description:
@@ -2034,38 +2002,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" 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: win32:
dependency: transitive dependency: transitive
description: description: