feat: add digest authentication

This commit is contained in:
Udhay-Adithya
2025-07-03 19:24:56 +05:30
parent 6c5862cd85
commit 92af4fba77
18 changed files with 1026 additions and 160 deletions

View File

@@ -0,0 +1,139 @@
import 'package:apidash/screens/common_widgets/auth_textfield.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:apidash_design_system/widgets/widgets.dart';
import 'package:flutter/material.dart';
class DigestAuthFields extends StatefulWidget {
final AuthModel? authData;
final bool readOnly;
final Function(AuthModel?) updateAuth;
const DigestAuthFields({
super.key,
required this.authData,
required this.updateAuth,
this.readOnly = false,
});
@override
State<DigestAuthFields> createState() => _DigestAuthFieldsState();
}
class _DigestAuthFieldsState extends State<DigestAuthFields> {
late TextEditingController _usernameController;
late TextEditingController _passwordController;
late TextEditingController _realmController;
late TextEditingController _nonceController;
late String _algorithmController;
late TextEditingController _qopController;
late TextEditingController _opaqueController;
@override
void initState() {
super.initState();
final digest = widget.authData?.digest;
_usernameController = TextEditingController(text: digest?.username ?? '');
_passwordController = TextEditingController(text: digest?.password ?? '');
_realmController = TextEditingController(text: digest?.realm ?? '');
_nonceController = TextEditingController(text: digest?.nonce ?? '');
_algorithmController = digest?.algorithm ?? 'MD5';
_qopController = TextEditingController(text: digest?.qop ?? 'auth');
_opaqueController = TextEditingController(text: digest?.opaque ?? '');
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AuthTextField(
readOnly: widget.readOnly,
controller: _usernameController,
hintText: "Username",
onChanged: (_) => _updateDigestAuth(),
),
const SizedBox(height: 16),
AuthTextField(
readOnly: widget.readOnly,
controller: _passwordController,
hintText: "Password",
isObscureText: true,
onChanged: (_) => _updateDigestAuth(),
),
const SizedBox(height: 16),
AuthTextField(
readOnly: widget.readOnly,
controller: _realmController,
hintText: "Realm",
onChanged: (_) => _updateDigestAuth(),
),
const SizedBox(height: 16),
AuthTextField(
readOnly: widget.readOnly,
controller: _nonceController,
hintText: "Nonce",
onChanged: (_) => _updateDigestAuth(),
),
const SizedBox(height: 16),
Text(
"Algorithm",
style: TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
SizedBox(height: 4),
ADPopupMenu<String>(
value: _algorithmController.trim(),
values: const [
('MD5', 'MD5'),
('MD5-sess', 'MD5-sess'),
('SHA-256', 'SHA-256'),
('SHA-256-sess', 'SHA-256-sess'),
],
tooltip: "this algorithm will be used to produce the digest",
isOutlined: true,
onChanged: (String? newLocation) {
if (newLocation != null) {
setState(() {
_algorithmController = newLocation;
});
_updateDigestAuth();
}
},
),
const SizedBox(height: 16),
AuthTextField(
readOnly: widget.readOnly,
controller: _qopController,
hintText: "QOP (e.g. auth)",
onChanged: (_) => _updateDigestAuth(),
),
const SizedBox(height: 16),
AuthTextField(
readOnly: widget.readOnly,
controller: _opaqueController,
hintText: "Opaque",
onChanged: (_) => _updateDigestAuth(),
),
],
);
}
void _updateDigestAuth() {
widget.updateAuth(
widget.authData?.copyWith(
type: APIAuthType.digest,
digest: AuthDigestModel(
username: _usernameController.text.trim(),
password: _passwordController.text.trim(),
realm: _realmController.text.trim(),
nonce: _nonceController.text.trim(),
algorithm: _algorithmController.trim(),
qop: _qopController.text.trim(),
opaque: _opaqueController.text.trim(),
),
),
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:apidash/screens/common_widgets/auth/api_key_auth_fields.dart';
import 'package:apidash/screens/common_widgets/auth/basic_auth_fields.dart';
import 'package:apidash/screens/common_widgets/auth/bearer_auth_fields.dart';
import 'package:apidash/screens/common_widgets/auth/digest_auth_fields.dart';
import 'package:apidash/screens/common_widgets/auth/jwt_auth_fields.dart';
import 'package:apidash_design_system/widgets/popup_menu.dart';
import 'package:flutter/material.dart';
@@ -53,27 +54,6 @@ class EditAuthType extends ConsumerWidget {
SizedBox(
height: 8,
),
if (readOnly)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
Theme.of(context).colorScheme.outline.withOpacity(0.3),
),
),
child: Text(
currentAuthType.name.toUpperCase(),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary,
),
),
)
else
ADPopupMenu<APIAuthType>(
value: currentAuthType.name.capitalize(),
values: const [
@@ -88,11 +68,15 @@ class EditAuthType extends ConsumerWidget {
],
tooltip: "Select Authentication Type",
isOutlined: true,
onChanged: (APIAuthType? newType) {
onChanged: readOnly
? null
: (APIAuthType? newType) {
final selectedRequest =
ref.read(selectedRequestModelProvider);
if (newType != null) {
ref.read(collectionStateNotifierProvider.notifier).update(
ref
.read(collectionStateNotifierProvider.notifier)
.update(
authModel: selectedRequest
?.httpRequestModel?.authModel
?.copyWith(type: newType) ??
@@ -151,6 +135,12 @@ class EditAuthType extends ConsumerWidget {
authData: authData,
updateAuth: updateAuth,
);
case APIAuthType.digest:
return DigestAuthFields(
readOnly: readOnly,
authData: authData,
updateAuth: updateAuth,
);
case APIAuthType.none:
return Text(readOnly
? "No authentication was used for this request."

View File

@@ -48,6 +48,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:

View File

@@ -98,4 +98,5 @@ const LineSplitter kSplitter = LineSplitter();
const kCodeCharsPerLineLimit = 200;
const kHeaderContentType = "Content-Type";
const kHeaderWwwAuthenticate = 'www-authenticate';
const kMsgRequestCancelled = 'Request Cancelled';

View File

@@ -4,22 +4,21 @@ import 'auth_api_key_model.dart';
import 'auth_basic_model.dart';
import 'auth_bearer_model.dart';
import 'auth_jwt_model.dart';
import 'auth_digest_model.dart';
part 'api_auth_model.g.dart';
part 'api_auth_model.freezed.dart';
@freezed
class AuthModel with _$AuthModel {
@JsonSerializable(
explicitToJson: true,
anyMap: true,
)
@JsonSerializable(explicitToJson: true, anyMap: true)
const factory AuthModel({
required APIAuthType type,
AuthApiKeyModel? apikey,
AuthBearerModel? bearer,
AuthBasicAuthModel? basic,
AuthJwtModel? jwt,
AuthDigestModel? digest,
}) = _AuthModel;
factory AuthModel.fromJson(Map<String, dynamic> json) =>

View File

@@ -26,6 +26,7 @@ mixin _$AuthModel {
AuthBearerModel? get bearer => throw _privateConstructorUsedError;
AuthBasicAuthModel? get basic => throw _privateConstructorUsedError;
AuthJwtModel? get jwt => throw _privateConstructorUsedError;
AuthDigestModel? get digest => throw _privateConstructorUsedError;
/// Serializes this AuthModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -48,12 +49,14 @@ abstract class $AuthModelCopyWith<$Res> {
AuthBearerModel? bearer,
AuthBasicAuthModel? basic,
AuthJwtModel? jwt,
AuthDigestModel? digest,
});
$AuthApiKeyModelCopyWith<$Res>? get apikey;
$AuthBearerModelCopyWith<$Res>? get bearer;
$AuthBasicAuthModelCopyWith<$Res>? get basic;
$AuthJwtModelCopyWith<$Res>? get jwt;
$AuthDigestModelCopyWith<$Res>? get digest;
}
/// @nodoc
@@ -76,6 +79,7 @@ class _$AuthModelCopyWithImpl<$Res, $Val extends AuthModel>
Object? bearer = freezed,
Object? basic = freezed,
Object? jwt = freezed,
Object? digest = freezed,
}) {
return _then(
_value.copyWith(
@@ -99,6 +103,10 @@ class _$AuthModelCopyWithImpl<$Res, $Val extends AuthModel>
? _value.jwt
: jwt // ignore: cast_nullable_to_non_nullable
as AuthJwtModel?,
digest: freezed == digest
? _value.digest
: digest // ignore: cast_nullable_to_non_nullable
as AuthDigestModel?,
)
as $Val,
);
@@ -159,6 +167,20 @@ class _$AuthModelCopyWithImpl<$Res, $Val extends AuthModel>
return _then(_value.copyWith(jwt: value) as $Val);
});
}
/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$AuthDigestModelCopyWith<$Res>? get digest {
if (_value.digest == null) {
return null;
}
return $AuthDigestModelCopyWith<$Res>(_value.digest!, (value) {
return _then(_value.copyWith(digest: value) as $Val);
});
}
}
/// @nodoc
@@ -176,6 +198,7 @@ abstract class _$$AuthModelImplCopyWith<$Res>
AuthBearerModel? bearer,
AuthBasicAuthModel? basic,
AuthJwtModel? jwt,
AuthDigestModel? digest,
});
@override
@@ -186,6 +209,8 @@ abstract class _$$AuthModelImplCopyWith<$Res>
$AuthBasicAuthModelCopyWith<$Res>? get basic;
@override
$AuthJwtModelCopyWith<$Res>? get jwt;
@override
$AuthDigestModelCopyWith<$Res>? get digest;
}
/// @nodoc
@@ -207,6 +232,7 @@ class __$$AuthModelImplCopyWithImpl<$Res>
Object? bearer = freezed,
Object? basic = freezed,
Object? jwt = freezed,
Object? digest = freezed,
}) {
return _then(
_$AuthModelImpl(
@@ -230,6 +256,10 @@ class __$$AuthModelImplCopyWithImpl<$Res>
? _value.jwt
: jwt // ignore: cast_nullable_to_non_nullable
as AuthJwtModel?,
digest: freezed == digest
? _value.digest
: digest // ignore: cast_nullable_to_non_nullable
as AuthDigestModel?,
),
);
}
@@ -245,6 +275,7 @@ class _$AuthModelImpl implements _AuthModel {
this.bearer,
this.basic,
this.jwt,
this.digest,
});
factory _$AuthModelImpl.fromJson(Map<String, dynamic> json) =>
@@ -260,10 +291,12 @@ class _$AuthModelImpl implements _AuthModel {
final AuthBasicAuthModel? basic;
@override
final AuthJwtModel? jwt;
@override
final AuthDigestModel? digest;
@override
String toString() {
return 'AuthModel(type: $type, apikey: $apikey, bearer: $bearer, basic: $basic, jwt: $jwt)';
return 'AuthModel(type: $type, apikey: $apikey, bearer: $bearer, basic: $basic, jwt: $jwt, digest: $digest)';
}
@override
@@ -275,13 +308,14 @@ class _$AuthModelImpl implements _AuthModel {
(identical(other.apikey, apikey) || other.apikey == apikey) &&
(identical(other.bearer, bearer) || other.bearer == bearer) &&
(identical(other.basic, basic) || other.basic == basic) &&
(identical(other.jwt, jwt) || other.jwt == jwt));
(identical(other.jwt, jwt) || other.jwt == jwt) &&
(identical(other.digest, digest) || other.digest == digest));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, type, apikey, bearer, basic, jwt);
Object.hash(runtimeType, type, apikey, bearer, basic, jwt, digest);
/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.
@@ -304,6 +338,7 @@ abstract class _AuthModel implements AuthModel {
final AuthBearerModel? bearer,
final AuthBasicAuthModel? basic,
final AuthJwtModel? jwt,
final AuthDigestModel? digest,
}) = _$AuthModelImpl;
factory _AuthModel.fromJson(Map<String, dynamic> json) =
@@ -319,6 +354,8 @@ abstract class _AuthModel implements AuthModel {
AuthBasicAuthModel? get basic;
@override
AuthJwtModel? get jwt;
@override
AuthDigestModel? get digest;
/// Create a copy of AuthModel
/// with the given fields replaced by the non-null parameter values.

View File

@@ -26,6 +26,11 @@ _$AuthModelImpl _$$AuthModelImplFromJson(Map json) => _$AuthModelImpl(
jwt: json['jwt'] == null
? null
: AuthJwtModel.fromJson(Map<String, dynamic>.from(json['jwt'] as Map)),
digest: json['digest'] == null
? null
: AuthDigestModel.fromJson(
Map<String, dynamic>.from(json['digest'] as Map),
),
);
Map<String, dynamic> _$$AuthModelImplToJson(_$AuthModelImpl instance) =>
@@ -35,6 +40,7 @@ Map<String, dynamic> _$$AuthModelImplToJson(_$AuthModelImpl instance) =>
'bearer': instance.bearer?.toJson(),
'basic': instance.basic?.toJson(),
'jwt': instance.jwt?.toJson(),
'digest': instance.digest?.toJson(),
};
const _$APIAuthTypeEnumMap = {

View File

@@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'auth_digest_model.g.dart';
part 'auth_digest_model.freezed.dart';
@freezed
class AuthDigestModel with _$AuthDigestModel {
const factory AuthDigestModel({
required String username,
required String password,
required String realm,
required String nonce,
required String algorithm,
required String qop,
required String opaque,
}) = _AuthDigestModel;
factory AuthDigestModel.fromJson(Map<String, dynamic> json) =>
_$AuthDigestModelFromJson(json);
}

View File

@@ -0,0 +1,314 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'auth_digest_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
AuthDigestModel _$AuthDigestModelFromJson(Map<String, dynamic> json) {
return _AuthDigestModel.fromJson(json);
}
/// @nodoc
mixin _$AuthDigestModel {
String get username => throw _privateConstructorUsedError;
String get password => throw _privateConstructorUsedError;
String get realm => throw _privateConstructorUsedError;
String get nonce => throw _privateConstructorUsedError;
String get algorithm => throw _privateConstructorUsedError;
String get qop => throw _privateConstructorUsedError;
String get opaque => throw _privateConstructorUsedError;
/// Serializes this AuthDigestModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of AuthDigestModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AuthDigestModelCopyWith<AuthDigestModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AuthDigestModelCopyWith<$Res> {
factory $AuthDigestModelCopyWith(
AuthDigestModel value,
$Res Function(AuthDigestModel) then,
) = _$AuthDigestModelCopyWithImpl<$Res, AuthDigestModel>;
@useResult
$Res call({
String username,
String password,
String realm,
String nonce,
String algorithm,
String qop,
String opaque,
});
}
/// @nodoc
class _$AuthDigestModelCopyWithImpl<$Res, $Val extends AuthDigestModel>
implements $AuthDigestModelCopyWith<$Res> {
_$AuthDigestModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AuthDigestModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? username = null,
Object? password = null,
Object? realm = null,
Object? nonce = null,
Object? algorithm = null,
Object? qop = null,
Object? opaque = null,
}) {
return _then(
_value.copyWith(
username: null == username
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
realm: null == realm
? _value.realm
: realm // ignore: cast_nullable_to_non_nullable
as String,
nonce: null == nonce
? _value.nonce
: nonce // ignore: cast_nullable_to_non_nullable
as String,
algorithm: null == algorithm
? _value.algorithm
: algorithm // ignore: cast_nullable_to_non_nullable
as String,
qop: null == qop
? _value.qop
: qop // ignore: cast_nullable_to_non_nullable
as String,
opaque: null == opaque
? _value.opaque
: opaque // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$AuthDigestModelImplCopyWith<$Res>
implements $AuthDigestModelCopyWith<$Res> {
factory _$$AuthDigestModelImplCopyWith(
_$AuthDigestModelImpl value,
$Res Function(_$AuthDigestModelImpl) then,
) = __$$AuthDigestModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String username,
String password,
String realm,
String nonce,
String algorithm,
String qop,
String opaque,
});
}
/// @nodoc
class __$$AuthDigestModelImplCopyWithImpl<$Res>
extends _$AuthDigestModelCopyWithImpl<$Res, _$AuthDigestModelImpl>
implements _$$AuthDigestModelImplCopyWith<$Res> {
__$$AuthDigestModelImplCopyWithImpl(
_$AuthDigestModelImpl _value,
$Res Function(_$AuthDigestModelImpl) _then,
) : super(_value, _then);
/// Create a copy of AuthDigestModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? username = null,
Object? password = null,
Object? realm = null,
Object? nonce = null,
Object? algorithm = null,
Object? qop = null,
Object? opaque = null,
}) {
return _then(
_$AuthDigestModelImpl(
username: null == username
? _value.username
: username // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
realm: null == realm
? _value.realm
: realm // ignore: cast_nullable_to_non_nullable
as String,
nonce: null == nonce
? _value.nonce
: nonce // ignore: cast_nullable_to_non_nullable
as String,
algorithm: null == algorithm
? _value.algorithm
: algorithm // ignore: cast_nullable_to_non_nullable
as String,
qop: null == qop
? _value.qop
: qop // ignore: cast_nullable_to_non_nullable
as String,
opaque: null == opaque
? _value.opaque
: opaque // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$AuthDigestModelImpl implements _AuthDigestModel {
const _$AuthDigestModelImpl({
required this.username,
required this.password,
required this.realm,
required this.nonce,
required this.algorithm,
required this.qop,
required this.opaque,
});
factory _$AuthDigestModelImpl.fromJson(Map<String, dynamic> json) =>
_$$AuthDigestModelImplFromJson(json);
@override
final String username;
@override
final String password;
@override
final String realm;
@override
final String nonce;
@override
final String algorithm;
@override
final String qop;
@override
final String opaque;
@override
String toString() {
return 'AuthDigestModel(username: $username, password: $password, realm: $realm, nonce: $nonce, algorithm: $algorithm, qop: $qop, opaque: $opaque)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AuthDigestModelImpl &&
(identical(other.username, username) ||
other.username == username) &&
(identical(other.password, password) ||
other.password == password) &&
(identical(other.realm, realm) || other.realm == realm) &&
(identical(other.nonce, nonce) || other.nonce == nonce) &&
(identical(other.algorithm, algorithm) ||
other.algorithm == algorithm) &&
(identical(other.qop, qop) || other.qop == qop) &&
(identical(other.opaque, opaque) || other.opaque == opaque));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
username,
password,
realm,
nonce,
algorithm,
qop,
opaque,
);
/// Create a copy of AuthDigestModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AuthDigestModelImplCopyWith<_$AuthDigestModelImpl> get copyWith =>
__$$AuthDigestModelImplCopyWithImpl<_$AuthDigestModelImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$AuthDigestModelImplToJson(this);
}
}
abstract class _AuthDigestModel implements AuthDigestModel {
const factory _AuthDigestModel({
required final String username,
required final String password,
required final String realm,
required final String nonce,
required final String algorithm,
required final String qop,
required final String opaque,
}) = _$AuthDigestModelImpl;
factory _AuthDigestModel.fromJson(Map<String, dynamic> json) =
_$AuthDigestModelImpl.fromJson;
@override
String get username;
@override
String get password;
@override
String get realm;
@override
String get nonce;
@override
String get algorithm;
@override
String get qop;
@override
String get opaque;
/// Create a copy of AuthDigestModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AuthDigestModelImplCopyWith<_$AuthDigestModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auth_digest_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AuthDigestModelImpl _$$AuthDigestModelImplFromJson(
Map<String, dynamic> json,
) => _$AuthDigestModelImpl(
username: json['username'] as String,
password: json['password'] as String,
realm: json['realm'] as String,
nonce: json['nonce'] as String,
algorithm: json['algorithm'] as String,
qop: json['qop'] as String,
opaque: json['opaque'] as String,
);
Map<String, dynamic> _$$AuthDigestModelImplToJson(
_$AuthDigestModelImpl instance,
) => <String, dynamic>{
'username': instance.username,
'password': instance.password,
'realm': instance.realm,
'nonce': instance.nonce,
'algorithm': instance.algorithm,
'qop': instance.qop,
'opaque': instance.opaque,
};

View File

@@ -5,3 +5,4 @@ export 'auth/auth_api_key_model.dart';
export 'auth/auth_basic_model.dart';
export 'auth/auth_bearer_model.dart';
export 'auth/auth_jwt_model.dart';
export 'auth/auth_digest_model.dart';

View File

@@ -20,14 +20,22 @@ Future<(HttpResponse?, Duration?, String?)> sendHttpRequest(
HttpRequestModel requestModel, {
SupportedUriSchemes defaultUriScheme = kDefaultUriScheme,
bool noSSL = false,
bool enableAuth = true,
}) async {
if (httpClientManager.wasRequestCancelled(requestId)) {
httpClientManager.removeCancelledRequest(requestId);
}
final client = httpClientManager.createClient(requestId, noSSL: noSSL);
// Handle authentication
final authenticatedRequestModel = handleAuth(requestModel, authData);
HttpRequestModel authenticatedRequestModel = requestModel.copyWith();
try {
if (enableAuth) {
authenticatedRequestModel = await handleAuth(requestModel, authData);
}
} catch (e) {
return (null, null, e.toString());
}
(Uri?, String?) uriRec = getValidRequestUri(
authenticatedRequestModel.url,

View File

@@ -0,0 +1,237 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:convert/convert.dart';
import 'package:crypto/crypto.dart' as crypto;
import '../../models/auth/auth_digest_model.dart';
import '../../models/models.dart';
Map<String, String>? splitAuthenticateHeader(String header) {
if (!header.startsWith('Digest ')) {
return null;
}
header = header.substring(7); // remove 'Digest '
var ret = <String, String>{};
final components = header.split(',').map((token) => token.trim());
for (var component in components) {
final kv = component.split('=');
ret[kv[0]] = kv.getRange(1, kv.length).join('=').replaceAll('"', '');
}
return ret;
}
String sha256Hash(String data) {
var content = const Utf8Encoder().convert(data);
var sha256 = crypto.sha256;
var digest = sha256.convert(content).toString();
return digest;
}
String md5Hash(String data) {
var content = const Utf8Encoder().convert(data);
var md5 = crypto.md5;
var digest = md5.convert(content).toString();
return digest;
}
String _formatNonceCount(int nc) {
return nc.toRadixString(16).padLeft(8, '0');
}
String _computeHA1(
String realm,
String? algorithm,
String username,
String password,
String? nonce,
String? cnonce,
) {
if (algorithm == 'MD5') {
final token1 = '$username:$realm:$password';
return md5Hash(token1);
} else if (algorithm == 'MD5-sess') {
final token1 = '$username:$realm:$password';
final md51 = md5Hash(token1);
final token2 = '$md51:$nonce:$cnonce';
return md5Hash(token2);
} else if (algorithm == 'SHA-256') {
final token1 = '$username:$realm:$password';
return sha256Hash(token1);
} else if (algorithm == 'SHA-256-sess') {
final token1 = '$username:$realm:$password';
final sha256_1 = sha256Hash(token1);
final token2 = '$sha256_1:$nonce:$cnonce';
return sha256Hash(token2);
} else {
throw ArgumentError.value(algorithm, 'algorithm', 'Unsupported algorithm');
}
}
Map<String, String?> computeResponse(
String method,
String path,
String body,
String? algorithm,
String? qop,
String? opaque,
String realm,
String? cnonce,
String? nonce,
int nc,
String username,
String password,
) {
var ret = <String, String?>{};
algorithm ??= 'MD5';
final ha1 = _computeHA1(realm, algorithm, username, password, nonce, cnonce);
String ha2;
if (algorithm.startsWith('MD5')) {
if (qop == 'auth-int') {
final bodyHash = md5Hash(body);
final token2 = '$method:$path:$bodyHash';
ha2 = md5Hash(token2);
} else {
// qop in [null, auth]
final token2 = '$method:$path';
ha2 = md5Hash(token2);
}
} else {
if (qop == 'auth-int') {
final bodyHash = sha256Hash(body);
final token2 = '$method:$path:$bodyHash';
ha2 = sha256Hash(token2);
} else {
// qop in [null, auth]
final token2 = '$method:$path';
ha2 = sha256Hash(token2);
}
}
final nonceCount = _formatNonceCount(nc);
ret['username'] = username;
ret['realm'] = realm;
ret['nonce'] = nonce;
ret['uri'] = path;
if (qop != null) {
ret['qop'] = qop;
}
ret['nc'] = nonceCount;
ret['cnonce'] = cnonce;
if (opaque != null) {
ret['opaque'] = opaque;
}
ret['algorithm'] = algorithm;
if (algorithm.startsWith('MD5')) {
if (qop == null) {
final token3 = '$ha1:$nonce:$ha2';
ret['response'] = md5Hash(token3);
} else if (qop == 'auth' || qop == 'auth-int') {
final token3 = '$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2';
ret['response'] = md5Hash(token3);
}
} else {
if (qop == null) {
final token3 = '$ha1:$nonce:$ha2';
ret['response'] = sha256Hash(token3);
} else if (qop == 'auth' || qop == 'auth-int') {
final token3 = '$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2';
ret['response'] = sha256Hash(token3);
}
}
return ret;
}
class DigestAuth {
String username;
String password;
// must get from first response
String? _algorithm;
String? _qop;
String? _realm;
String? _nonce;
String? _opaque;
int _nc = 0; // request counter
DigestAuth(this.username, this.password);
// Constructor that takes an AuthDigestModel
DigestAuth.fromModel(AuthDigestModel model)
: username = model.username,
password = model.password,
_realm = model.realm,
_nonce = model.nonce,
_algorithm = model.algorithm,
_qop = model.qop,
_opaque = model.opaque.isNotEmpty ? model.opaque : null;
String _computeNonce() {
final rnd = math.Random.secure();
final values = List<int>.generate(16, (i) => rnd.nextInt(256));
return hex.encode(values);
}
String getAuthString(HttpRequestModel res) {
final cnonce = _computeNonce();
final url = Uri.parse(res.url);
final method = res.method.name.toUpperCase();
final body = res.body ?? '';
_nc += 1;
// if url has query parameters, append query to path
final path = url.hasQuery ? '${url.path}?${url.query}' : url.path;
// after the first request we have the nonce, so we can provide credentials
final authValues = computeResponse(
method,
path,
body,
_algorithm,
_qop,
_opaque,
_realm!,
cnonce,
_nonce,
_nc,
username,
password,
);
final authValuesString = authValues.entries
.where((e) => e.value != null)
.map(
(e) => [
e.key,
'=',
['algorithm', 'qop', 'nc'].contains(e.key) ? '' : '"',
e.value,
['algorithm', 'qop', 'nc'].contains(e.key) ? '' : '"',
].join(''),
)
.toList()
.join(', ');
final authString = 'Digest $authValuesString';
return authString;
}
// TODO: Use this function
void initFromAuthenticateHeader(String /*!*/ authInfo) {
final values = splitAuthenticateHeader(authInfo);
if (values != null) {
_algorithm = values['algorithm'] ?? _algorithm;
_qop = values['qop'] ?? _qop;
_realm = values['realm'] ?? _realm;
_nonce = values['nonce'] ?? _nonce;
_opaque = values['opaque'] ?? _opaque;
_nc = 0;
}
}
}

View File

@@ -0,0 +1,175 @@
import 'dart:convert';
import 'dart:math';
import 'package:better_networking/utils/auth/jwt_auth_utils.dart';
import 'package:better_networking/utils/auth/digest_auth_utils.dart';
import 'package:better_networking/better_networking.dart';
Future<HttpRequestModel> handleAuth(
HttpRequestModel httpRequestModel,
AuthModel? authData,
) async {
if (authData == null || authData.type == APIAuthType.none) {
return httpRequestModel;
}
List<NameValueModel> updatedHeaders = List.from(
httpRequestModel.headers ?? [],
);
List<NameValueModel> updatedParams = List.from(httpRequestModel.params ?? []);
List<bool> updatedHeaderEnabledList = List.from(
httpRequestModel.isHeaderEnabledList ?? [],
);
List<bool> updatedParamEnabledList = List.from(
httpRequestModel.isParamEnabledList ?? [],
);
switch (authData.type) {
case APIAuthType.basic:
if (authData.basic != null) {
final basicAuth = authData.basic!;
final encoded = base64Encode(
utf8.encode('${basicAuth.username}:${basicAuth.password}'),
);
updatedHeaders.add(
NameValueModel(name: 'Authorization', value: 'Basic $encoded'),
);
updatedHeaderEnabledList.add(true);
}
break;
case APIAuthType.bearer:
if (authData.bearer != null) {
final bearerAuth = authData.bearer!;
updatedHeaders.add(
NameValueModel(
name: 'Authorization',
value: 'Bearer ${bearerAuth.token}',
),
);
updatedHeaderEnabledList.add(true);
}
break;
case APIAuthType.jwt:
if (authData.jwt != null) {
final jwtAuth = authData.jwt!;
// Generate JWT token
final jwtToken = generateJWT(jwtAuth);
if (jwtAuth.addTokenTo == 'header') {
// Add to request header with prefix
final headerValue = jwtAuth.headerPrefix.isNotEmpty
? '${jwtAuth.headerPrefix} $jwtToken'
: jwtToken;
updatedHeaders.add(
NameValueModel(name: 'Authorization', value: headerValue),
);
updatedHeaderEnabledList.add(true);
} else if (jwtAuth.addTokenTo == 'query') {
// Add to query parameters(if selected)
final paramKey = jwtAuth.queryParamKey.isNotEmpty
? jwtAuth.queryParamKey
: 'token';
updatedParams.add(NameValueModel(name: paramKey, value: jwtToken));
updatedParamEnabledList.add(true);
}
}
break;
case APIAuthType.apiKey:
if (authData.apikey != null) {
final apiKeyAuth = authData.apikey!;
if (apiKeyAuth.location == 'header') {
updatedHeaders.add(
NameValueModel(name: apiKeyAuth.name, value: apiKeyAuth.key),
);
updatedHeaderEnabledList.add(true);
} else if (apiKeyAuth.location == 'query') {
updatedParams.add(
NameValueModel(name: apiKeyAuth.name, value: apiKeyAuth.key),
);
updatedParamEnabledList.add(true);
}
}
break;
case APIAuthType.none:
break;
case APIAuthType.digest:
if (authData.digest != null) {
final digestAuthModel = authData.digest!;
if (digestAuthModel.realm.isNotEmpty &&
digestAuthModel.nonce.isNotEmpty) {
final digestAuth = DigestAuth.fromModel(digestAuthModel);
final authString = digestAuth.getAuthString(httpRequestModel);
updatedHeaders.add(
NameValueModel(name: 'Authorization', value: authString),
);
updatedHeaderEnabledList.add(true);
} else {
final httpResult = await sendHttpRequest(
"digest-${Random.secure()}",
APIType.rest,
authData,
httpRequestModel,
enableAuth: false,
);
final httpResponse = httpResult.$1;
if (httpResponse == null) {
throw Exception("Initial Digest request failed: no response");
}
if (httpResponse.statusCode == 401) {
final wwwAuthHeader = httpResponse.headers[kHeaderWwwAuthenticate];
if (wwwAuthHeader == null) {
throw Exception("401 response missing www-authenticate header");
}
final authParams = splitAuthenticateHeader(wwwAuthHeader);
if (authParams == null) {
throw Exception("Invalid Digest header format");
}
final updatedDigestModel = digestAuthModel.copyWith(
realm: authParams['realm'] ?? '',
nonce: authParams['nonce'] ?? '',
algorithm: authParams['algorithm'] ?? 'MD5',
qop: authParams['qop'] ?? 'auth',
opaque: authParams['opaque'] ?? '',
);
final digestAuth = DigestAuth.fromModel(updatedDigestModel);
final authString = digestAuth.getAuthString(httpRequestModel);
updatedHeaders.add(
NameValueModel(name: 'Authorization', value: authString),
);
updatedHeaderEnabledList.add(true);
} else {
throw Exception(
"Initial Digest request failed due to unexpected status code: ${httpResponse.body}. Status Code: ${httpResponse.statusCode}",
);
}
}
}
break;
case APIAuthType.oauth1:
// TODO: Handle this case.
throw UnimplementedError();
case APIAuthType.oauth2:
// TODO: Handle this case.
throw UnimplementedError();
}
return httpRequestModel.copyWith(
headers: updatedHeaders,
params: updatedParams,
isHeaderEnabledList: updatedHeaderEnabledList,
isParamEnabledList: updatedParamEnabledList,
);
}

View File

@@ -1,101 +0,0 @@
import 'dart:convert';
import 'package:better_networking/utils/auth_utils.dart';
import 'package:better_networking/better_networking.dart';
import '../models/auth/api_auth_model.dart';
HttpRequestModel handleAuth(HttpRequestModel httpRequestModel,AuthModel? authData) {
if (authData == null || authData.type == APIAuthType.none) {
return httpRequestModel;
}
List<NameValueModel> updatedHeaders =
List.from(httpRequestModel.headers ?? []);
List<NameValueModel> updatedParams = List.from(httpRequestModel.params ?? []);
List<bool> updatedHeaderEnabledList =
List.from(httpRequestModel.isHeaderEnabledList ?? []);
List<bool> updatedParamEnabledList =
List.from(httpRequestModel.isParamEnabledList ?? []);
switch (authData.type) {
case APIAuthType.basic:
if (authData.basic != null) {
final basicAuth = authData.basic!;
final encoded = base64Encode(
utf8.encode('${basicAuth.username}:${basicAuth.password}'));
updatedHeaders.add(
NameValueModel(name: 'Authorization', value: 'Basic $encoded'));
updatedHeaderEnabledList.add(true);
}
break;
case APIAuthType.bearer:
if (authData.bearer != null) {
final bearerAuth = authData.bearer!;
updatedHeaders.add(NameValueModel(
name: 'Authorization', value: 'Bearer ${bearerAuth.token}'));
updatedHeaderEnabledList.add(true);
}
break;
case APIAuthType.jwt:
if (authData.jwt != null) {
final jwtAuth = authData.jwt!;
// Generate JWT token
final jwtToken = generateJWT(jwtAuth);
if (jwtAuth.addTokenTo == 'header') {
// Add to request header with prefix
final headerValue = jwtAuth.headerPrefix.isNotEmpty
? '${jwtAuth.headerPrefix} $jwtToken'
: jwtToken;
updatedHeaders
.add(NameValueModel(name: 'Authorization', value: headerValue));
updatedHeaderEnabledList.add(true);
} else if (jwtAuth.addTokenTo == 'query') {
// Add to query parameters(if selected)
final paramKey = jwtAuth.queryParamKey.isNotEmpty
? jwtAuth.queryParamKey
: 'token';
updatedParams.add(NameValueModel(name: paramKey, value: jwtToken));
updatedParamEnabledList.add(true);
}
}
break;
case APIAuthType.apiKey:
if (authData.apikey != null) {
final apiKeyAuth = authData.apikey!;
if (apiKeyAuth.location == 'header') {
updatedHeaders.add(
NameValueModel(name: apiKeyAuth.name, value: apiKeyAuth.key));
updatedHeaderEnabledList.add(true);
} else if (apiKeyAuth.location == 'query') {
updatedParams.add(
NameValueModel(name: apiKeyAuth.name, value: apiKeyAuth.key));
updatedParamEnabledList.add(true);
}
}
break;
case APIAuthType.none:
break;
case APIAuthType.digest:
// TODO: Handle this case.
throw UnimplementedError();
case APIAuthType.oauth1:
// TODO: Handle this case.
throw UnimplementedError();
case APIAuthType.oauth2:
// TODO: Handle this case.
throw UnimplementedError();
}
return httpRequestModel.copyWith(
headers: updatedHeaders,
params: updatedParams,
isHeaderEnabledList: updatedHeaderEnabledList,
isParamEnabledList: updatedParamEnabledList,
);
}

View File

@@ -4,4 +4,4 @@ export 'http_request_utils.dart';
export 'http_response_utils.dart';
export 'string_utils.dart' hide RandomStringGenerator;
export 'uri_utils.dart';
export 'handle_auth.dart';
export 'auth/handle_auth.dart';

View File

@@ -26,6 +26,7 @@ dependencies:
seed: ^0.0.3
xml: ^6.3.0
crypto: ^3.0.6
convert: ^3.1.2
dev_dependencies:
flutter_test: