mirror of
https://github.com/foss42/apidash.git
synced 2025-12-03 19:39:25 +08:00
feat: implement local http server in desktop platforms for oauth2
This commit is contained in:
@@ -4,10 +4,17 @@ import 'dart:io';
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
|
||||
import '../../models/models.dart';
|
||||
import '../../models/auth/auth_oauth2_model.dart';
|
||||
import '../../services/http_client_manager.dart';
|
||||
import '../../services/oauth_callback_server.dart';
|
||||
import '../platform_utils.dart';
|
||||
|
||||
Future<oauth2.Client> oAuth2AuthorizationCodeGrantHandler({
|
||||
/// Advanced OAuth2 authorization code grant handler that returns both the client and server
|
||||
/// for cases where you need manual control over the callback server lifecycle.
|
||||
///
|
||||
/// Returns a tuple of (oauth2.Client, OAuthCallbackServer?) where the server is null for mobile platforms.
|
||||
/// The server should be stopped by the caller to clean up resources.
|
||||
Future<(oauth2.Client, OAuthCallbackServer?)> oAuth2AuthorizationCodeGrant({
|
||||
required String identifier,
|
||||
required String secret,
|
||||
required Uri authorizationEndpoint,
|
||||
@@ -17,19 +24,17 @@ Future<oauth2.Client> oAuth2AuthorizationCodeGrantHandler({
|
||||
String? state,
|
||||
String? scope,
|
||||
}) async {
|
||||
// Check for existing valid credentials first
|
||||
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,
|
||||
return (
|
||||
oauth2.Client(credentials, identifier: identifier, secret: secret),
|
||||
null,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -42,50 +47,120 @@ Future<oauth2.Client> oAuth2AuthorizationCodeGrantHandler({
|
||||
final httpClientManager = HttpClientManager();
|
||||
final baseClient = httpClientManager.createClientWithJsonAccept(requestId);
|
||||
|
||||
final grant = oauth2.AuthorizationCodeGrant(
|
||||
identifier,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
secret: secret,
|
||||
httpClient: baseClient,
|
||||
);
|
||||
|
||||
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),
|
||||
);
|
||||
OAuthCallbackServer? callbackServer;
|
||||
Uri actualRedirectUrl = redirectUrl;
|
||||
|
||||
try {
|
||||
// Use standard oauth2 package for other providers
|
||||
// Use localhost callback server for desktop platforms
|
||||
if (PlatformUtils.shouldUseLocalhostCallback) {
|
||||
callbackServer = OAuthCallbackServer();
|
||||
final localhostUrl = await callbackServer.start();
|
||||
actualRedirectUrl = Uri.parse(localhostUrl);
|
||||
log('Using localhost callback server: $localhostUrl');
|
||||
} else {
|
||||
log('Using custom scheme callback: ${redirectUrl.toString()}');
|
||||
}
|
||||
|
||||
final grant = oauth2.AuthorizationCodeGrant(
|
||||
identifier,
|
||||
authorizationEndpoint,
|
||||
tokenEndpoint,
|
||||
secret: secret,
|
||||
httpClient: baseClient,
|
||||
);
|
||||
|
||||
final authorizationUrl = grant.getAuthorizationUrl(
|
||||
actualRedirectUrl,
|
||||
scopes: scope != null ? [scope] : null,
|
||||
state: state,
|
||||
);
|
||||
|
||||
log('Generated authorization URL: ${authorizationUrl.toString()}');
|
||||
log('Expected redirect URL: ${actualRedirectUrl.toString()}');
|
||||
|
||||
String callbackUri;
|
||||
|
||||
if (PlatformUtils.shouldUseLocalhostCallback && callbackServer != null) {
|
||||
// For desktop: Open the authorization URL in the default browser
|
||||
// and wait for the callback on the localhost server with a 3-minute timeout
|
||||
await _openUrlInBrowser(authorizationUrl.toString());
|
||||
|
||||
try {
|
||||
callbackUri = await callbackServer.waitForCallback(
|
||||
timeout: const Duration(minutes: 3),
|
||||
);
|
||||
// Convert the relative callback to full URL
|
||||
callbackUri =
|
||||
'http://localhost${Uri.parse(callbackUri).path}${Uri.parse(callbackUri).query.isNotEmpty ? '?${Uri.parse(callbackUri).query}' : ''}';
|
||||
} on TimeoutException catch (e) {
|
||||
log('OAuth callback timeout: ${e.message}');
|
||||
throw Exception(
|
||||
'OAuth authorization timed out after 3 minutes. '
|
||||
'Please try again and complete the authorization in your browser. '
|
||||
'If you closed the browser tab, please restart the OAuth flow.',
|
||||
);
|
||||
} catch (e) {
|
||||
// Handle custom exceptions like browser tab closure
|
||||
final errorMessage = e.toString();
|
||||
if (errorMessage.contains('Browser tab was closed')) {
|
||||
log('OAuth authorization cancelled: Browser tab closed');
|
||||
throw Exception(
|
||||
'OAuth authorization was cancelled because the browser tab was closed. '
|
||||
'Please try again and complete the authorization process without closing the browser tab.',
|
||||
);
|
||||
} else if (errorMessage.contains('OAuth callback cancelled')) {
|
||||
log('OAuth authorization cancelled by user');
|
||||
throw Exception(
|
||||
'OAuth authorization was cancelled. Please try again if you want to complete the authentication.',
|
||||
);
|
||||
} else {
|
||||
log('OAuth callback error: $errorMessage');
|
||||
throw Exception('OAuth authorization failed: $errorMessage');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For mobile: Use the standard flutter_web_auth_2 approach
|
||||
callbackUri = await FlutterWebAuth2.authenticate(
|
||||
url: authorizationUrl.toString(),
|
||||
callbackUrlScheme: actualRedirectUrl.scheme,
|
||||
options: const FlutterWebAuth2Options(
|
||||
useWebview: true,
|
||||
windowName: 'OAuth Authorization - API Dash',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
log('Received callback URI: $callbackUri');
|
||||
|
||||
// Parse the callback URI and handle the authorization response
|
||||
final callbackUriParsed = Uri.parse(callbackUri);
|
||||
final client = await grant.handleAuthorizationResponse(
|
||||
Uri.parse(uri).queryParameters,
|
||||
callbackUriParsed.queryParameters,
|
||||
);
|
||||
|
||||
log('OAuth2 authorization successful, saving credentials');
|
||||
await credentialsFile.writeAsString(client.credentials.toJson());
|
||||
log(client.credentials.toJson());
|
||||
|
||||
// Clean up the HTTP client
|
||||
httpClientManager.closeClient(requestId);
|
||||
|
||||
return client;
|
||||
return (client, callbackServer);
|
||||
} catch (e) {
|
||||
log('Error handling authorization response: $e');
|
||||
log('URI query parameters: ${Uri.parse(uri).queryParameters}');
|
||||
|
||||
// Clean up the HTTP client on error
|
||||
httpClientManager.closeClient(requestId);
|
||||
|
||||
log('Error during OAuth2 flow: $e');
|
||||
// Clean up the callback server immediately on error
|
||||
if (callbackServer != null) {
|
||||
try {
|
||||
await callbackServer.stop();
|
||||
log('Callback server stopped due to OAuth2 flow error');
|
||||
} catch (serverError) {
|
||||
log(
|
||||
'Error stopping callback server during error cleanup: $serverError',
|
||||
);
|
||||
}
|
||||
}
|
||||
// Re-throw the original error
|
||||
rethrow;
|
||||
} finally {
|
||||
// Clean up HTTP client
|
||||
httpClientManager.closeClient(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,3 +297,32 @@ Future<oauth2.Client> oAuth2ResourceOwnerPasswordGrantHandler({
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a URL in the default system browser.
|
||||
/// This is used for desktop platforms where we want to open the OAuth authorization URL
|
||||
/// in the user's default browser and use localhost callback server to capture the response.
|
||||
Future<void> _openUrlInBrowser(String url) async {
|
||||
try {
|
||||
if (PlatformUtils.isDesktop) {
|
||||
Process? process;
|
||||
if (Platform.isMacOS) {
|
||||
process = await Process.start('open', [url]);
|
||||
} else if (Platform.isWindows) {
|
||||
process = await Process.start('rundll32', [
|
||||
'url.dll,FileProtocolHandler',
|
||||
url,
|
||||
]);
|
||||
} else if (Platform.isLinux) {
|
||||
process = await Process.start('xdg-open', [url]);
|
||||
}
|
||||
|
||||
if (process != null) {
|
||||
await process.exitCode; // Wait for the process to complete
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log('Error opening URL in browser: $e');
|
||||
// Fallback: throw an exception so the calling code can handle it
|
||||
throw Exception('Failed to open authorization URL in browser: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user