谷歌和微软的OAuth2登录流程在Flutter桌面应用上的实现(MacOS、Windows、Linux)。

8

如何在Flutter桌面应用中实现Google OAuth2或Microsoft(Azure) OAuth2登录?

2个回答

21

回答自己的问题。获得 OAuth2 结果的总体过程如下:

  1. 您必须让桌面应用程序托管本地服务器,并使 OAuth 服务重定向到 http://localhost:#####/,使 dart 应用程序监听它。
  2. 使用 oauth2 在浏览器中启动 URL,开始 OAuth2 流程。
  3. 在第一次返回到服务器时,使用 oauth2 处理 OAuth2 响应。

设置要支持的 OAuth 流程。 这是我使用的:

  1. 进入 Google 管理中心Azure 仪表板
  2. 创建一个新应用 + 添加 授权重定向 URI 的 localhost url:http://localhost/
  3. 将生成的 clientId 和 clientSecret 复制到下面的配置中:
enum LoginProvider { google, azure }

extension LoginProviderExtension on LoginProvider {
  String get key {
    switch (this) {
      case LoginProvider.google:
        return 'google';
      case LoginProvider.azure:
        return 'azure';
    }
  }

  String get authorizationEndpoint {
    switch (this) {
      case LoginProvider.google:
        return "https://accounts.google.com/o/oauth2/v2/auth";
      case LoginProvider.azure:
        return "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
    }
  }

  String get tokenEndpoint {
    switch (this) {
      case LoginProvider.google:
        return "https://oauth2.googleapis.com/token";
      case LoginProvider.azure:
        return "https://login.microsoftonline.com/common/oauth2/v2.0/token";
    }
  }

  String get clientId {
    switch (this) {
      case LoginProvider.google:
        return "GOOGLE_CLIENT_ID";
      case LoginProvider.azure:
        return "AZURE_CLIENT_ID";
    }
  }

  String? get clientSecret {
    switch (this) {
      case LoginProvider.google:
        return "GOOGLE_SECRET"; // if applicable
      case LoginProvider.azure:
        return "AZURE_SECRET"; // if applicable
    }
  }

  List<String> get scopes {
    return ['openid', 'email']; // OAuth Scopes
  }
}

设置OAuth管理器以便监听oauth2重定向

import 'dart:async';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:url_launcher/url_launcher.dart';
import 'package:window_to_front/window_to_front.dart';

class DesktopLoginManager {
  HttpServer? redirectServer;
  oauth2.Client? client;

  // Launch the URL in the browser using url_launcher
  Future<void> redirect(Uri authorizationUrl) async {
    var url = authorizationUrl.toString();
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw Exception('Could not launch $url');
    }
  }

  Future<Map<String, String>> listen() async {
    var request = await redirectServer!.first;
    var params = request.uri.queryParameters;
    await WindowToFront.activate(); // Using window_to_front package to bring the window to the front after successful login.  
    request.response.statusCode = 200;
    request.response.headers.set('content-type', 'text/plain');
    request.response.writeln('Authenticated! You can close this tab.');
    await request.response.close();
    await redirectServer!.close();
    redirectServer = null;
    return params;
  }
}

class DesktopOAuthManager extends DesktopLoginManager {
  final LoginProvider loginProvider;

  DesktopOAuthManager({
    required this.loginProvider,
  }) : super();

  void login() async {
    await redirectServer?.close();
    // Bind to an ephemeral port on localhost
    redirectServer = await HttpServer.bind('localhost', 0);
    final redirectURL = 'http://localhost:${redirectServer!.port}/auth';
    var authenticatedHttpClient =
        await _getOAuth2Client(Uri.parse(redirectURL));
    print("CREDENTIALS ${authenticatedHttpClient.credentials}");
    /// HANDLE SUCCESSFULL LOGIN RESPONSE HERE
    return;
  }

  Future<oauth2.Client> _getOAuth2Client(Uri redirectUrl) async {
    var grant = oauth2.AuthorizationCodeGrant(
      loginProvider.clientId,
      Uri.parse(loginProvider.authorizationEndpoint),
      Uri.parse(loginProvider.tokenEndpoint),
      httpClient: _JsonAcceptingHttpClient(),
      secret: loginProvider.clientSecret,
    );
    var authorizationUrl =
        grant.getAuthorizationUrl(redirectUrl, scopes: loginProvider.scopes);

    await redirect(authorizationUrl);
    var responseQueryParameters = await listen();
    var client =
        await grant.handleAuthorizationResponse(responseQueryParameters);
    return client;
  }
}

class _JsonAcceptingHttpClient extends http.BaseClient {
  final _httpClient = http.Client();
  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) {
    request.headers['Accept'] = 'application/json';
    return _httpClient.send(request);
  }
}

使用以下步骤开始登录流程

谷歌:

if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
   final provider = DesktopOAuthManager(loginProvider: LoginProvider.google);
   provider.login();
}

微软 Azure 云服务:

if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
   final provider = DesktopOAuthManager(loginProvider: LoginProvider. azure);
   provider.login();
}

完成了!


1
啊!运行自己的HTTP服务器来监听响应!我不知道为什么我没想到这个..谢谢! - Vance Palacio
4
太棒了!我增加了我的障碍来拯救下一个人:在MacOS上,您需要添加com.apple.security.network.client权限,请参阅此处的说明:https://dev59.com/UVEG5IYBdhLWcg3weOy7如果出现redirect_uri_mismatch,请确保将http://localhost/auth添加到您的重定向URI(没有端口!)。 这里有更多尝试的方法:https://dev59.com/lmgu5IYBdhLWcg3wOUkk - Oded Ben Dov
也就是说 http://localhost/auth(无法编辑我的上一条评论) - Oded Ben Dov
1
感谢@OdedBenDov的补充!我最初编写时只在Windows上进行了测试。很高兴它仍然可以在一年后使用。 - Neal Soni
1
这非常有帮助,谢谢!我已经在使用Dropbox API的macOS应用程序中使其工作。 - Clifton Labrum
显示剩余2条评论

1

补充@Neal的好答案,

如果您想让他的代码也返回刷新令牌,请添加以下内容:

var authorizationUrl = 
    grant.getAuthorizationUrl(redirectUrl, scopes: loginProvider.scopes);

// ADD THIS:
authorizationUrl = authorizationUrl.replace(queryParameters: {
    ...authorizationUrl.queryParameters,
    "access_type": "offline",  
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接