如何将跨平台 Flutter 应用程序与 Azure AD 连接起来

How do I hook up a cross platform Flutter app with Azure AD

我有一个在 Flutter 中创建的跨平台应用程序(移动、桌面和 Web),我想将其设置为使用 Azure AD 进行身份验证。

我知道您可以为移动设备甚至网络添加一些软件包,但我找不到适用于桌面的有效解决方案。

我想我可以打开设备上的浏览器并使用它来让用户登录,但是它需要一个 URI 来重定向到用户通过身份验证的时间,并且应用程序能够获得令牌然后我可以用它来拨打我的 API。我看不出这是如何工作的,因为应用程序托管在用户设备上,而不是像网站一样托管在具有固定 IP 的服务器上。

任何可能的解决方案或指导将不胜感激。

找到一个 MS 文档,您可以按照该文档在桌面应用程序中添加 Azure 身份验证。

参考这个:Sign-in a user with the Microsoft Identity Platform in a WPF Desktop application and call an ASP.NET Core Web API

还有另一种方法,但使用 Azure AD B2C:Configure authentication in a sample WPF desktop app by using Azure AD B2C

应用程序注册和架构如下图所示:

我最终结合使用了 this older tutorial for Facebook authentication along with Microsoft documentation 如何为本机应用程序获取令牌以创建如下所示的小型身份验证服务。

我使用了以下发布包:

  • url_launcher
  • flutter_dotenv
  • http

授权服务:

import 'dart:async';
import 'dart:io';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:research_library_viewer/Models/Token.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;

class AuthenticationService {
  String tenant = dotenv.env['MSAL_TENANT']!;
  String clientId = dotenv.env['MSAL_CLIENT_ID']!;
  String clientSecret = dotenv.env['MSAL_CLIENT_SECRET']!;
  String redirectURI = dotenv.env['MSAL_LOGIN_REDIRECT_URI']!;
  String scope = dotenv.env['MSAL_CLIENT_SCOPE']!;
  String authority = dotenv.env['MSAL_AUTHORITY_URI']!;

  Future<Stream<String>> _server() async {
    final StreamController<String> onCode = StreamController();
    HttpServer server =
        await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
    server.listen((HttpRequest request) async {
      final String? code = request.uri.queryParameters["code"];
      request.response
        ..statusCode = 200
        ..headers.set("Content-Type", ContentType.html.mimeType)
        ..write("<html><h1>You can now close this window</h1></html>");
      await request.response.close();
      await server.close(force: true);
      if (code != null) {
        onCode.add(code);
        await onCode.close();
      }
    });
    return onCode.stream;
  }

  String getAuthUrl() {
    String authUrl =
        "http://$authority/$tenant/oauth2/v2.0/authorize?client_id=$clientId&response_type=code&redirect_uri=$redirectURI&response_mode=query&scope=$scope";
    return authUrl;
  }

  Map<String, dynamic> getTokenParameters(String token, bool refresh) {
    Map<String, dynamic> tokenParameters = <String, dynamic>{};
    tokenParameters["client_id"] = clientId;
    tokenParameters["scope"] = scope;
    tokenParameters["client_secret"] = clientSecret;

    if (refresh) {
      tokenParameters["refresh_token"] = token;
      tokenParameters["grant_type"] = "refresh_token";
    } else {
      tokenParameters["code"] = token;
      tokenParameters["redirect_uri"] = redirectURI;
      tokenParameters["grant_type"] = "authorization_code";
    }

    return tokenParameters;
  }

  Future<Token> getToken() async {
    String url = getAuthUrl();
    Stream<String> onCode = await _server();
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw "Could not launch $url";
    }
    final String code = await onCode.first;
    final Map<String, dynamic> tokenParameters =
        getTokenParameters(code, false);
    final response = await http.post(
        Uri.https(
          'login.microsoftonline.com',
          '$tenant/oauth2/v2.0/token',
        ),
        headers: <String, String>{
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: tokenParameters);
    if (response.statusCode == 200) {
      return tokenFromJson(response.body);
    } else {
      throw Exception('Failed to acquire token');
    }
  }

  Future<Token> refreshToken(String? refreshToken) async {
    if (refreshToken == null) {
      return getToken();
    } else {
      final Map<String, dynamic> tokenParameters = getTokenParameters(refreshToken, true);
      final response = await http.post(
        Uri.https(
          'login.microsoftonline.com',
          '$tenant/oauth2/v2.0/token',
        ),
        headers: <String, String>{
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: tokenParameters);
      if (response.statusCode == 200) {
        return tokenFromJson(response.body);
      } else {
        throw Exception('Failed to acquire token');
      }
    }
  }
}

令牌:

import 'dart:convert';

Token tokenFromJson(String str) {
  final jsonData = json.decode(str);
  return Token.fromJson(jsonData);
}

class Token {
  String accessToken;
  String tokenType;
  num? expiresIn;
  String? refreshToken;
  String? idToken;
  String? scope;

  Token({
    required this.accessToken,
    required this.tokenType,
    this.expiresIn,
    this.refreshToken,
    this.idToken,
    this.scope,

  });

  factory Token.fromJson(Map<String, dynamic> json) => Token(
        accessToken: json["access_token"],
        tokenType: json["token_type"],
        expiresIn: json["expires_in"],
        refreshToken: json["refresh_token"],
        idToken: json["id_token"],
        scope: json["scope"],
      );

  Map<String, dynamic> toJson() => {
        "access_token": accessToken,
        "token_type": tokenType,
        "expires_in": expiresIn,
        "refresh_token": refreshToken,
        "id_token": idToken,
        "scope": scope,
      };
}

我认为这仍然可以改进很多,但如果您面临类似的挑战,这绝对是一个值得入手的东西。