如何将跨平台 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 身份验证。
还有另一种方法,但使用 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,
};
}
我认为这仍然可以改进很多,但如果您面临类似的挑战,这绝对是一个值得入手的东西。
我有一个在 Flutter 中创建的跨平台应用程序(移动、桌面和 Web),我想将其设置为使用 Azure AD 进行身份验证。
我知道您可以为移动设备甚至网络添加一些软件包,但我找不到适用于桌面的有效解决方案。
我想我可以打开设备上的浏览器并使用它来让用户登录,但是它需要一个 URI 来重定向到用户通过身份验证的时间,并且应用程序能够获得令牌然后我可以用它来拨打我的 API。我看不出这是如何工作的,因为应用程序托管在用户设备上,而不是像网站一样托管在具有固定 IP 的服务器上。
任何可能的解决方案或指导将不胜感激。
找到一个 MS 文档,您可以按照该文档在桌面应用程序中添加 Azure 身份验证。
还有另一种方法,但使用 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,
};
}
我认为这仍然可以改进很多,但如果您面临类似的挑战,这绝对是一个值得入手的东西。