fix: auth header generation

This commit is contained in:
Udhay-Adithya
2025-07-23 00:59:10 +05:30
parent bc779882f7
commit a29f594f00
6 changed files with 124 additions and 69 deletions

View File

@@ -32,7 +32,7 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
late TextEditingController _timestampController; late TextEditingController _timestampController;
late TextEditingController _realmController; late TextEditingController _realmController;
late TextEditingController _nonceController; late TextEditingController _nonceController;
late String _signatureMethodController; late OAuth1SignatureMethod _signatureMethodController;
late String _addAuthDataTo; late String _addAuthDataTo;
@override @override
@@ -53,7 +53,8 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
_timestampController = TextEditingController(text: oauth1?.timestamp ?? ''); _timestampController = TextEditingController(text: oauth1?.timestamp ?? '');
_realmController = TextEditingController(text: oauth1?.realm ?? ''); _realmController = TextEditingController(text: oauth1?.realm ?? '');
_nonceController = TextEditingController(text: oauth1?.nonce ?? ''); _nonceController = TextEditingController(text: oauth1?.nonce ?? '');
_signatureMethodController = oauth1?.signatureMethod ?? 'hmacsha1'; _signatureMethodController =
oauth1?.signatureMethod ?? OAuth1SignatureMethod.hmacSha1;
_addAuthDataTo = oauth1?.parameterLocation ?? 'url'; _addAuthDataTo = oauth1?.parameterLocation ?? 'url';
} }
@@ -115,17 +116,12 @@ class _OAuth1FieldsState extends State<OAuth1Fields> {
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
ADPopupMenu<String>( ADPopupMenu<OAuth1SignatureMethod>(
value: _signatureMethodController.trim(), value: _signatureMethodController.displayType,
values: const [ values: OAuth1SignatureMethod.values.map((e) => (e, e.displayType)),
('HMAC-SHA1', 'hmacsha1'),
('HMAC-SHA256', 'hmacsha256'),
('HMAC-SHA512', 'hmacsha512'),
('Plaintext', 'plaintext'),
],
tooltip: "this algorithm will be used to produce the digest", tooltip: "this algorithm will be used to produce the digest",
isOutlined: true, isOutlined: true,
onChanged: (String? newAlgo) { onChanged: (OAuth1SignatureMethod? newAlgo) {
if (newAlgo != null) { if (newAlgo != null) {
setState(() { setState(() {
_signatureMethodController = newAlgo; _signatureMethodController = newAlgo;

View File

@@ -52,6 +52,14 @@ enum OAuth2GrantType {
final String displayType; final String displayType;
} }
enum OAuth1SignatureMethod {
hmacSha1("HMAC-SHA1"),
plaintext("Plaintext");
const OAuth1SignatureMethod(this.displayType);
final String displayType;
}
enum HTTPVerb { enum HTTPVerb {
get("GET"), get("GET"),
head("HEAD"), head("HEAD"),

View File

@@ -1,3 +1,4 @@
import 'package:better_networking/consts.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'auth_oauth1_model.g.dart'; part 'auth_oauth1_model.g.dart';
@@ -12,7 +13,7 @@ class AuthOAuth1Model with _$AuthOAuth1Model {
required String credentialsFilePath, required String credentialsFilePath,
String? accessToken, String? accessToken,
String? tokenSecret, String? tokenSecret,
@Default("hmacSha1") String signatureMethod, @Default(OAuth1SignatureMethod.hmacSha1) OAuth1SignatureMethod signatureMethod,
@Default("header") String parameterLocation, @Default("header") String parameterLocation,
@Default('1.0') String version, @Default('1.0') String version,
String? realm, String? realm,

View File

@@ -26,7 +26,8 @@ mixin _$AuthOAuth1Model {
String get credentialsFilePath => 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; OAuth1SignatureMethod get signatureMethod =>
throw _privateConstructorUsedError;
String get parameterLocation => throw _privateConstructorUsedError; String get parameterLocation => throw _privateConstructorUsedError;
String get version => throw _privateConstructorUsedError; String get version => throw _privateConstructorUsedError;
String? get realm => throw _privateConstructorUsedError; String? get realm => throw _privateConstructorUsedError;
@@ -59,7 +60,7 @@ abstract class $AuthOAuth1ModelCopyWith<$Res> {
String credentialsFilePath, String credentialsFilePath,
String? accessToken, String? accessToken,
String? tokenSecret, String? tokenSecret,
String signatureMethod, OAuth1SignatureMethod signatureMethod,
String parameterLocation, String parameterLocation,
String version, String version,
String? realm, String? realm,
@@ -126,7 +127,7 @@ class _$AuthOAuth1ModelCopyWithImpl<$Res, $Val extends AuthOAuth1Model>
signatureMethod: null == signatureMethod signatureMethod: null == signatureMethod
? _value.signatureMethod ? _value.signatureMethod
: signatureMethod // ignore: cast_nullable_to_non_nullable : signatureMethod // ignore: cast_nullable_to_non_nullable
as String, as OAuth1SignatureMethod,
parameterLocation: null == parameterLocation parameterLocation: null == parameterLocation
? _value.parameterLocation ? _value.parameterLocation
: parameterLocation // ignore: cast_nullable_to_non_nullable : parameterLocation // ignore: cast_nullable_to_non_nullable
@@ -180,7 +181,7 @@ abstract class _$$AuthOAuth1ModelImplCopyWith<$Res>
String credentialsFilePath, String credentialsFilePath,
String? accessToken, String? accessToken,
String? tokenSecret, String? tokenSecret,
String signatureMethod, OAuth1SignatureMethod signatureMethod,
String parameterLocation, String parameterLocation,
String version, String version,
String? realm, String? realm,
@@ -246,7 +247,7 @@ class __$$AuthOAuth1ModelImplCopyWithImpl<$Res>
signatureMethod: null == signatureMethod signatureMethod: null == signatureMethod
? _value.signatureMethod ? _value.signatureMethod
: signatureMethod // ignore: cast_nullable_to_non_nullable : signatureMethod // ignore: cast_nullable_to_non_nullable
as String, as OAuth1SignatureMethod,
parameterLocation: null == parameterLocation parameterLocation: null == parameterLocation
? _value.parameterLocation ? _value.parameterLocation
: parameterLocation // ignore: cast_nullable_to_non_nullable : parameterLocation // ignore: cast_nullable_to_non_nullable
@@ -293,7 +294,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
required this.credentialsFilePath, required this.credentialsFilePath,
this.accessToken, this.accessToken,
this.tokenSecret, this.tokenSecret,
this.signatureMethod = "hmacSha1", this.signatureMethod = OAuth1SignatureMethod.hmacSha1,
this.parameterLocation = "header", this.parameterLocation = "header",
this.version = '1.0', this.version = '1.0',
this.realm, this.realm,
@@ -319,7 +320,7 @@ class _$AuthOAuth1ModelImpl implements _AuthOAuth1Model {
final String? tokenSecret; final String? tokenSecret;
@override @override
@JsonKey() @JsonKey()
final String signatureMethod; final OAuth1SignatureMethod signatureMethod;
@override @override
@JsonKey() @JsonKey()
final String parameterLocation; final String parameterLocation;
@@ -421,7 +422,7 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model {
required final String credentialsFilePath, required final String credentialsFilePath,
final String? accessToken, final String? accessToken,
final String? tokenSecret, final String? tokenSecret,
final String signatureMethod, final OAuth1SignatureMethod signatureMethod,
final String parameterLocation, final String parameterLocation,
final String version, final String version,
final String? realm, final String? realm,
@@ -446,7 +447,7 @@ abstract class _AuthOAuth1Model implements AuthOAuth1Model {
@override @override
String? get tokenSecret; String? get tokenSecret;
@override @override
String get signatureMethod; OAuth1SignatureMethod get signatureMethod;
@override @override
String get parameterLocation; String get parameterLocation;
@override @override

View File

@@ -14,7 +14,12 @@ _$AuthOAuth1ModelImpl _$$AuthOAuth1ModelImplFromJson(
credentialsFilePath: json['credentialsFilePath'] 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:
$enumDecodeNullable(
_$OAuth1SignatureMethodEnumMap,
json['signatureMethod'],
) ??
OAuth1SignatureMethod.hmacSha1,
parameterLocation: json['parameterLocation'] as String? ?? "header", parameterLocation: json['parameterLocation'] as String? ?? "header",
version: json['version'] as String? ?? '1.0', version: json['version'] as String? ?? '1.0',
realm: json['realm'] as String?, realm: json['realm'] as String?,
@@ -33,7 +38,7 @@ Map<String, dynamic> _$$AuthOAuth1ModelImplToJson(
'credentialsFilePath': instance.credentialsFilePath, 'credentialsFilePath': instance.credentialsFilePath,
'accessToken': instance.accessToken, 'accessToken': instance.accessToken,
'tokenSecret': instance.tokenSecret, 'tokenSecret': instance.tokenSecret,
'signatureMethod': instance.signatureMethod, 'signatureMethod': _$OAuth1SignatureMethodEnumMap[instance.signatureMethod]!,
'parameterLocation': instance.parameterLocation, 'parameterLocation': instance.parameterLocation,
'version': instance.version, 'version': instance.version,
'realm': instance.realm, 'realm': instance.realm,
@@ -43,3 +48,8 @@ Map<String, dynamic> _$$AuthOAuth1ModelImplToJson(
'timestamp': instance.timestamp, 'timestamp': instance.timestamp,
'includeBodyHash': instance.includeBodyHash, 'includeBodyHash': instance.includeBodyHash,
}; };
const _$OAuth1SignatureMethodEnumMap = {
OAuth1SignatureMethod.hmacSha1: 'hmacSha1',
OAuth1SignatureMethod.plaintext: 'plaintext',
};

View File

@@ -1,13 +1,20 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import '../../models/models.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( String generateOAuth1AuthHeader(
AuthOAuth1Model oauth1Model, AuthOAuth1Model oauth1Model,
HttpRequestModel request, HttpRequestModel request,
@@ -19,7 +26,7 @@ String generateOAuth1AuthHeader(
// Build OAuth parameters map // Build OAuth parameters map
final oauthParams = <String, String>{ final oauthParams = <String, String>{
'oauth_consumer_key': oauth1Model.consumerKey, 'oauth_consumer_key': oauth1Model.consumerKey,
'oauth_signature_method': "HMAC-SHA1", 'oauth_signature_method': oauth1Model.signatureMethod.displayType,
'oauth_timestamp': timestamp, 'oauth_timestamp': timestamp,
'oauth_nonce': nonce, 'oauth_nonce': nonce,
'oauth_version': oauth1Model.version, 'oauth_version': oauth1Model.version,
@@ -34,12 +41,13 @@ String generateOAuth1AuthHeader(
final method = request.method.name.toUpperCase(); final method = request.method.name.toUpperCase();
final uri = Uri.parse(request.url); final uri = Uri.parse(request.url);
final baseString = _createSignatureBaseString(method, uri, oauthParams); final baseString = _createSignatureBaseString(method, uri, oauthParams);
final signingKey =
'${Uri.encodeComponent(oauth1Model.consumerSecret)}&'
'${Uri.encodeComponent(oauth1Model.tokenSecret ?? '')}';
// Generate signature using HMAC-SHA1 // Generate signature based on signature method
final signature = _generateHmacSha1Signature(baseString, signingKey); final signature = _generateSignature(
oauth1Model.signatureMethod,
baseString,
oauth1Model,
);
oauthParams['oauth_signature'] = signature; oauthParams['oauth_signature'] = signature;
// Build Authorization header // Build Authorization header
@@ -50,18 +58,6 @@ String generateOAuth1AuthHeader(
return 'OAuth $authParts'; 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 /// Generates a random nonce for OAuth 1.0a
String _generateNonce() { String _generateNonce() {
const chars = 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 /// Creates the signature base string for OAuth 1.0a
String _createSignatureBaseString( String _createSignatureBaseString(
String method, String method,
Uri uri, Uri uri,
Map<String, String> parameters, Map<String, String> parameters,
) { ) {
// Combine OAuth parameters with query parameters // 1. Percent encode every key and value that will be signed
final allParameters = <String, String>{...parameters}; final Map<String, String> encodedParams = <String, String>{};
// Add query parameters from the URI // Encode OAuth parameters
uri.queryParameters.forEach((key, value) { parameters.forEach((String k, String v) {
allParameters[key] = value; encodedParams[_encodeParam(k)] = _encodeParam(v);
}); });
// Sort parameters by key // Add and encode query parameters from the URI
final sortedKeys = allParameters.keys.toList()..sort(); uri.queryParameters.forEach((String k, String v) {
encodedParams[_encodeParam(k)] = _encodeParam(v);
});
// Create parameter string // Remove 'realm' parameter if present (not included in signature)
final paramString = sortedKeys encodedParams.remove('realm');
.map(
(key) => // 2. Sort the list of parameters alphabetically by encoded key
'${Uri.encodeComponent(key)}=${Uri.encodeComponent(allParameters[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('&'); .join('&');
// Create base URI (without query parameters) // Create base URI (origin + path)
final baseUri = uri.replace(queryParameters: {}).toString(); final baseUri = uri.origin + uri.path;
// Create signature base string // Create signature base string
return '${method.toUpperCase()}&' return '${method.toUpperCase()}&'
'${Uri.encodeComponent(baseUri)}&' '${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 /// Generates HMAC-SHA1 signature for OAuth 1.0a
String _generateHmacSha1Signature(String baseString, String key) { String _generateHmacSha1Signature(String text, String key) {
final keyBytes = utf8.encode(key); final hmac = Hmac(sha1, utf8.encode(key));
final messageBytes = utf8.encode(baseString); final digest = hmac.convert(utf8.encode(text)).bytes;
final hmac = Hmac(sha1, keyBytes); return base64Encode(digest);
final digest = hmac.convert(messageBytes);
return base64Encode(digest.bytes);
} }