Users love SSO options. They reduce typing, remove onboarding friction, and add a dash of credibility to your software applications. There have been a thousand articles written about adding Google Sign In to Android and iOS Flutter apps so this post will focus instead on the web app implementation. The Peculiarities of Web Google Sign In Authentication vs Authorization establishes who someone is and this process is typically handled by a “Sign In” or “Sign Up” button. Authentication on the other hand, is the process of granting or rejecting access to a user’s data. A user can be authenticated (signed in) to an app but refuse to allow that app to access their personal information. Authorization In the past, Google Sign In on the web used the to authenticate and authorize users in a single go. On March 31st, 2023, that library was deprecated and the web implementation migrated to the new which treats authentication and authorization separately. This newer way of doing things unfortunately means the Flutter web implementation is more complex than those on the mobile platforms. Google Sign-In platform library Google Identity Services SDK ID Token vs Access Token are artifacts introduced by the OpenID Connect protocol that prove a user has been authenticated. ID Tokens allow a client application to access resources on behalf of a user and are therefore associated with authorization rather than authentication. Access Tokens On the web, the google_sign_in plugin will only provide an ID token if you use the method or the widget. The signIn method used on mobile platforms only provides an access token and so should be avoided if your app needs an ID token. As explained in the , the signIn method uses the “OAuth implicit flow” to authorize scopes which omits the ID token in its response. signInSilently renderButton readme for the google_sign_in_web plugin This further explains the difference between the two types of tokens. blog post by Auth0 Summary The following highlights the primary factors that make Google Sign In on the web slightly more complicated: Authentication and Authorization of scopes must be handled in separate steps Authorization of requested scopes is not renewed once a token expires An ID token can only be acquired using signInSilently or the renderButton widget With these things in mind, lets get started. Setup First, add the plugin to your file. This plugin handles all of the heavy lifting related to Google SSO and as mentioned, supports Android, iOS, and web. google_sign_in pubspec.yaml Next, locate or create a Google client ID on the Credentials page in your Google Cloud console . The client should be of the type “Web application” and you should add both and as authorized JavaScript origins so you can test locally. (steps here) http://localhost http://localhost:port : Add —web-port 5000 to your run configuration to fix the localhost port of your web app. Tip Once you have a client ID, you can pass it to the GoogleSignIn class one of two ways. Pass it directly in the dart file GoogleSignIn googleSignIn = GoogleSignIn( clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'), ); Add the client ID as a meta tag in web/index.html <meta name="google-signin-client_id" content="YOUR_GOOGLE_SIGN_IN_OAUTH_CLIENT_ID.apps.googleusercontent.com"> Sign Users In There are a few different ways to “sign users in” with the google_sign_in package on web. The method you use will determine if you receive an ID token or access token. signIn Although the signIn method is not recommended on Flutter web (since it no longer returns an ID token to verify the user’s authentication status), you can still use it to initiate the OAuth flow. GoogleSignIn googleSignIn = GoogleSignIn( clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'), ); await googleSignIn.signIn(); This code will open the Google Sign In popup and allow the user to select their account. Once again, this process does not return an ID token so it is NOT RECOMMENDED. Further, you may see the following error logged in the console: The OAuth token was not passed to gapi.client, since the gapi.client library is not loaded in your page. This message can be safely ignored although it should serve as a reminder that you will not have access to the user’s ID token. signInSilently When you use the signInSilently method, the user will be shown the for Google Sign In. If they are not signed in, the box will ask them to select an account. If they are already signed in, the popup will indicate which account they are signed into and then disappear. One Tap UX If the One Tap UX is closed manually by the user using the close icon, you will not see the popup again according to the . exponential cool-down time table GoogleSignIn googleSignIn = GoogleSignIn( clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'), ); await googleSignIn.signInSilently(); renderButton The renderButton widget is a web-only widget included in the google_sign_in_web plugin. Since it can only be used on web, you’ll need to stub its implementation on mobile platforms as shown in the . If you are developing an app specifically for web however, the following few lines of code are all you need. official example import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in_web/google_sign_in_web.dart' as web; // ... (GoogleSignInPlatform.instance as web.GoogleSignInPlugin).renderButton(), The button doesn’t provide a direct way to monitor the signed in user but the GoogleSignIn class does. You can use the stream property to react to users signing in and out. onCurrentUserChanged GoogleSignInAccount? user; GoogleSignIn googleSignIn = GoogleSignIn( clientId: const String.fromEnvironment('GOOGLE_CLIENT_ID'), ); @override void initState() { googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) { setState(() { user = account; }); }); super.initState(); } The Tokens Once the user has signed in using one of the methods above, you can retrieve their ID and access tokens if they are available. To do this, use the property on the GoogleSignInAccount instance for the user. authentication FutureBuilder<GoogleSignInAuthentication>( future: user!.authentication, builder: (context, auth) { return Column( children: [ const ListTile( leading: Icon(Icons.person), title: Text('ID Token'), ), SelectableText(auth.data?.idToken ?? ''), const ListTile( leading: Icon(Icons.lock), title: Text('Access Token'), ), SelectableText(auth.data?.accessToken ?? '') ], ); }, ), The ID token is a JWT-encoded data structure containing information about the token’s issuer, audience, and expiration date. You can plug the value into the decoder at to examine its contents. You should notice that the “aud” property of the JWT payload matches the client ID of your web application (ex. 967…apps.googleusercontent.com), meaning that this token was generated for your app specifically. Generally though, your client app should never do this. jtw.io The access token in this case is JWT-encoded but instead adheres to the format agreed on by the authorization server that issued it and the resource server where it will be used to access data. Again, your client app should leave this token alone. not Accessing Scopes If you’d like to request information about your user from Google (such as from the People API), you’ll first need to check if the user has authorized the necessary scopes using the web-only method. canAccessScopes Future<bool> _canAccessBirthday() async { return googleSignIn.canAccessScopes( ['https://www.googleapis.com/auth/user.birthday.read'], ); } If this method returns , your web application can fetch the requested data. If the method returns , you’ll need to request access to the related scopes using . true false requestScopes await googleSignIn.requestScopes([ 'https://www.googleapis.com/auth/user.birthday.read', ]); Doing this will cause a popup to appear. Here the user can grant your web app access to the requested data. The google_sign_in implementation on Flutter web does not remember a user’s authorized scopes by default. This means that if a user refreshes the page, you’ll need to request the required scopes all over again. Not great. You can alleviate some of this user pain by caching the Google user’s access token and passing it to the the canAccessScopes method like this: Future<bool> _canAccessBirthday() async { String accessToken = sharedPreferences.getString('googleAccessToken') ?? ''; debugPrint('accessToken: $accessToken'); return googleSignIn.canAccessScopes( ['https://www.googleapis.com/auth/user.birthday.read'], accessToken: accessToken, ); } You can check for access and request it if its missing back-to-back as well: Future<bool> _canAccessBirthday() async { String accessToken = sharedPreferences.getString('googleAccessToken') ?? ''; bool authorized = await googleSignIn.canAccessScopes( ['https://www.googleapis.com/auth/user.birthday.read'], accessToken: accessToken, ); if (authorized) return true; try { bool authorized = await googleSignIn.requestScopes(['https://www.googleapis.com/auth/user.birthday.read']); return authorized; } catch (e) { debugPrint('Error: $e'); return false; } } Sign Users Out There are two ways to sign users out. The first and obvious way is to use the method which as the docs state “marks the user as being in a signed out state”. The important thing about this state is that it still “remembers” the authorized scopes. If a user signs in again on the same device, they will not need to approve scopes that have already been approved. signOut The second way to log a user out is to use the method. Unlike the signOut method, this one completely disconnects the user from the application and revokes previous authorizations. If the user signs in again at a later point, they will be asked to approve all scopes again (ex. birthday, gender, addresses, contacts, etc). disconnect The method you choose will depend on your application and use case. For many web apps where users are expected to return at a later date, signing them out is preferrable to disconnecting them. Check It Out You can download a simple application that demonstrates how the google_sign_in plugin works on web from the . Happy coding! COTR GitHub account Also published . here