mirror of
https://github.com/foss42/apidash.git
synced 2025-12-01 10:17:47 +08:00
feat: implement oauth1
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
121
packages/better_networking/lib/utils/auth/oauth1_utils.dart
Normal file
121
packages/better_networking/lib/utils/auth/oauth1_utils.dart
Normal 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);
|
||||
}
|
||||
80
pubspec.lock
80
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:
|
||||
|
||||
Reference in New Issue
Block a user