Google Authentication with Firebase for Flutter Desktop Apps

Implement Google authentication into your application using Firebase
Feb 6 2023 · 6 min read

Background

Google Sign-In with Firebase Auth is a secure authentication system that allows users to sign in to your Flutter app using their Google account, while also allowing you to manage the authentication process on the backend using Firebase. 

Integrating Google sign-in with a Flutter desktop app is a bit different from mobile apps because google_sign_in is a popular Flutter package for that but it does not have desktop platform support yet.

This article will guide you on implementing Sign-in with Google for your Flutter desktop apps using OAuth2.0 protocol and desktop browser.

Google APIs use the OAuth 2.0 protocol for authentication and authorization. 


The OAuth 2.0 flow in Google’s Implementation:

  • User clicks on the “Sign in with Google” button in our app, which redirects them to Google’s 2.0 authorization endpoint.
  • The user is prompted to log in to their Google account and grant permission for our app to access their data.
  • If the user grants permission, Google redirects the browser to the redirect URI specified in the initial request, along with an authorization code.
  • Our app sends a request to Google’s OAuth 2.0 token endpoint, along with the authorization code for an access token.
  • Google returns an access token, which our app can use to authenticate requests to the corresponding Google service’s API.
  • Our app can now make authorized requests on behalf of the user until the access token expires or is revoked by the user.

Sponsored

We believe that everyone deserves the opportunity to succeed and thrive, and Justly is designed to provide the guidance and support needed to make that a reality.

Prerequisites

Step 1: Set up the Firebase project

The first step is to set up the Firebase project and enable Google sign-in. if you already have a flutter project, you can skip this step.

  1. Go to the Firebase console and create a new project.
  2. Click on the Authentication link in the left-hand menu, then click on the Sign-in Method tab.
  3. Enable the Google sign-in method.

Step 2: Configure the OAuth client

You need your application's client ID and Secret from Google Cloud Console to enable Google sign-in. If you’ve it already then skip this step.

  • To get the client ID and secret, follow the steps from the given link.
  • In step 6, choose a web application.
  • In the Authorized redirect URIs and Authorized JavaScript origin, enter the URL “http://localhost

Note: In step 6, we have selected web application because in Android and iOS mobile, OS redirects this code to the corresponding app eg: In Android, they use intent to broadcast the authorization code. But in Windows desktop apps, We have no OS support to send the query parameters (eg: authorization code) from a browser to the corresponding desktop app.

we have to use Loopback IP. In the Loopback IP concept, the app will start a local server on its scope with a fixed IP address like http://localhost:9298/callback and listen for the HTTP request received on this port.

Step 3: Set up the Flutter Project

  • Create a Flutter app that supports a Desktop application and you can follow the instructions in the Flutter documentation to learn how to create a Desktop app.

Let’s Get Started

1. Add Dependencies

Add these dependencies to your pubspec.yaml file:

window_to_front: <latest version>
url_launcher: <latest version>
oauth2: <latest version>
firebase_auth: <latest version>
http: <latest version>

2. Import the Dependencies

import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:http/http.dart' as http;
import 'package:window_to_front/window_to_front.dart';
import 'package:url_launcher/url_launcher.dart';

3. Getting credentials

Let’s define the AuthManager class to Handle all the login-related stuff.

//URL that you've registered as redirect URL.
const String redirectURL= 'http://localhost:'

class AuthManager{
  HttpServer? redirectServer;
 
  Future<oauth2.Credentials> login() async {
    await redirectServer?.close();
    redirectServer = await HttpServer.bind('localhost', 0);
    final redirectURL = redirectUrl + redirectServer!.port.toString();

    oauth2.Client authenticatedHttpClient = await _getOauthClient(Uri.parse(redirectURL));
    return authenticatedHttpClient.credentials;
  }
}

Here we have

  • redirectServer: This field stores a reference to the HttpServer object that is used to listen for redirect requests.
  • login(): It first closes redirectServer that may be open. We need a Client object that has been authorized to access the user’s resources to perform OAuth 2.0 authentication. For that, first, create a new HTTP server by binding to the local host IP address on a random port. pass this redirectURL as a parameter to get the oauth2.Credentials object containing the user’s access token and refresh token.

4. Create OAuth2 Client

const String googleAuthApi= "https://accounts.google.com/o/oauth2/v2/auth";
const String googleTokenApi= "https://oauth2.googleapis.com/token";

class AuthManager{  

    ... 

  Future<oauth2.Client> _getOauthClient(Uri redirectUrl) async {
    var grant = oauth2.AuthorizationCodeGrant(
        googleClientId,//Your google client ID 
        Uri.parse(googleAuthApi),
        Uri.parse(googleTokenApi),
        httpClient: JsonAcceptingHttpClient(), 
        secret: authClientSecret //Your google client secret
    );

    var authorizationUrl = grant.getAuthorizationUrl(redirectUrl, scopes: [emailScope]);
    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);

  }
}
  • _getOauthClient(Uri redirectUrl): It calls the getAuthorizaionUrl() method on the oauth2.AuthorizationCodeGrant object to generate an authorization URL. It calls the redirect() function with this URL to redirect the user to Google’s sign-in page.
  • It calls the listen() function to listen for a response from the server to get query parameters from the request as Map<String, String>.
  • It calls handleAuthorizationResponse() method to exchange the authorization code for an access token and create an oauth2.Client object that is used to make authenticated requests to the authorization server.
  • JsonAcceptingHttpClient: It is just a simple class that extends the http.BaseClient class and overrides the send method by adding a certain header to the request.

5. Redirecting to the Authorization URL

class AuthManager{

   ...
   
  Future<void> redirect(Uri authorizationUri) async {
    if(await canLaunchUrl(authorizationUri)){
      await launchUrl(authorizationUri);
    } else{
      throw Exception('Can not launch $authorizationUri');
    }
  }
}
  • redirect(Uri authorizationUri)This method opens the given uri in the user’s default web browser. If the authorizationUri can be opened in the web browser, the method launches the URL using the launchUrl() function from the url_launcher package.

6. Get the Authorization Code from the request

class AuthManager {

    ...

  Future<Map<String, String>> listen() async {

    var request = await redirectServer!.first;
    var params = request.uri.queryParameters;
    await WindowToFront.activate();

    request.response.statusCode = 200;
    request.response.headers.set('content-type', 'text/plain');
    request.response.writeln('Please close the tab');

    await request.response.close();
    await redirectServer!.close();
    redirectServer = null;

    return params;
  }
} 
  • listen(): This method waits for the first HTTP request to the server created in  login() , then retrieves the query parameters from the request and returns them as a map. The query parameter typically includes an authorization code that can be exchanged for an access token.
  • It calls the WindowToFront.activate() method to bring the window to the front.
  • Then it sets the status code, header, and response body of the request and sets the redirectServer to null after the response & redirectServer are closed.

Yeah!! we got the credentials 🙌

7. Creating AuthCredentials for Firebase authentication

Now, in our business Logic class, use these Credentials to get AuthCredentials to perform Firebase authentication.


class AuthService {

  Future<User?> signInWithGoogle() async {
   User? user;

   if (Platform.isMacOS||Platform.isWindows||Platform.isLinux) {

      Credentials credentials = await _authManager.login();

      AuthCredential authCredential = GoogleAuthProvider.credential(
           idToken: credentials.idToken,
           accessToken: credentials.accessToken);

      UserCredential userCredential = await _signInWithFirebase(authCredential);
      user= userCredential.user;
    } 
   return user;
  }
}

Store the access token somewhere as you wish. so, you can use it later.

  • signInWithGoogle(): This method uses the GoogleAuthProvider to create an AuthCredential object using the idToken and accessToken obtained from the Credentials object obtained via a call to _authManager.login() and retrieve the UserCredential from _signInWithCredentials method to return signed-in user.

8. Signing a user with AuthCredentials

class AuthService {

  ...

  Future<UserCredential> _signInWithFirebase(AuthCredential authCredential) async {

    final FirebaseAuth auth = FirebaseAuth.instance;
    UserCredential userCredential;

    try {
      userCredential = await auth.signInWithCredential(authCredential);
    } on FirebaseAuthException catch (error) {
      throw Exception('Could not authenticated $error');
    }
    return userCredential;
  }
}
  • _signInWithFirebase(authCredential): This method passes the AuthCredential Object as a parameter and perform Firebase authentication using these credentials. It will return the UserCredential object if the authentication is successful.

9. Implementing Firebase Sign-Out

const  String revokeTokenUrl = 'https://oauth2.googleapis.com/revoke';

class AuthManager{

  ...

  Future<bool> signOutFromGoogle(String accessToken) async {

    final Uri uri = Uri.parse(revokeTokenUrl).replace(queryParameters: {token: accessToken});
    final http.Response response = await http.post(uri);

     if (response.statusCode == 200) {
         return true;
     } else {
         return false;
     }
   }
 }
  • signOutFromGoogle(String accessToken): This method sends a request to the Google revoke token URL. It is used access token as a parameter and returns a boolean value indicating whether the token has been revoked or not.
  • If the request is successful, the access token is revoked and the user is signed out of their Google account.
class AuthService {

  ...

  Future<void> signOut() async {

    final FirebaseAuth auth = FirebaseAuth.instance;

    try {
      if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
        await _desktopAuthManager.signOutFromGoogle(token);
      } 
        await auth.signOut();
      } on Exception {
        throw Exception('Something went wrong');
      }
    }
  }

Now, use it in your UI and call this method from your UI and you’ve successfully redirected to your default browser to get Google authentication and then get back to the app after authentication.

And here we are done.

Conclusion

Google Sign-In with Firebase can be a powerful combination for creating secure and user-friendly applications.

I hope this blog will provide sufficient information to try up Google Sign-In in your Flutter Desktop projects. There may be a better solution than this, so if you find any other way to implement this thing then feel free to add your solution in the comment.

Happy coding! 🚀✨

Useful Articles


sneha-s image
Sneha Sanghani
Flutter developer | Writing a Blog on Flutter development


sneha-s image
Sneha Sanghani
Flutter developer | Writing a Blog on Flutter development

footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.