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:
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.
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.
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.
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.
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>
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';
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.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.listen()
function to listen for a response from the server to get query parameters from the request as Map<String, String>.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.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.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.redirectServer
to null after the response & redirectServer are closed.Yeah!! we got the credentials 🙌
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.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.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.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.
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.
We’re Grateful to have you with us on this journey!
Suggestions and feedback are more than welcome!
Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.
Happy coding! 🚀✨
Let's Work Together
Not sure where to start? We also offer code and architecture reviews, strategic planning, and more.