How do you implement Google OAuth2 or Microsoft (Azure) OAuth2 login on Flutter Desktop?
Asked
Active
Viewed 2,426 times
2 Answers
19
Answering my own question. The overarching process to get the OAuth2 result is:
- You have to have the desktop app host a local server and have the OAuth services redirect to
http://localhost:#####/that the dart app is listening to. - Launch the URL to start the OAuth2 flow in the browser using oauth2
- On first return to the server, process the OAuth2 response using oauth2
Setup the OAuth flows you want to support. This is what I used:
- Go to google admin or azure dashboard,
- create a new app + add in
Authorized redirect URIsthe localhost url:http://localhost/ - Copy the generated clientId and clientSecret into the configuration below:
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
}
}
Setup the OAuth Manager to create listen for the oauth2 redirect
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);
}
}
Begin the login flow using
GOOGLE:
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();
}
You're done!
Neal Soni
- 556
- 1
- 5
- 13
-
1Ah! Run your own http server to listen for the response! I don't know why I didn't think of that.. Thanks! – Vance Palacio Apr 01 '22 at 16:25
-
4Fantastic! Adding my hurdles to save the next person: On MacOS you need to add an com.apple.security.network.client entitlement as explained here: https://stackoverflow.com/questions/65458903/socketexception-connection-failed-os-error-operation-not-permitted-errno-1 And if you get a redirect_uri_mismatch, make sure to add http://localhost/auth to your redirect URI's (no port!). Many more things to try here: https://stackoverflow.com/questions/11485271/google-oauth-2-authorization-error-redirect-uri-mismatch – Oded Ben Dov Jul 06 '22 at 12:45
-
That is to say `http://localhost/auth` (couldn't edit my previous comment) – Oded Ben Dov Jul 06 '22 at 12:52
-
1thanks @OdedBenDov for that addition! I only tested on windows when originally writing this. Glad it still works a year later. – Neal Soni Jul 07 '22 at 13:52
-
1This was extremely helpful, thank you! I got it working in a macOS app that uses the Dropbox API. – Clifton Labrum Jul 23 '22 at 19:20
-
It seems that Apple support neither localhost nor http (without s). – Dmitry Sikorsky Nov 01 '22 at 09:03
-
Anyone know how to create a firebase instance from the credentials returned? Currently, I do not think this works on windows due to platform support – MarvinKweyu Aug 17 '23 at 12:45
1
To add on @Neal's great answer,
If you want his code to return a refresh token as well, add this:
var authorizationUrl =
grant.getAuthorizationUrl(redirectUrl, scopes: loginProvider.scopes);
// ADD THIS:
authorizationUrl = authorizationUrl.replace(queryParameters: {
...authorizationUrl.queryParameters,
"access_type": "offline",
});
Eric Aya
- 69,473
- 35
- 181
- 253
Oded Ben Dov
- 9,936
- 6
- 38
- 53