From a29f594f00201a77b5de9b256f62d4d1026a75b2 Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Wed, 23 Jul 2025 00:59:10 +0530 Subject: [PATCH] fix: auth header generation --- .../common_widgets/auth/oauth1_fields.dart | 18 +-- packages/better_networking/lib/consts.dart | 8 ++ .../lib/models/auth/auth_oauth1_model.dart | 3 +- .../auth/auth_oauth1_model.freezed.dart | 19 +-- .../lib/models/auth/auth_oauth1_model.g.dart | 14 +- .../lib/utils/auth/oauth1_utils.dart | 131 ++++++++++++------ 6 files changed, 124 insertions(+), 69 deletions(-) diff --git a/lib/screens/common_widgets/auth/oauth1_fields.dart b/lib/screens/common_widgets/auth/oauth1_fields.dart index b8537eeb..9cb83395 100644 --- a/lib/screens/common_widgets/auth/oauth1_fields.dart +++ b/lib/screens/common_widgets/auth/oauth1_fields.dart @@ -32,7 +32,7 @@ class _OAuth1FieldsState extends State { 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 { _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 { ), ), const SizedBox(height: 4), - ADPopupMenu( - value: _signatureMethodController.trim(), - values: const [ - ('HMAC-SHA1', 'hmacsha1'), - ('HMAC-SHA256', 'hmacsha256'), - ('HMAC-SHA512', 'hmacsha512'), - ('Plaintext', 'plaintext'), - ], + ADPopupMenu( + 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; diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 3752e2d7..21d7cb38 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -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"), diff --git a/packages/better_networking/lib/models/auth/auth_oauth1_model.dart b/packages/better_networking/lib/models/auth/auth_oauth1_model.dart index 646c5897..815377f2 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth1_model.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth1_model.dart @@ -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, diff --git a/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart index 035fb71f..2d3a2007 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth1_model.freezed.dart @@ -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 diff --git a/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart b/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart index a5f436b1..0d0413d4 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth1_model.g.dart @@ -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 _$$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 _$$AuthOAuth1ModelImplToJson( 'timestamp': instance.timestamp, 'includeBodyHash': instance.includeBodyHash, }; + +const _$OAuth1SignatureMethodEnumMap = { + OAuth1SignatureMethod.hmacSha1: 'hmacSha1', + OAuth1SignatureMethod.plaintext: 'plaintext', +}; diff --git a/packages/better_networking/lib/utils/auth/oauth1_utils.dart b/packages/better_networking/lib/utils/auth/oauth1_utils.dart index 54d17719..e0c7420e 100644 --- a/packages/better_networking/lib/utils/auth/oauth1_utils.dart +++ b/packages/better_networking/lib/utils/auth/oauth1_utils.dart @@ -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 = { '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 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 parameters, ) { - // Combine OAuth parameters with query parameters - final allParameters = {...parameters}; + // 1. Percent encode every key and value that will be signed + final Map encodedParams = {}; - // 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 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); }