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_core/apidash_core.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.authData?.copyWith(
type: APIAuthType.oauth1,
@@ -204,6 +211,7 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
timestamp: _timestampController.text.trim(),
nonce: _nonceController.text.trim(),
realm: _realmController.text.trim(),
credentialsFilePath: credentialsFilePath,
),
) ??
AuthModel(
@@ -220,6 +228,7 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
timestamp: _timestampController.text.trim(),
nonce: _nonceController.text.trim(),
realm: _realmController.text.trim(),
credentialsFilePath: credentialsFilePath,
),
),
);

View File

@@ -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,

View File

@@ -23,6 +23,7 @@ AuthOAuth1Model _$AuthOAuth1ModelFromJson(Map<String, dynamic> 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;

View File

@@ -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<String, dynamic> _$$AuthOAuth1ModelImplToJson(
) => <String, dynamic>{
'consumerKey': instance.consumerKey,
'consumerSecret': instance.consumerSecret,
'credentialsFilePath': instance.credentialsFilePath,
'accessToken': instance.accessToken,
'tokenSecret': instance.tokenSecret,
'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:flutter/foundation.dart';
import 'oauth1_utils.dart';
Future<HttpRequestModel> handleAuth(
HttpRequestModel httpRequestModel,
AuthModel? authData,
@@ -157,8 +159,24 @@ Future<HttpRequestModel> 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;

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
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: