Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/google_sign_in/google_sign_in/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
## NEXT

## 6.1.0

* Exposes the new method `canAccessScopes`.
* This method is only needed, and implemented, on the web platform.
* Other platforms will throw an `UnimplementedError`.
* Updates example app to separate Authentication from Authorization for those
platforms where scopes are not automatically granted upon `signIn` (like the web).
* When `signInSilently` is successful, it returns a User object again on the web.
* Updates README with information about these changes.
* Updates minimum Flutter version to 3.3.
* Aligns Dart and Flutter SDK constraints.

Expand Down
89 changes: 87 additions & 2 deletions packages/google_sign_in/google_sign_in/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,15 @@ be an option.

### Web integration

For web integration details, see the
The new SDK used by the web has fully separated Authentication from Authorization,
so `signIn` and `signInSilently` no longer authorize OAuth `scopes`.

Flutter apps must be able to detect what scopes have been granted by their users,
and if the grants are still valid.

Read below about **Working with scopes, and incremental authorization** for
general information about changes that may be needed on an app, and for more
specific web integration details, see the
[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web).

## Usage
Expand All @@ -114,7 +122,7 @@ Add the following import to your Dart code:
import 'package:google_sign_in/google_sign_in.dart';
```

Initialize GoogleSignIn with the scopes you want:
Initialize `GoogleSignIn` with the scopes you want:

```dart
GoogleSignIn _googleSignIn = GoogleSignIn(
Expand All @@ -139,6 +147,83 @@ Future<void> _handleSignIn() async {
}
```

In the web, you should use the **Google Sign In button** (and not the `signIn` method)
to guarantee that your user authentication contains a valid `idToken`.

For more details, take a look at the
[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web).

## Working with scopes, and incremental authorization.

If your app supports both mobile and web, read this section!

### Checking if scopes have been granted

Users may (or may *not*) grant all the scopes that an application requests at
Sign In. In fact, in the web, no scopes are granted by `signIn`, `silentSignIn`
or the `renderButton` widget anymore.

Applications must be able to:

* Detect if the authenticated user has authorized the scopes they need.
* Determine if the scopes that were granted a few minutes ago are still valid.

There's a new method that enables the checks above, `canAccessScopes`:

```dart
final bool isAuthorized = await _googleSignIn.canAccessScopes(scopes);
```

_(Only implemented in the web platform, from version 6.1.0 of this package)_

### Requesting more scopes when needed

If an app determines that the user hasn't granted the scopes it requires, it
should initiate an Authorization request. (Remember that in the web platform,
this request **must be initiated from an user interaction**, like a button press).

```dart
Future<void> _handleAuthorizeScopes() async {
final bool isAuthorized = await _googleSignIn.requestScopes(scopes);
if (isAuthorized) {
// Do things that only authorized users can do!
_handleGetContact(_currentUser!);
}
}
```

The `requestScopes` returns a `boolean` value that is `true` if the user has
granted all the requested scopes or `false` otherwise.

Once your app determines that the current user `isAuthorized` to access the
services for which you need `scopes`, it can proceed normally.

### Authorization expiration

In the web, **the `accessToken` is no longer refreshed**. It expires after 3600
seconds (one hour), so your app needs to be able to handle failed REST requests,
and update its UI to prompt the user for a new Authorization round.

This can be done by combining the error responses from your REST requests with
the `canAccessScopes` and `requestScopes` methods described above.

For more details, take a look at the
[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web).

### Does an app always need to check `canAccessScopes`?

The new web SDK implicitly grant access to the `email`, `profile` and `openid`
scopes when users complete the sign-in process (either via the One Tap UX or the
Google Sign In button).

If an app only needs an `idToken`, or only requests permissions to any/all of
the three scopes mentioned above
([OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect)),
it won't need to implement any additional scope handling.

If an app needs any scope other than `email`, `profile` and `openid`, it **must**
implement a more complete scope handling, as described above.

## Example

Find the example wiring in the
Expand Down
100 changes: 82 additions & 18 deletions packages/google_sign_in/google_sign_in/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs, avoid_print
// ignore_for_file: avoid_print

import 'dart:async';
import 'dart:convert' show json;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;

import 'src/sign_in_button.dart';

/// The scopes required by this application.
const List<String> scopes = <String>[
'email',
'https://www.googleapis.com/auth/contacts.readonly',
];

GoogleSignIn _googleSignIn = GoogleSignIn(
// Optional clientId
// clientId: '479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com',
scopes: <String>[
'email',
'https://www.googleapis.com/auth/contacts.readonly',
],
// clientId: 'your-client_id.apps.googleusercontent.com',
scopes: scopes,
);

void main() {
Expand All @@ -29,31 +35,54 @@ void main() {
);
}

/// The SignInDemo app.
class SignInDemo extends StatefulWidget {
///
const SignInDemo({super.key});

@override
State createState() => SignInDemoState();
State createState() => _SignInDemoState();
}

class SignInDemoState extends State<SignInDemo> {
class _SignInDemoState extends State<SignInDemo> {
GoogleSignInAccount? _currentUser;
bool _isAuthorized = false; // has granted permissions?
String _contactText = '';

@override
void initState() {
super.initState();
_googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) {

_googleSignIn.onCurrentUserChanged
.listen((GoogleSignInAccount? account) async {
// In mobile, being authenticated means being authorized...
bool isAuthorized = account != null;
// However, in the web...
if (kIsWeb && account != null) {
isAuthorized = await _googleSignIn.canAccessScopes(scopes);
}

setState(() {
_currentUser = account;
_isAuthorized = isAuthorized;
});
if (_currentUser != null) {
_handleGetContact(_currentUser!);

// Now that we know that the user can access the required scopes, the app
// can call the REST API.
if (isAuthorized) {
_handleGetContact(account!);
}
});

// In the web, _googleSignIn.signInSilently() triggers the One Tap UX.
//
// It is recommended by Google Identity Services to render both the One Tap UX
// and the Google Sign In button together to "reduce friction and improve
// sign-in rates" ([docs](https://developers.google.com/identity/gsi/web/guides/display-button#html)).
_googleSignIn.signInSilently();
}

// Calls the People API REST endpoint for the signed-in user to retrieve information.
Future<void> _handleGetContact(GoogleSignInAccount user) async {
setState(() {
_contactText = 'Loading contact info...';
Expand Down Expand Up @@ -103,6 +132,10 @@ class SignInDemoState extends State<SignInDemo> {
return null;
}

// This is the on-click handler for the Sign In button that is rendered by Flutter.
//
// On the web, the on-click handler of the Sign In button is owned by the JS
// SDK, so this method can be considered mobile only.
Future<void> _handleSignIn() async {
try {
await _googleSignIn.signIn();
Expand All @@ -111,11 +144,28 @@ class SignInDemoState extends State<SignInDemo> {
}
}

// Prompts the user to authorize `scopes`.
//
// This action is **required** in platforms that don't perform Authentication
// and Authorization at the same time (like the web).
//
// On the web, this must be called from an user interaction (button click).
Future<void> _handleAuthorizeScopes() async {
final bool isAuthorized = await _googleSignIn.requestScopes(scopes);
setState(() {
_isAuthorized = isAuthorized;
});
if (isAuthorized) {
_handleGetContact(_currentUser!);
}
}

Future<void> _handleSignOut() => _googleSignIn.disconnect();

Widget _buildBody() {
final GoogleSignInAccount? user = _currentUser;
if (user != null) {
// The user is Authenticated
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expand All @@ -127,25 +177,39 @@ class SignInDemoState extends State<SignInDemo> {
subtitle: Text(user.email),
),
const Text('Signed in successfully.'),
Text(_contactText),
if (_isAuthorized) ...<Widget>[
// The user has Authorized all required scopes
Text(_contactText),
ElevatedButton(
child: const Text('REFRESH'),
onPressed: () => _handleGetContact(user),
),
],
if (!_isAuthorized) ...<Widget>[
// The user has NOT Authorized all required scopes.
// (Mobile users may never see this button!)
const Text('Additional permissions needed to read your contacts.'),
ElevatedButton(
onPressed: _handleAuthorizeScopes,
child: const Text('REQUEST PERMISSIONS'),
),
],
ElevatedButton(
onPressed: _handleSignOut,
child: const Text('SIGN OUT'),
),
ElevatedButton(
child: const Text('REFRESH'),
onPressed: () => _handleGetContact(user),
),
],
);
} else {
// The user is NOT Authenticated
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
const Text('You are not currently signed in.'),
ElevatedButton(
// This method is used to separate mobile from web code with conditional exports.
// See: src/sign_in_button.dart
buildSignInButton(
onPressed: _handleSignIn,
child: const Text('SIGN IN'),
),
],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export 'sign_in_button/stub.dart'
if (dart.library.js_util) 'sign_in_button/web.dart'
if (dart.library.io) 'sign_in_button/mobile.dart';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

import 'stub.dart';

/// Renders a SIGN IN button that calls `handleSignIn` onclick.
Widget buildSignInButton({HandleSignInFn? onPressed}) {
return ElevatedButton(
onPressed: onPressed,
child: const Text('SIGN IN'),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';

/// The type of the onClick callback for the (mobile) Sign In Button.
typedef HandleSignInFn = Future<void> Function();

/// Renders a SIGN IN button that (maybe) calls the `handleSignIn` onclick.
Widget buildSignInButton({HandleSignInFn? onPressed}) {
return Container();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
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;

import 'stub.dart';

/// Renders a web-only SIGN IN button.
Widget buildSignInButton({HandleSignInFn? onPressed}) {
return (GoogleSignInPlatform.instance as web.GoogleSignInPlugin)
.renderButton();
}
2 changes: 2 additions & 0 deletions packages/google_sign_in/google_sign_in/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
google_sign_in_platform_interface: ^2.4.0
google_sign_in_web: ^0.12.0
http: ^0.13.0

dev_dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="google-signin-client_id" content="159623150305-q05bbbtsutr02abhips3suj7hujfk4bg.apps.googleusercontent.com" />
<meta name="google-signin-client_id" content="your-client_id.apps.googleusercontent.com">
<title>Google Sign-in Example</title>
</head>
<body>
Expand Down
Loading