diff --git a/lib/screens/common_widgets/auth/auth_page.dart b/lib/screens/common_widgets/auth/auth_page.dart index e34b9f06..fa24da72 100644 --- a/lib/screens/common_widgets/auth/auth_page.dart +++ b/lib/screens/common_widgets/auth/auth_page.dart @@ -7,6 +7,7 @@ import 'bearer_auth_fields.dart'; import 'digest_auth_fields.dart'; import 'jwt_auth_fields.dart'; import 'consts.dart'; +import 'oauth2_field.dart'; class AuthPage extends StatelessWidget { final AuthModel? authModel; @@ -75,6 +76,11 @@ class AuthPage extends StatelessWidget { authData: authModel, updateAuth: updateAuthData, ), + APIAuthType.oauth2 => OAuth2Fields( + readOnly: readOnly, + authData: authModel, + updateAuth: updateAuthData, + ), APIAuthType.none => Text(readOnly ? kMsgNoAuth : kMsgNoAuthSelected), _ => Text(readOnly diff --git a/lib/screens/common_widgets/auth/oauth2_field.dart b/lib/screens/common_widgets/auth/oauth2_field.dart new file mode 100644 index 00000000..c0300a39 --- /dev/null +++ b/lib/screens/common_widgets/auth/oauth2_field.dart @@ -0,0 +1,485 @@ +import 'package:apidash/widgets/field_auth.dart'; +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; + +class OAuth2Fields extends StatefulWidget { + final AuthModel? authData; + + final bool readOnly; + + final Function(AuthModel?)? updateAuth; + + const OAuth2Fields({ + super.key, + required this.authData, + this.updateAuth, + this.readOnly = false, + }); + + @override + State createState() => _OAuth2FieldsState(); +} + +class _OAuth2FieldsState extends State { + late OAuth2GrantType _grantType; + + late TextEditingController _authorizationUrlController; + + late TextEditingController _accessTokenUrlController; + + late TextEditingController _clientIdController; + + late TextEditingController _clientSecretController; + + late TextEditingController _redirectUrlController; + + late TextEditingController _scopeController; + + late TextEditingController _stateController; + + late String _codeChallengeMethod; + + late TextEditingController _usernameController; + + late TextEditingController _passwordController; + + late TextEditingController _refreshTokenController; + + late TextEditingController _identityTokenController; + + late TextEditingController _accessTokenController; + +// late TextEditingController _headerPrefixController; + +// late TextEditingController _audienceController; + + @override + void initState() { + super.initState(); + + final oauth2 = widget.authData?.oauth2; + + _grantType = oauth2?.grantType ?? OAuth2GrantType.authorizationCode; + + _authorizationUrlController = + TextEditingController(text: oauth2?.authorizationUrl ?? ''); + + _accessTokenUrlController = + TextEditingController(text: oauth2?.accessTokenUrl ?? ''); + + _clientIdController = TextEditingController(text: oauth2?.clientId ?? ''); + + _clientSecretController = + TextEditingController(text: oauth2?.clientSecret ?? ''); + + _redirectUrlController = + TextEditingController(text: oauth2?.redirectUrl ?? ''); + + _scopeController = TextEditingController(text: oauth2?.scope ?? ''); + + _stateController = TextEditingController(text: oauth2?.state ?? ''); + + _usernameController = TextEditingController(text: oauth2?.username ?? ''); + + _passwordController = TextEditingController(text: oauth2?.password ?? ''); + + _refreshTokenController = + TextEditingController(text: oauth2?.refreshToken ?? ''); + + _identityTokenController = + TextEditingController(text: oauth2?.identityToken ?? ''); + + _accessTokenController = + TextEditingController(text: oauth2?.accessToken ?? ''); + +// _headerPrefixController = TextEditingController(text: oauth2?.headerPrefix ?? ''); + +// _audienceController = TextEditingController(text: oauth2?.audience ?? ''); + + _codeChallengeMethod = oauth2?.codeChallengeMethod ?? 'sha-256'; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Grant Type", + style: Theme.of(context).textTheme.labelLarge, + ), + + kVSpacer5, + + ADPopupMenu( + value: _grantType.displayType, + values: OAuth2GrantType.values.map((e) => (e, e.displayType)), + tooltip: "Select OAuth 2.0 grant type", + isOutlined: true, + onChanged: (OAuth2GrantType? newGrantType) { + if (newGrantType != null && newGrantType != _grantType) { + setState(() { + _grantType = newGrantType; + }); + + _updateOAuth2(); + } + }, + ), + + kVSpacer16, + + if (_shouldShowField(OAuth2Field.authorizationUrl)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _authorizationUrlController, + hintText: "Authorization URL", + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.username)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _usernameController, + hintText: "Username", + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.password)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _passwordController, + hintText: "Password", + isObscureText: true, + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.accessTokenUrl)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _accessTokenUrlController, + hintText: "Access Token URL", + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.clientId)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _clientIdController, + hintText: "Client ID", + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.clientSecret)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _clientSecretController, + hintText: "Client Secret", + isObscureText: true, + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.codeChallengeMethod)) ...[ + Text( + "Code Challenge Method", + style: Theme.of(context).textTheme.labelLarge, + ), + kVSpacer5, + ADPopupMenu( + value: _codeChallengeMethod.toUpperCase(), + values: const [ + ('SHA-256', 'sha-256'), + ('Plaintext', 'plaintext'), + ], + tooltip: "Code challenge method for PKCE", + isOutlined: true, + onChanged: (String? newMethod) { + if (newMethod != null && newMethod != _codeChallengeMethod) { + setState(() { + _codeChallengeMethod = newMethod; + }); + + _updateOAuth2(); + } + }, + ), + kVSpacer16, + ], + + if (_shouldShowField(OAuth2Field.redirectUrl)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _redirectUrlController, + hintText: "Redirect URL", + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField( + OAuth2Field.scope)) // Based on refined list, Scope is always shown + + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _scopeController, + hintText: "Scope", + onChanged: (_) => _updateOAuth2(), + ), + ), + + if (_shouldShowField(OAuth2Field.state)) + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _stateController, + hintText: "State", + onChanged: (_) => _updateOAuth2(), + ), + ), + +// if (_shouldShowField(OAuth2Field + +// .headerPrefix)) + +// ..._buildFieldWithSpacing( + +// AuthTextField( + +// readOnly: widget.readOnly, + +// controller: _headerPrefixController, + +// hintText: "Header Prefix", + +// onChanged: (_) => _updateOAuth2(), + +// ), + +// ), + +// if (_shouldShowField(OAuth2Field + +// .audience)) + +// ..._buildFieldWithSpacing( + +// AuthTextField( + +// readOnly: widget.readOnly, + +// controller: _audienceController, + +// hintText: "Audience", + +// onChanged: (_) => _updateOAuth2(), + +// ), + +// ), + + Divider(), + + kVSpacer16, + + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _refreshTokenController, + hintText: "Refresh Token", + onChanged: (_) => _updateOAuth2(), + ), + ), + + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _identityTokenController, + hintText: "Identity Token", + onChanged: (_) => _updateOAuth2(), + ), + ), + + ..._buildFieldWithSpacing( + AuthTextField( + readOnly: widget.readOnly, + controller: _accessTokenController, + hintText: "Access Token", + onChanged: (_) => _updateOAuth2(), + ), + ), + + kVSpacer16, + ], + ); + } + + List _buildFieldWithSpacing(Widget field) { + return [ + field, + kVSpacer16, + ]; + } + + bool _shouldShowField(OAuth2Field field) { + const alwaysShownFields = { + OAuth2Field.accessTokenUrl, + OAuth2Field.clientId, + OAuth2Field.clientSecret, + OAuth2Field.scope, + OAuth2Field.headerPrefix, + OAuth2Field.audience, + OAuth2Field.refreshToken, + OAuth2Field.identityToken, + OAuth2Field.accessToken, + }; + + if (alwaysShownFields.contains(field)) { + return true; + } + + switch (_grantType) { + case OAuth2GrantType.authorizationCode: + return const { + OAuth2Field.authorizationUrl, + OAuth2Field.redirectUrl, + OAuth2Field.codeChallengeMethod, + OAuth2Field.state, + }.contains(field); + + case OAuth2GrantType.resourceOwnerPassword: + return const { + OAuth2Field.username, + OAuth2Field.password, + }.contains(field); + + case OAuth2GrantType.clientCredentials: + return false; + } + } + + void _updateOAuth2() { + final updatedOAuth2 = AuthOAuth2Model( + grantType: _grantType, + + authorizationUrl: _authorizationUrlController.text.trim(), + + clientId: _clientIdController.text.trim(), + + accessTokenUrl: _accessTokenUrlController.text.trim(), + + clientSecret: _clientSecretController.text.trim(), + + codeChallengeMethod: _codeChallengeMethod, + + redirectUrl: _redirectUrlController.text.trim(), + + scope: _scopeController.text.trim(), + + state: _stateController.text.trim(), + + username: _usernameController.text.trim(), + + password: _passwordController.text.trim(), + + refreshToken: _refreshTokenController.text.trim(), + + identityToken: _identityTokenController.text.trim(), + + accessToken: _accessTokenController.text.trim(), + +// headerPrefix: _headerPrefixController.text.trim(), + +// audience: _audienceController.text.trim(), + ); + + widget.updateAuth?.call( + widget.authData?.copyWith( + type: APIAuthType.oauth2, + oauth2: updatedOAuth2, + ) ?? + AuthModel( + type: APIAuthType.oauth2, + oauth2: updatedOAuth2, + ), + ); + } + + @override + void dispose() { + _authorizationUrlController.dispose(); + + _accessTokenUrlController.dispose(); + + _clientIdController.dispose(); + + _clientSecretController.dispose(); + + _redirectUrlController.dispose(); + + _scopeController.dispose(); + + _stateController.dispose(); + + _usernameController.dispose(); + + _passwordController.dispose(); + + _refreshTokenController.dispose(); + + _identityTokenController.dispose(); + + _accessTokenController.dispose(); + +// _headerPrefixController.dispose(); + +// _audienceController.dispose(); + + super.dispose(); + } +} + +enum OAuth2Field { + authorizationUrl, + + accessTokenUrl, + + clientId, + + clientSecret, + + redirectUrl, + + scope, + + state, + + codeChallengeMethod, + + username, + + password, + + refreshToken, + + identityToken, + + accessToken, + + headerPrefix, + + audience +} diff --git a/lib/utils/file_utils.dart b/lib/utils/file_utils.dart index 8f7c6a72..10743b18 100644 --- a/lib/utils/file_utils.dart +++ b/lib/utils/file_utils.dart @@ -34,6 +34,18 @@ Future getFileDownloadpath(String? name, String? ext) async { return null; } +Future getTempFilePath(String? name, String? ext) async { + final Directory tempDir = await getApplicationCacheDirectory(); + name = name ?? getTempFileName(); + ext = (ext != null) ? ".$ext" : ""; + String path = '${tempDir.path}/$name$ext'; + int num = 1; + while (await File(path).exists()) { + path = '${tempDir.path}/$name (${num++})$ext'; + } + return path; +} + Future saveFile(String path, Uint8List content) async { final file = File(path); await file.writeAsBytes(content); diff --git a/packages/better_networking/better_networking_example/pubspec.lock b/packages/better_networking/better_networking_example/pubspec.lock index 10c4eb73..bf796c18 100644 --- a/packages/better_networking/better_networking_example/pubspec.lock +++ b/packages/better_networking/better_networking_example/pubspec.lock @@ -9,6 +9,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + 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" async: dependency: transitive description: @@ -88,6 +120,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + url: "https://pub.dev" + source: hosted + version: "0.2.3" ed25519_edwards: dependency: transitive description: @@ -104,6 +144,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" fixnum: dependency: transitive description: @@ -130,6 +178,27 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: "3c14babeaa066c371f3a743f204dd0d348b7d42ffa6fae7a9847a521aff33696" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: c63a472c8070998e4e422f6b34a17070e60782ac442107c70000dd1bed645f4d + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" freezed_annotation: dependency: transitive description: @@ -138,6 +207,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.4" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: transitive description: @@ -226,6 +303,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + oauth2: + dependency: transitive + description: + name: oauth2 + sha256: c84470642cbb2bec450ccab2f8520c079cd1ca546a76ffd5c40589e07f4e8bf4 + url: "https://pub.dev" + source: hosted + version: "2.0.3" path: dependency: transitive description: @@ -234,6 +319,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -242,6 +375,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pointycastle: dependency: transitive description: @@ -318,6 +467,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: @@ -342,6 +555,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.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" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -352,4 +613,4 @@ packages: version: "6.5.0" sdks: dart: ">=3.8.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.27.0" diff --git a/packages/better_networking/lib/consts.dart b/packages/better_networking/lib/consts.dart index 778dba2d..3752e2d7 100644 --- a/packages/better_networking/lib/consts.dart +++ b/packages/better_networking/lib/consts.dart @@ -43,6 +43,15 @@ const kJwtAlgos = [ 'EdDSA', ]; +enum OAuth2GrantType { + authorizationCode("Authorization Code"), + clientCredentials("Client Credentials"), + resourceOwnerPassword("Resource Owner Password"); + + const OAuth2GrantType(this.displayType); + final String displayType; +} + enum HTTPVerb { get("GET"), head("HEAD"), diff --git a/packages/better_networking/lib/models/auth/auth_oauth2_model.dart b/packages/better_networking/lib/models/auth/auth_oauth2_model.dart index fd5335bd..17148fc1 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth2_model.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth2_model.dart @@ -1,3 +1,4 @@ +import 'package:better_networking/consts.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'auth_oauth2_model.g.dart'; @@ -7,7 +8,7 @@ part 'auth_oauth2_model.freezed.dart'; @freezed class AuthOAuth2Model with _$AuthOAuth2Model { const factory AuthOAuth2Model({ - @Default("authorization_code") String grantType, + @Default(OAuth2GrantType.authorizationCode) OAuth2GrantType grantType, required String authorizationUrl, diff --git a/packages/better_networking/lib/models/auth/auth_oauth2_model.freezed.dart b/packages/better_networking/lib/models/auth/auth_oauth2_model.freezed.dart index c48c5853..41f0697e 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth2_model.freezed.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth2_model.freezed.dart @@ -21,7 +21,7 @@ AuthOAuth2Model _$AuthOAuth2ModelFromJson(Map json) { /// @nodoc mixin _$AuthOAuth2Model { - String get grantType => throw _privateConstructorUsedError; + OAuth2GrantType get grantType => throw _privateConstructorUsedError; String get authorizationUrl => throw _privateConstructorUsedError; String get accessTokenUrl => throw _privateConstructorUsedError; String get clientId => throw _privateConstructorUsedError; @@ -56,7 +56,7 @@ abstract class $AuthOAuth2ModelCopyWith<$Res> { ) = _$AuthOAuth2ModelCopyWithImpl<$Res, AuthOAuth2Model>; @useResult $Res call({ - String grantType, + OAuth2GrantType grantType, String authorizationUrl, String accessTokenUrl, String clientId, @@ -112,7 +112,7 @@ class _$AuthOAuth2ModelCopyWithImpl<$Res, $Val extends AuthOAuth2Model> grantType: null == grantType ? _value.grantType : grantType // ignore: cast_nullable_to_non_nullable - as String, + as OAuth2GrantType, authorizationUrl: null == authorizationUrl ? _value.authorizationUrl : authorizationUrl // ignore: cast_nullable_to_non_nullable @@ -189,7 +189,7 @@ abstract class _$$AuthOAuth2ModelImplCopyWith<$Res> @override @useResult $Res call({ - String grantType, + OAuth2GrantType grantType, String authorizationUrl, String accessTokenUrl, String clientId, @@ -244,7 +244,7 @@ class __$$AuthOAuth2ModelImplCopyWithImpl<$Res> grantType: null == grantType ? _value.grantType : grantType // ignore: cast_nullable_to_non_nullable - as String, + as OAuth2GrantType, authorizationUrl: null == authorizationUrl ? _value.authorizationUrl : authorizationUrl // ignore: cast_nullable_to_non_nullable @@ -314,7 +314,7 @@ class __$$AuthOAuth2ModelImplCopyWithImpl<$Res> @JsonSerializable() class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model { const _$AuthOAuth2ModelImpl({ - this.grantType = "authorization_code", + this.grantType = OAuth2GrantType.authorizationCode, required this.authorizationUrl, required this.accessTokenUrl, required this.clientId, @@ -337,7 +337,7 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model { @override @JsonKey() - final String grantType; + final OAuth2GrantType grantType; @override final String authorizationUrl; @override @@ -453,7 +453,7 @@ class _$AuthOAuth2ModelImpl implements _AuthOAuth2Model { abstract class _AuthOAuth2Model implements AuthOAuth2Model { const factory _AuthOAuth2Model({ - final String grantType, + final OAuth2GrantType grantType, required final String authorizationUrl, required final String accessTokenUrl, required final String clientId, @@ -475,7 +475,7 @@ abstract class _AuthOAuth2Model implements AuthOAuth2Model { _$AuthOAuth2ModelImpl.fromJson; @override - String get grantType; + OAuth2GrantType get grantType; @override String get authorizationUrl; @override diff --git a/packages/better_networking/lib/models/auth/auth_oauth2_model.g.dart b/packages/better_networking/lib/models/auth/auth_oauth2_model.g.dart index 79b7e0ad..742832f2 100644 --- a/packages/better_networking/lib/models/auth/auth_oauth2_model.g.dart +++ b/packages/better_networking/lib/models/auth/auth_oauth2_model.g.dart @@ -9,7 +9,9 @@ part of 'auth_oauth2_model.dart'; _$AuthOAuth2ModelImpl _$$AuthOAuth2ModelImplFromJson( Map json, ) => _$AuthOAuth2ModelImpl( - grantType: json['grantType'] as String? ?? "authorization_code", + grantType: + $enumDecodeNullable(_$OAuth2GrantTypeEnumMap, json['grantType']) ?? + OAuth2GrantType.authorizationCode, authorizationUrl: json['authorizationUrl'] as String, accessTokenUrl: json['accessTokenUrl'] as String, clientId: json['clientId'] as String, @@ -30,7 +32,7 @@ _$AuthOAuth2ModelImpl _$$AuthOAuth2ModelImplFromJson( Map _$$AuthOAuth2ModelImplToJson( _$AuthOAuth2ModelImpl instance, ) => { - 'grantType': instance.grantType, + 'grantType': _$OAuth2GrantTypeEnumMap[instance.grantType]!, 'authorizationUrl': instance.authorizationUrl, 'accessTokenUrl': instance.accessTokenUrl, 'clientId': instance.clientId, @@ -47,3 +49,9 @@ Map _$$AuthOAuth2ModelImplToJson( 'identityToken': instance.identityToken, 'accessToken': instance.accessToken, }; + +const _$OAuth2GrantTypeEnumMap = { + OAuth2GrantType.authorizationCode: 'authorizationCode', + OAuth2GrantType.clientCredentials: 'clientCredentials', + OAuth2GrantType.resourceOwnerPassword: 'resourceOwnerPassword', +}; diff --git a/packages/better_networking/lib/utils/auth/handle_auth.dart b/packages/better_networking/lib/utils/auth/handle_auth.dart index b18c3dab..4fd00c7e 100644 --- a/packages/better_networking/lib/utils/auth/handle_auth.dart +++ b/packages/better_networking/lib/utils/auth/handle_auth.dart @@ -1,8 +1,11 @@ import 'dart:convert'; +import 'dart:io'; 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'; +import 'package:better_networking/utils/auth/oauth2_utils.dart'; +import 'package:flutter/foundation.dart'; Future handleAuth( HttpRequestModel httpRequestModel, @@ -157,8 +160,54 @@ Future handleAuth( // TODO: Handle this case. throw UnimplementedError(); case APIAuthType.oauth2: - // TODO: Handle this case. - throw UnimplementedError(); + final oauth2 = authData.oauth2; + + if (oauth2 == null) { + throw Exception("Failed to get OAuth2 Data"); + } + + if (oauth2.redirectUrl == null) { + throw Exception("No Redirect URL found!"); + } + + //TODO: Create a proper credentials file path, use the existing file utils if needed. + final credentialsDir = Directory.systemTemp; + final credentialsFile = File( + '${credentialsDir.path}/oauth2_credentials.json', + ); + + switch (oauth2.grantType) { + case OAuth2GrantType.authorizationCode: + final res = await oAuth2AuthorizationCodeGrantHandler( + identifier: oauth2.clientId, + secret: oauth2.clientSecret, + authorizationEndpoint: Uri.parse(oauth2.authorizationUrl), + redirectUrl: Uri.parse( + oauth2.redirectUrl ?? "apidash://oauth2/callback", + ), + tokenEndpoint: Uri.parse(oauth2.accessTokenUrl), + credentialsFile: credentialsFile, + scope: oauth2.scope, + ); + debugPrint(res.credentials.accessToken); + + // Add the access token to the request headers + updatedHeaders.add( + NameValueModel( + name: 'Authorization', + value: 'Bearer ${res.credentials.accessToken}', + ), + ); + updatedHeaderEnabledList.add(true); + + break; + case OAuth2GrantType.clientCredentials: + // TODO: Handle this case. + throw UnimplementedError(); + case OAuth2GrantType.resourceOwnerPassword: + // TODO: Handle this case. + throw UnimplementedError(); + } } return httpRequestModel.copyWith( diff --git a/packages/better_networking/lib/utils/auth/oauth2_utils.dart b/packages/better_networking/lib/utils/auth/oauth2_utils.dart new file mode 100644 index 00000000..dc1b56dc --- /dev/null +++ b/packages/better_networking/lib/utils/auth/oauth2_utils.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:developer' show log; +import 'dart:io'; +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:oauth2/oauth2.dart' as oauth2; + +Future oAuth2AuthorizationCodeGrantHandler({ + required String identifier, + required String secret, + required Uri authorizationEndpoint, + required Uri tokenEndpoint, + required Uri redirectUrl, + required File credentialsFile, + String? state, + String? scope, +}) async { + if (await credentialsFile.exists()) { + try { + final json = await credentialsFile.readAsString(); + + final credentials = oauth2.Credentials.fromJson(json); + + if (credentials.accessToken.isNotEmpty && !credentials.isExpired) { + log('Using existing valid credentials'); + + return oauth2.Client( + credentials, + identifier: identifier, + secret: secret, + ); + } + } catch (e) { + log('Error reading existing credentials: $e'); + } + } + + final grant = oauth2.AuthorizationCodeGrant( + identifier, + authorizationEndpoint, + tokenEndpoint, + secret: secret, + ); + + final authorizationUrl = grant.getAuthorizationUrl( + redirectUrl, + scopes: scope != null ? [scope] : null, + state: state, + ); + log('Generated authorization URL: ${authorizationUrl.toString()}'); + log('Expected redirect URL: ${redirectUrl.toString()}'); + + final uri = await FlutterWebAuth2.authenticate( + url: authorizationUrl.toString(), + callbackUrlScheme: redirectUrl.scheme, + options: const FlutterWebAuth2Options(useWebview: true), + ); + + try { + final client = await grant.handleAuthorizationResponse( + Uri.parse(uri).queryParameters, + ); + + log('OAuth2 authorization successful, saving credentials'); + + await credentialsFile.writeAsString(client.credentials.toJson()); + + return client; + } catch (e) { + log('Error handling authorization response: $e'); + + log('URI query parameters: ${Uri.parse(uri).queryParameters}'); + + rethrow; + } +} diff --git a/packages/better_networking/lib/utils/utils.dart b/packages/better_networking/lib/utils/utils.dart index 7857ed81..eaaa5171 100644 --- a/packages/better_networking/lib/utils/utils.dart +++ b/packages/better_networking/lib/utils/utils.dart @@ -5,3 +5,5 @@ export 'http_response_utils.dart'; export 'string_utils.dart' hide RandomStringGenerator; export 'uri_utils.dart'; export 'auth/handle_auth.dart'; +export 'auth/oauth2_webview_utils.dart'; +export 'auth/handle_auth_webview.dart'; diff --git a/packages/better_networking/pubspec.yaml b/packages/better_networking/pubspec.yaml index 50caa33f..96e4b7bb 100644 --- a/packages/better_networking/pubspec.yaml +++ b/packages/better_networking/pubspec.yaml @@ -28,6 +28,10 @@ dependencies: json_annotation: ^4.9.0 seed: ^0.0.3 xml: ^6.3.0 + oauth2: ^2.0.3 + flutter_web_auth_2: ^4.1.0 + app_links: ^6.4.0 + webview_flutter: ^4.10.0 dev_dependencies: flutter_test: diff --git a/pubspec.lock b/pubspec.lock index 0536f2ac..1910e0f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -55,6 +55,38 @@ 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: @@ -397,6 +429,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + url: "https://pub.dev" + source: hosted + version: "0.2.3" ed25519_edwards: dependency: transitive description: @@ -660,6 +700,22 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: "3c14babeaa066c371f3a743f204dd0d348b7d42ffa6fae7a9847a521aff33696" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: c63a472c8070998e4e422f6b34a17070e60782ac442107c70000dd1bed645f4d + url: "https://pub.dev" + source: hosted + version: "4.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -726,6 +782,14 @@ 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: @@ -1134,6 +1198,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + oauth2: + dependency: transitive + description: + name: oauth2 + sha256: c84470642cbb2bec450ccab2f8520c079cd1ca546a76ffd5c40589e07f4e8bf4 + url: "https://pub.dev" + source: hosted + version: "2.0.3" ollama_dart: dependency: "direct main" description: @@ -1962,6 +2034,38 @@ 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: @@ -1987,6 +2091,14 @@ packages: url: "https://github.com/google/flutter-desktop-embedding.git" source: git version: "0.1.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" xdg_directories: dependency: transitive description: