Search code examples
flutteroauth-2.0google-oauthflutter-desktopflutter-windows

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


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


Solution

  • 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!