Google & Microsoft OAuth2 login flow Flutter Desktop (MacOS, Windows, Linux)

Viewed 872

How do you implement Google OAuth2 or Microsoft (Azure) OAuth2 login on Flutter Desktop?

1 Answers

Answering my own question. The overarching process to get the OAuth2 result is:

  1. 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.
  2. Launch the URL to start the OAuth2 flow in the browser using oauth2
  3. 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:

  1. Go to google admin or azure dashboard,
  2. create a new app + add in Authorized redirect URIs the localhost url: http://localhost/
  3. 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!

Credits to this tutorial for explaining this process of Github OAuth2 login on Flutter Desktop.

Related