diff --git a/example/pubspec.lock b/example/pubspec.lock index 013928c..bbac586 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -17,6 +17,38 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.0" + app_links: + dependency: transitive + description: + name: app_links + sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + 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" args: dependency: transitive description: @@ -203,14 +235,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_web_auth: - dependency: transitive - description: - name: flutter_web_auth - sha256: a69fa8f43b9e4d86ac72176bf747b735e7b977dd7cf215076d95b87cb05affdd - url: "https://pub.dev" - source: hosted - version: "0.5.0" flutter_web_plugins: dependency: transitive description: flutter @@ -232,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: "direct main" description: @@ -620,6 +652,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + url: "https://pub.dev" + source: hosted + version: "6.3.14" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + url: "https://pub.dev" + source: hosted + version: "6.3.1" + 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: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + 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: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + url: "https://pub.dev" + source: hosted + version: "2.2.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + url: "https://pub.dev" + source: hosted + version: "3.1.3" vector_math: dependency: transitive description: @@ -702,4 +798,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.5.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.24.0" diff --git a/lib/logto_client.dart b/lib/logto_client.dart index b0dd8b8..f89df8d 100644 --- a/lib/logto_client.dart +++ b/lib/logto_client.dart @@ -1,7 +1,13 @@ -import 'package:flutter_web_auth/flutter_web_auth.dart'; +import 'dart:async'; +import 'dart:io'; + +import 'package:app_links/app_links.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:http/http.dart' as http; import 'package:jose/jose.dart'; +import '/src/utilities/stream_awaiter.dart'; +import '/src/modules/callback_strategy.dart'; import '/src/exceptions/logto_auth_exceptions.dart'; import '/src/interfaces/logto_interfaces.dart'; import '/src/modules/id_token.dart'; @@ -15,6 +21,7 @@ import '/src/utilities/constants.dart'; export '/src/exceptions/logto_auth_exceptions.dart'; export '/src/interfaces/logto_interfaces.dart'; export '/src/utilities/constants.dart'; +export '/src/modules/callback_strategy.dart'; /** * LogtoClient @@ -35,6 +42,8 @@ export '/src/utilities/constants.dart'; * * final logtoClient = LogtoClient(config); */ +final appLinks = AppLinks(); + class LogtoClient { final LogtoConfig config; @@ -53,13 +62,26 @@ class LogtoClient { OidcProviderConfig? _oidcConfig; - LogtoClient({ - required this.config, - LogtoStorageStrategy? storageProvider, - http.Client? httpClient, - }) { + late final CallbackStrategy _callbackStrategy; + + LogtoClient( + {required this.config, + LogtoStorageStrategy? storageProvider, + http.Client? httpClient, + CallbackStrategy? callbackStrategy}) { _httpClient = httpClient; _tokenStorage = TokenStorage(storageProvider); + _callbackStrategy = callbackStrategy ?? SchemeStrategy(); + } + + final _authController = StreamController.broadcast(); + + Stream get isAuthenticatedStream => _authController.stream; + + void init() async { + bool value = await isAuthenticated; + + _authController.sink.add(value); } // Use idToken to check if the user is authenticated. @@ -159,6 +181,8 @@ class LogtoClient { final idToken = IdToken.unverified(response.idToken!); await _verifyIdToken(idToken, oidcConfig); await _tokenStorage.setIdToken(idToken); + + _authController.sink.add(true); } return await _tokenStorage.getAccessToken( @@ -227,13 +251,7 @@ class LogtoClient { extraParams: extraParams, ); - final redirectUriScheme = Uri.parse(redirectUri).scheme; - - final String callbackUri = await FlutterWebAuth.authenticate( - url: signInUri.toString(), - callbackUrlScheme: redirectUriScheme, - preferEphemeral: true, - ); + final String callbackUri = await _getCallbackUrl(signInUri); await _handleSignInCallback(callbackUri, redirectUri, httpClient); } finally { @@ -272,6 +290,8 @@ class LogtoClient { refreshToken: tokenResponse.refreshToken, expiresIn: tokenResponse.expiresIn, scopes: tokenResponse.scope.split(' ')); + + _authController.sink.add(true); } // Sign out the user. @@ -306,6 +326,7 @@ class LogtoClient { } await _tokenStorage.clear(); + _authController.sink.add(false); } finally { if (_httpClient == null) { httpClient.close(); @@ -338,4 +359,63 @@ class LogtoClient { if (_httpClient == null) httpClient.close(); } } + + Future _getCallbackUrl(Uri url) async { + final LaunchMode launchMode = + _callbackStrategy.launchMode == BrowserLaunchMode.platformDefault + ? LaunchMode.platformDefault + : LaunchMode.externalApplication; + + if (!await launchUrl(url, mode: launchMode)) { + throw Exception('Could not launch ${url.toString()}'); + } + + var result = await _athuneticateUserFlow(); + + return result.toString(); + } + + Future _athuneticateUserFlow() async { + late Uri result; + + if (_callbackStrategy.strategy == CallbackStrategyType.scheme) { + await awaitUriLinkStream( + appLinks.uriLinkStream, + (uri) async { + if (uri != null) { + result = uri; + } else { + throw Exception("Failed to authorize"); + } + }, + ); + + return result; + } + + final server = await HttpServer.bind(InternetAddress.anyIPv4, + (_callbackStrategy as LocalServerStrategy).port); + + await for (HttpRequest request in server) { + // Handle the request + request.response + ..statusCode = HttpStatus.ok + ..headers.contentType = ContentType.html + ..writeln(""" + + + +"""); + await request.response.close(); + result = request.requestedUri; + await server.close(); + break; // Exit the loop + } + + return result; + } + + void dispose() { + _authController.close(); + } } diff --git a/lib/src/modules/callback_strategy.dart b/lib/src/modules/callback_strategy.dart new file mode 100644 index 0000000..6a819a7 --- /dev/null +++ b/lib/src/modules/callback_strategy.dart @@ -0,0 +1,49 @@ +enum CallbackStrategyType { + scheme, + localServer +} + +enum BrowserLaunchMode { + platformDefault, + external +} + +abstract class CallbackStrategy { + CallbackStrategyType get strategy; + BrowserLaunchMode get launchMode; +} + +class SchemeStrategy implements CallbackStrategy { + late BrowserLaunchMode _launchMode; + final CallbackStrategyType _strategy = CallbackStrategyType.scheme; + + SchemeStrategy({BrowserLaunchMode? launchMode}){ + _launchMode = launchMode ?? BrowserLaunchMode.platformDefault; + } + + @override + CallbackStrategyType get strategy => _strategy; + + @override + BrowserLaunchMode get launchMode => _launchMode; + +} + +class LocalServerStrategy implements CallbackStrategy { + late BrowserLaunchMode _launchMode; + final CallbackStrategyType _strategy = CallbackStrategyType.localServer; + final int _port; + + LocalServerStrategy(this._port,{BrowserLaunchMode? launchMode}){ + _launchMode = launchMode ?? BrowserLaunchMode.platformDefault; + } + + @override + CallbackStrategyType get strategy => _strategy; + + int get port => _port; + + @override + BrowserLaunchMode get launchMode => throw _launchMode; + +} \ No newline at end of file diff --git a/lib/src/utilities/stream_awaiter.dart b/lib/src/utilities/stream_awaiter.dart new file mode 100644 index 0000000..3ef701c --- /dev/null +++ b/lib/src/utilities/stream_awaiter.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +Future awaitUriLinkStream(Stream uriLinkStream, Future Function(Uri? uri) onUri) async { + final completer = Completer(); + late StreamSubscription subscription; + + subscription = uriLinkStream.listen( + (uri) async { + try { + await onUri(uri); + completer.complete(); + } catch (e) { + completer.completeError(e); + } finally { + await subscription.cancel(); // Ensure subscription is canceled + } + }, + onError: (error) { + if (!completer.isCompleted) { + completer.completeError(error); + } + }, + onDone: () { + if (!completer.isCompleted) { + completer.complete(); + } + }, + cancelOnError: true, + ); + + return completer.future; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 1db1d4b..76744b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,8 +17,9 @@ dependencies: jose: ^0.3.2 json_annotation: ^4.6.0 path: ^1.8.1 - flutter_web_auth: ^0.6.0 collection: ^1.17.1 + url_launcher: ^6.3.1 + app_links: ^6.3.2 dev_dependencies: flutter_test: