mirror of
https://github.com/foss42/apidash.git
synced 2025-12-01 10:17:47 +08:00
fix: auth header generation
This commit is contained in:
@@ -32,7 +32,7 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
|
||||
late TextEditingController _timestampController;
|
||||
late TextEditingController _realmController;
|
||||
late TextEditingController _nonceController;
|
||||
late String _signatureMethodController;
|
||||
late OAuth1SignatureMethod _signatureMethodController;
|
||||
late String _addAuthDataTo;
|
||||
|
||||
@override
|
||||
@@ -53,7 +53,8 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
|
||||
_timestampController = TextEditingController(text: oauth1?.timestamp ?? '');
|
||||
_realmController = TextEditingController(text: oauth1?.realm ?? '');
|
||||
_nonceController = TextEditingController(text: oauth1?.nonce ?? '');
|
||||
_signatureMethodController = oauth1?.signatureMethod ?? 'hmacsha1';
|
||||
_signatureMethodController =
|
||||
oauth1?.signatureMethod ?? OAuth1SignatureMethod.hmacSha1;
|
||||
_addAuthDataTo = oauth1?.parameterLocation ?? 'url';
|
||||
}
|
||||
|
||||
@@ -115,17 +116,12 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
ADPopupMenu<String>(
|
||||
value: _signatureMethodController.trim(),
|
||||
values: const [
|
||||
('HMAC-SHA1', 'hmacsha1'),
|
||||
('HMAC-SHA256', 'hmacsha256'),
|
||||
('HMAC-SHA512', 'hmacsha512'),
|
||||
('Plaintext', 'plaintext'),
|
||||
],
|
||||
ADPopupMenu<OAuth1SignatureMethod>(
|
||||
value: _signatureMethodController.displayType,
|
||||
values: OAuth1SignatureMethod.values.map((e) => (e, e.displayType)),
|
||||
tooltip: "this algorithm will be used to produce the digest",
|
||||
isOutlined: true,
|
||||
onChanged: (String? newAlgo) {
|
||||
onChanged: (OAuth1SignatureMethod? newAlgo) {
|
||||
if (newAlgo != null) {
|
||||
setState(() {
|
||||
_signatureMethodController = newAlgo;
|
||||
|
||||
@@ -52,6 +52,14 @@ enum OAuth2GrantType {
|
||||
final String displayType;
|
||||
}
|
||||
|
||||
enum OAuth1SignatureMethod {
|
||||
hmacSha1("HMAC-SHA1"),
|
||||
plaintext("Plaintext");
|
||||
|
||||
const OAuth1SignatureMethod(this.displayType);
|
||||
final String displayType;
|
||||
}
|
||||
|
||||
enum HTTPVerb {
|
||||
get("GET"),
|
||||
head("HEAD"),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:better_networking/consts.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'auth_oauth1_model.g.dart';
|
||||
@@ -12,7 +13,7 @@ class AuthOAuth1Model with _$AuthOAuth1Model {
|
||||
required String credentialsFilePath,
|
||||
String? accessToken,
|
||||
String? tokenSecret,
|
||||
@Default("hmacSha1") String signatureMethod,
|
||||
@Default(OAuth1SignatureMethod.hmacSha1) OAuth1SignatureMethod signatureMethod,
|
||||
@Default("header") String parameterLocation,
|
||||
@Default('1.0') String version,
|
||||
String? realm,
|
||||
|
||||
@@ -26,7 +26,8 @@ mixin _$AuthOAuth1Model {
|
||||
String get credentialsFilePath => throw _privateConstructorUsedError;
|
||||
String? get accessToken => throw _privateConstructorUsedError;
|
||||
String? get tokenSecret => throw _privateConstructorUsedError;
|
||||
String get signatureMethod => throw _privateConstructorUsedError;
|
||||
OAuth1SignatureMethod get signatureMethod =>
|
||||
throw _privateConstructorUsedError;
|
||||
String get parameterLocation => throw _privateConstructorUsedError;
|
||||
String get version => throw _privateConstructorUsedError;
|
||||
String? get realm => throw _privateConstructorUsedError;
|
||||
@@ -59,7 +60,7 @@ abstract class $AuthOAuth1ModelCopyWith<$Res> {
|
||||
String credentialsFilePath,
|
||||
String? accessToken,
|
||||
String? tokenSecret,
|
||||
String signatureMethod,
|
||||
OAuth1SignatureMethod signatureMethod,
|
||||
String parameterLocation,
|
||||
String version,
|
||||
String? realm,
|
||||
@@ -126,7 +127,7 @@ class _$AuthOAuth1ModelCopyWithImpl<$Res, $Val extends AuthOAuth1Model>
|
||||
signatureMethod: null == signatureMethod
|
||||
? _value.signatureMethod
|
||||
: signatureMethod // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as OAuth1SignatureMethod,
|
||||
parameterLocation: null == parameterLocation
|
||||
? _value.parameterLocation
|
||||
: parameterLocation // ignore: cast_nullable_to_non_nullable
|
||||
@@ -180,7 +181,7 @@ abstract class _$$AuthOAuth1ModelImplCopyWith<$Res>
|
||||
String credentialsFilePath,
|
||||
String? accessToken,
|
||||
String? tokenSecret,
|
||||
String signatureMethod,
|
||||
OAuth1SignatureMethod signatureMethod,
|
||||
String parameterLocation,
|
||||
String version,
|
||||
String? realm,
|
||||
@@ -246,7 +247,7 @@ class __$$AuthOAuth1ModelImplCopyWithImpl<$Res>
|
||||
signatureMethod: null == signatureMethod
|
||||
? _value.signatureMethod
|
||||
: signatureMethod // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as OAuth1SignatureMethod,
|
||||
parameterLocation: null == parameterLocation
|
||||
? _value.parameterLocation
|
||||
: parameterLocation // ignore: cast_nullable_to_non_nullable
|
||||
@@ -293,7 +294,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
|
||||
required this.credentialsFilePath,
|
||||
this.accessToken,
|
||||
this.tokenSecret,
|
||||
this.signatureMethod = "hmacSha1",
|
||||
this.signatureMethod = OAuth1SignatureMethod.hmacSha1,
|
||||
this.parameterLocation = "header",
|
||||
this.version = '1.0',
|
||||
this.realm,
|
||||
@@ -319,7 +320,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
|
||||
final String? tokenSecret;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String signatureMethod;
|
||||
final OAuth1SignatureMethod signatureMethod;
|
||||
@override
|
||||
@JsonKey()
|
||||
final String parameterLocation;
|
||||
@@ -421,7 +422,7 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model {
|
||||
required final String credentialsFilePath,
|
||||
final String? accessToken,
|
||||
final String? tokenSecret,
|
||||
final String signatureMethod,
|
||||
final OAuth1SignatureMethod signatureMethod,
|
||||
final String parameterLocation,
|
||||
final String version,
|
||||
final String? realm,
|
||||
@@ -446,7 +447,7 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model {
|
||||
@override
|
||||
String? get tokenSecret;
|
||||
@override
|
||||
String get signatureMethod;
|
||||
OAuth1SignatureMethod get signatureMethod;
|
||||
@override
|
||||
String get parameterLocation;
|
||||
@override
|
||||
|
||||
@@ -14,7 +14,12 @@ _$AuthOAuth1ModelImpl _$$AuthOAuth1ModelImplFromJson(
|
||||
credentialsFilePath: json['credentialsFilePath'] as String,
|
||||
accessToken: json['accessToken'] as String?,
|
||||
tokenSecret: json['tokenSecret'] as String?,
|
||||
signatureMethod: json['signatureMethod'] as String? ?? "hmacSha1",
|
||||
signatureMethod:
|
||||
$enumDecodeNullable(
|
||||
_$OAuth1SignatureMethodEnumMap,
|
||||
json['signatureMethod'],
|
||||
) ??
|
||||
OAuth1SignatureMethod.hmacSha1,
|
||||
parameterLocation: json['parameterLocation'] as String? ?? "header",
|
||||
version: json['version'] as String? ?? '1.0',
|
||||
realm: json['realm'] as String?,
|
||||
@@ -33,7 +38,7 @@ Map<String, dynamic> _$$AuthOAuth1ModelImplToJson(
|
||||
'credentialsFilePath': instance.credentialsFilePath,
|
||||
'accessToken': instance.accessToken,
|
||||
'tokenSecret': instance.tokenSecret,
|
||||
'signatureMethod': instance.signatureMethod,
|
||||
'signatureMethod': _$OAuth1SignatureMethodEnumMap[instance.signatureMethod]!,
|
||||
'parameterLocation': instance.parameterLocation,
|
||||
'version': instance.version,
|
||||
'realm': instance.realm,
|
||||
@@ -43,3 +48,8 @@ Map<String, dynamic> _$$AuthOAuth1ModelImplToJson(
|
||||
'timestamp': instance.timestamp,
|
||||
'includeBodyHash': instance.includeBodyHash,
|
||||
};
|
||||
|
||||
const _$OAuth1SignatureMethodEnumMap = {
|
||||
OAuth1SignatureMethod.hmacSha1: 'hmacSha1',
|
||||
OAuth1SignatureMethod.plaintext: 'plaintext',
|
||||
};
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
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';
|
||||
import '../../consts.dart';
|
||||
|
||||
/// Generates a simple OAuth 1.0a Authorization header directly from model
|
||||
/// Generates a simple OAuth 1.0a Authorization header directly from AuthOAuth1Model model
|
||||
///
|
||||
/// This function supports two OAuth 1.0a signature methods:
|
||||
/// - HMAC-SHA1: Most commonly used, requires consumer secret and optional token secret
|
||||
/// - Plaintext: Simple concatenation, only use over HTTPS
|
||||
///
|
||||
/// The function automatically:
|
||||
/// - Generates timestamp and nonce
|
||||
/// - Creates the signature base string
|
||||
/// - Signs using the specified method
|
||||
/// - Formats the Authorization header
|
||||
String generateOAuth1AuthHeader(
|
||||
AuthOAuth1Model oauth1Model,
|
||||
HttpRequestModel request,
|
||||
@@ -19,7 +26,7 @@ String generateOAuth1AuthHeader(
|
||||
// Build OAuth parameters map
|
||||
final oauthParams = <String, String>{
|
||||
'oauth_consumer_key': oauth1Model.consumerKey,
|
||||
'oauth_signature_method': "HMAC-SHA1",
|
||||
'oauth_signature_method': oauth1Model.signatureMethod.displayType,
|
||||
'oauth_timestamp': timestamp,
|
||||
'oauth_nonce': nonce,
|
||||
'oauth_version': oauth1Model.version,
|
||||
@@ -34,12 +41,13 @@ String generateOAuth1AuthHeader(
|
||||
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);
|
||||
// Generate signature based on signature method
|
||||
final signature = _generateSignature(
|
||||
oauth1Model.signatureMethod,
|
||||
baseString,
|
||||
oauth1Model,
|
||||
);
|
||||
oauthParams['oauth_signature'] = signature;
|
||||
|
||||
// Build Authorization header
|
||||
@@ -50,18 +58,6 @@ String generateOAuth1AuthHeader(
|
||||
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 =
|
||||
@@ -75,47 +71,90 @@ String _generateNonce() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Percent-encodes the [param] following RFC 5849.
|
||||
///
|
||||
/// All characters except uppercase and lowercase letters, digits and the
|
||||
/// characters `-_.~` are percent-encoded.
|
||||
///
|
||||
/// See https://oauth.net/core/1.0a/#encoding_parameters.
|
||||
String _encodeParam(String param) {
|
||||
return Uri.encodeComponent(param)
|
||||
.replaceAll('!', '%21')
|
||||
.replaceAll('*', '%2A')
|
||||
.replaceAll("'", '%27')
|
||||
.replaceAll('(', '%28')
|
||||
.replaceAll(')', '%29');
|
||||
}
|
||||
|
||||
/// 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};
|
||||
// 1. Percent encode every key and value that will be signed
|
||||
final Map<String, String> encodedParams = <String, String>{};
|
||||
|
||||
// Add query parameters from the URI
|
||||
uri.queryParameters.forEach((key, value) {
|
||||
allParameters[key] = value;
|
||||
// Encode OAuth parameters
|
||||
parameters.forEach((String k, String v) {
|
||||
encodedParams[_encodeParam(k)] = _encodeParam(v);
|
||||
});
|
||||
|
||||
// Sort parameters by key
|
||||
final sortedKeys = allParameters.keys.toList()..sort();
|
||||
// Add and encode query parameters from the URI
|
||||
uri.queryParameters.forEach((String k, String v) {
|
||||
encodedParams[_encodeParam(k)] = _encodeParam(v);
|
||||
});
|
||||
|
||||
// Create parameter string
|
||||
final paramString = sortedKeys
|
||||
.map(
|
||||
(key) =>
|
||||
'${Uri.encodeComponent(key)}=${Uri.encodeComponent(allParameters[key]!)}',
|
||||
)
|
||||
// Remove 'realm' parameter if present (not included in signature)
|
||||
encodedParams.remove('realm');
|
||||
|
||||
// 2. Sort the list of parameters alphabetically by encoded key
|
||||
final List<String> sortedEncodedKeys = encodedParams.keys.toList()..sort();
|
||||
|
||||
// 3-7. Create parameter string
|
||||
final String baseParams = sortedEncodedKeys
|
||||
.map((String k) {
|
||||
return '$k=${encodedParams[k]}';
|
||||
})
|
||||
.join('&');
|
||||
|
||||
// Create base URI (without query parameters)
|
||||
final baseUri = uri.replace(queryParameters: {}).toString();
|
||||
// Create base URI (origin + path)
|
||||
final baseUri = uri.origin + uri.path;
|
||||
|
||||
// Create signature base string
|
||||
return '${method.toUpperCase()}&'
|
||||
'${Uri.encodeComponent(baseUri)}&'
|
||||
'${Uri.encodeComponent(paramString)}';
|
||||
'${Uri.encodeComponent(baseParams)}';
|
||||
}
|
||||
|
||||
/// Generates signature based on the specified signature method
|
||||
///
|
||||
/// Supports three OAuth 1.0a signature methods:
|
||||
/// - HMAC-SHA1: Creates HMAC signature using SHA-1 hash (recommended)
|
||||
/// - Plaintext: Returns the signing key directly (use only over HTTPS)
|
||||
String _generateSignature(
|
||||
OAuth1SignatureMethod signatureMethod,
|
||||
String baseString,
|
||||
AuthOAuth1Model oauth1Model,
|
||||
) {
|
||||
switch (signatureMethod) {
|
||||
case OAuth1SignatureMethod.hmacSha1:
|
||||
final signingKey =
|
||||
'${Uri.encodeComponent(oauth1Model.consumerSecret)}&'
|
||||
'${Uri.encodeComponent(oauth1Model.tokenSecret ?? '')}';
|
||||
return _generateHmacSha1Signature(baseString, signingKey);
|
||||
case OAuth1SignatureMethod.plaintext:
|
||||
final signingKey =
|
||||
'${Uri.encodeComponent(oauth1Model.consumerSecret)}&'
|
||||
'${Uri.encodeComponent(oauth1Model.tokenSecret ?? '')}';
|
||||
return signingKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates HMAC-SHA1 signature for OAuth 1.0a
|
||||
String _generateHmacSha1Signature(String baseString, String key) {
|
||||
final keyBytes = utf8.encode(key);
|
||||
final messageBytes = utf8.encode(baseString);
|
||||
String _generateHmacSha1Signature(String text, String key) {
|
||||
final hmac = Hmac(sha1, utf8.encode(key));
|
||||
final digest = hmac.convert(utf8.encode(text)).bytes;
|
||||
|
||||
final hmac = Hmac(sha1, keyBytes);
|
||||
final digest = hmac.convert(messageBytes);
|
||||
|
||||
return base64Encode(digest.bytes);
|
||||
return base64Encode(digest);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user