How to handle granular permissions

Overview

With granular permissions, consumers get more fine-grained control over what account data they choose to share with each app. They benefit both users and developers by providing greater control, transparency, and security. This guide will help you to understand the necessary changes and steps to successfully update your applications to handle granular permissions.

What is granular permission?

Imagine you develop a productivity app that requests both email and calendar scopes. Your users may want to use your application only for Google Calendar, but not Gmail. With granular OAuth permissions, users can choose to only grant Google Calendar permission but not Gmail. By letting users grant access to specific data, this minimizes data exposure, bolsters trust, and empowers users with privacy-first control over their digital life. It is important to design your application to handle such scenarios.

When more than one non-Sign-In scope is requested

Sign-In and non-Sign-In scopes

For applications that request both Sign-In and non-Sign-In scopes, users first see the consent page for Sign-In scopes. After users consent to share their basic identity information (name, email address, and profile photo), users will see a granular permission consent screen for the non-Sign-In scopes. In this case, the application must check what scopes are granted by the users and can't assume users grant all requested scopes. In the following example, the web application requests all three Sign-In scopes and a Google drive non-Sign-In scope. After users consent to the Sign-In scopes, users will see the granular permissions consent screen for the Google Drive permission:

Sign-In and non-Sign-In scopes

More than one non-Sign-In scope

A granular permission consent screen would be displayed to users when applications request more than one non-Sign-In scope. Users can select which permissions they want to approve to share with the application. The following is an example granular permission consent screen requesting access to user's Gmail messages and Google Calendar data:

More than one non-Sign-In scope

Determine if your applications are affected

Conduct a thorough review of all sections within your application where Google OAuth 2.0 authorization endpoints are utilized for permission requests. Pay attention to those that request multiple scopes as they activate granular permission consent screens presented to users. In such instances, make sure your code can handle the case where users only authorize some of the scopes.

How to determine if your application is using multiple scopes

Inspect your app code or the outgoing network call to determine if the Google OAuth 2.0 authorization requests your app makes will cause the granular permissions consent screen to be shown.

Inspect your application code

Review the sections of your application code where you are making calls to the Google OAuth authorization endpoints to request permission from users. If you use one of the Google API Client Libraries, you can often find what scopes your application requests in the client initialization steps. Some examples are shown in the following section. You should refer to the documentation of the SDKs your application uses to handle Google OAuth 2.0 to determine if your application is affected, using the guidance shown in the following examples as a reference.

Google Identity Services

The following Google Identity Services JavaScript library code snippet initializes the TokenClient with multiple non-Sign-In scopes. The granular permission consent screen would be displayed when the web app requests authorization from users.

const client = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly \
  https://www.googleapis.com/auth/contacts.readonly',
  callback: (response) => {
    ...
  },
});

Python

The following code snippet uses the google-auth-oauthlib.flow module to construct the authorization request; The scope parameter includes two non-Sign-In scopes. The granular permission consent screen would be displayed when the web application requests authorization from users.

import google.oauth2.credentials
import google_auth_oauthlib.flow

# Use the client_secret.json file to identify the application requesting
# authorization. The client ID (from that file) and access scopes are required.
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
    'client_secret.json',
    scopes=['https://www.googleapis.com/auth/calendar.readonly',
                    'https://www.googleapis.com/auth/contacts.readonly'])

Node.js

The code snippet following creates a google.auth.OAuth2 object, which defines the parameters in the authorization request whose scope parameter includes two non-Sign-In scopes. The granular permission consent screen would display when the web app requests authorization from users.

const {google} = require('googleapis');

/**
  * To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI
  * from the client_secret.json file. To get these credentials for your application, visit
  * https://console.cloud.google.com/apis/credentials.
  */
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for read-only Calendar and Contacts.
const scopes = [
  'https://www.googleapis.com/auth/calendar.readonly',
  'https://www.googleapis.com/auth/contacts.readonly']
];

// Generate a url that asks permissions
const authorizationUrl = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
  /** Pass in the scopes array defined above.
    * Alternatively, if only one scope is needed, you can pass a scope URL as a string */
  scope: scopes,
  // Enable incremental authorization. Recommended as best practices.
  include_granted_scopes: true
});

Inspect outgoing network call

The method for inspecting network calls will vary depending on your application client type.

While inspecting network calls, look for requests sent to the Google OAuth authorization endpoints and examine the scope parameter.

These values cause the granular permissions consent screen to be shown.

  • The scope parameter contains Sign-In scopes and non-Sign-In scopes.

    A following sample request contains all three Sign-In scopes and one non-Sign-In scope to view the metadata of user's Google Drive files:

    https://accounts.google.com/o/oauth2/v2/auth?
    access_type=offline&
    scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.metadata.readonly&
    include_granted_scopes=true&
    response_type=code&
    redirect_uri=YOUR_REDIRECT_URL&
    client_id=YOUR_CLIENT_ID
  • The scope parameter contains more than one non-Sign-In scope.

    A following sample request contains two non-Sign-In scopes to view the user's Google Drive metadata and manage specific Google Drive files:

  • https://accounts.google.com/o/oauth2/v2/auth?
    access_type=offline&
    scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.metadata.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.file&
    include_granted_scopes=true&
    response_type=code&
    redirect_uri=YOUR_REDIRECT_URL&
    client_id=YOUR_CLIENT_ID

Best practices to handle granular permissions

If you determine that your application needs to be updated to handle granular permissions, you should make necessary updates to your code to properly handle consent for multiple scopes. All applications should comply with the following best practices:

  1. Review the Google API Services: User Data Policy and make sure you comply with them.
  2. Request specific scopes that are needed for a task. You must comply with the Google OAuth 2.0 policy that you only request scopes that you need. You should avoid asking for multiple scopes at sign-in, unless it is essential for the core functionality of your app. Bundling several scopes together, especially for the first-time users unfamiliar with your application's features, can make it challenging for them to comprehend the need for these permissions. This may raise alarms and deter users from further engaging with your application.
  3. Provide justification to users before asking the authorization request. Clearly explain why your application needs the requested permission, what you'll do with the user's data, and how the user will benefit from approving the request. Our research indicates that these explanations increase user trust and engagement.
  4. Use incremental authorization whenever your application requests scopes to avoid having to manage multiple access tokens.
  5. Check which scopes users granted. When requesting multiple scopes at once, users may not grant all scopes your app requests. Your app should always check which scopes were granted by the user and handle any denial of scopes by disabling relevant features. Follow the Google OAuth 2.0 policies on handling consent for multiple scopes and only prompt the user for consent again once they have clearly indicated an intent to use the specific feature that requires the scope.

Update your application to handle granular permissions

Android applications

You should consult the documentation of SDKs you use to interact with Google OAuth 2.0 and update your application to handle granular permissions based on best practices.

If you use auth.api.signin SDK from Play Services to interact with Google OAuth 2.0, you can use requestPermissions function to request the smallest set of scopes needed, and the hasPermissions function to check which scopes the user granted when requesting granular permissions.

Chrome extension applications

You should use Chrome Identity API to work with Google OAuth 2.0 based on best practices.

The following example shows how to properly handle granular permissions.

manifest.json

The example manifest file declares two non-Sign-In scopes for the Chrome extension application.

{
  "name": "Example Chrome extension application",
  ...
  "permissions": [
      "identity"
    ],
  "oauth2" : {
      "client_id": "YOUR_CLIENT_ID",
      "scopes":["https://www.googleapis.com/auth/calendar.readonly",
                "https://www.googleapis.com/auth/contacts.readonly"]
  }
}

Incorrect Approach

All or nothing

Users click the button to initiate the authorization process. The code snippet assumes users are presented with an "all-or-nothing" consent screen for the two scopes specified in manifest.json file. It neglects to check which scopes users granted.

oauth.js

...
document.querySelector('button').addEventListener('click', function () {
  chrome.identity.getAuthToken({ interactive: true },
      function (token) {
          if (token === undefined) {
            // User didn't authorize both scopes.
            // Updating the UX and application accordingly
            ...
          } else {
            // User authorized both or one of the scopes.
            // It neglects to check which scopes users granted and assumes users granted all scopes.

            // Calling the APIs, etc.
            ...
          }
      });
});

Correct Approach

Smallest scopes

Select the smallest set of scopes needed

Application should only request the smallest set of scopes needed. It is recommended that your application requests one scope at a time when it is needed to complete a task.

In this example, it is assumed that both scopes declared in the manifest.json file are the smallest set of scopes needed. The oauth.js file uses Chrome Identity API to initiate the authorization process with Google. You should opt in to enable granular permissions, so users have greater control granting permissions to your application. Your application should properly handle the response from users by checking which scopes users authorize.

oauth.js

...
document.querySelector('button').addEventListener('click', function () {
  chrome.identity.getAuthToken({ interactive: true, enableGranularPermissions: true },
      function (token, grantedScopes) {
          if (token === undefined) {
            // User didn't authorize any scope.
            // Updating the UX and application accordingly
            ...
          } else {
            // User authorized the request. Now, check which scopes were granted.
            if (grantedScopes.includes('https://www.googleapis.com/auth/calendar.readonly'))
            {
              // User authorized Calendar read permission.
              // Calling the APIs, etc.
              ...
            }
            else
            {
              // User didn't authorize Calendar read permission.
              // Update UX and application accordingly
              ...
            }

            if (grantedScopes.includes('https://www.googleapis.com/auth/contacts.readonly'))
            {
              // User authorized Contacts read permission.
              // Calling the APIs, etc.
              ...
            }
            else
            {
              // User didn't authorize Contacts read permission.
              // Update UX and application accordingly
              ...
            }
          }
      });
});

iOS, iPadOS, and macOS applications

You should consult the documentation of SDKs you use to interact with Google OAuth 2.0 and update your application to handle granular permissions based on best practices.

If you use the Google Sign-In for iOS and macOS library to interact with Google OAuth 2.0, you should review the documentation on handling granular permissions.

Web applications

You should consult the documentation of SDKs you use to interact with Google OAuth 2.0 and update your application to handle granular permissions based on best practices.

Server-side (offline) access

The server-side (offline) access mode requires you to do the following:
  • Stand up a server and define a publicly accessible endpoint to receive the authorization code.
  • Configure the redirect URI of your public endpoint in the Credentials page of the Google Cloud console.

The following code snippet shows a NodeJS example requests two non-Sign-In scopes. Users will see the granular permission consent screen.

Incorrect Approach

All or nothing

Users are redirected to authorization URL. The code snippet assumes users are presented with an "all-or-nothing" consent screen for the two scopes specified in the scopes arrary. It neglects to check which scopes users granted.

main.js

...
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for two non-Sign-In scopes - Google Calendar and Contacts
const scopes = [
  'https://www.googleapis.com/auth/contacts.readonly',
  'https://www.googleapis.com/auth/calendar.readonly'
];

// Generate a url that asks permissions for the Google Calendar and Contacts scopes
const authorizationUrl = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
  // Pass in the scopes array defined above
  scope: scopes,
  // Enable incremental authorization. Recommended as best practices.
  include_granted_scopes: true
});

async function main() {
  const server = http.createServer(async function (req, res) {
    // Example on redirecting user to Google OAuth 2.0 server.
    if (req.url == '/') {
      res.writeHead(301, { "Location": authorizationUrl });
    }
    // Receive the callback from Google OAuth 2.0 server.
    if (req.url.startsWith('/oauth2callback')) {
      // Handle the Google OAuth 2.0 server response
      let q = url.parse(req.url, true).query;

      if (q.error) {
        // User didn't authorize both scopes.
        // Updating the UX and application accordingly
        ...
      } else {
        // User authorized both or one of the scopes.
        // It neglects to check which scopes users granted and assumes users granted all scopes.

        // Get access and refresh tokens (if access_type is offline)
        let { tokens } = await oauth2Client.getToken(q.code);
        // Calling the APIs, etc.
        ...
      }
    }
    res.end();
  }).listen(80);
}
Correct Approach

Smallest scope

Select the smallest set of scopes needed

Application should only request the smallest set of scopes needed. It is recommended that your application requests one scope at a time when it is needed to complete a task. Whenever your application requests scopes, it should use incremental authorization to avoid having to manage multiple access tokens.

If your application must request multiple non-Sign-In scopes, you should always use incremental authorization when requesting and check which scopes users granted.

In this example, it is assumed that both scopes stated are required for the app to properly function. You should opt in to enable granular permissions, so users have greater control granting permissions to your application. Your application should properly handle the response from users by checking which scopes they have authorized.

main.js

...
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for two non-Sign-In scopes - Google Calendar and Contacts
const scopes = [
  'https://www.googleapis.com/auth/contacts.readonly',
  'https://www.googleapis.com/auth/calendar.readonly'
];

// Generate a url that asks permissions for the Google Calendar and Contacts scopes
const authorizationUrl = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
  // Pass in the scopes array defined above
  scope: scopes,
  // Enable incremental authorization. Recommended as best practices.
  include_granted_scopes: true,
  // Set to true to enable more granular permissions for Google OAuth 2.0 client IDs created before 2019.
  // No effect for newer Google OAuth 2.0 client IDs, since more granular permissions is always enabled for them.
  enable_granular_consent: true
});

async function main() {
  const server = http.createServer(async function (req, res) {
    // Redirect users to Google OAuth 2.0 server.
    if (req.url == '/') {
      res.writeHead(301, { "Location": authorizationUrl });
    }
    // Receive the callback from Google OAuth 2.0 server.
    if (req.url.startsWith('/oauth2callback')) {
      // Handle the Google OAuth 2.0 server response
      let q = url.parse(req.url, true).query;

      if (q.error) {
        // User didn't authorize both scopes.
        // Updating the UX and application accordingly
        ...
      } else {
        // Get access and refresh tokens (if access_type is offline)
        let { tokens } = await oauth2Client.getToken(q.code);
        oauth2Client.setCredentials(tokens);

        // User authorized the request. Now, check which scopes were granted.
        if (tokens.scope.includes('https://www.googleapis.com/auth/calendar.readonly'))
        {
          // User authorized Calendar read permission.
          // Calling the APIs, etc.
          ...
        }
        else
        {
          // User didn't authorize Calendar read permission.
          // Calling the APIs, etc.
          ...
        }

        // Check which scopes user granted the permission to application
        if (tokens.scope.includes('https://www.googleapis.com/auth/contacts.readonly'))
        {
          // User authorized Contacts read permission.
          // Calling the APIs, etc.
          ...
        }
        else
        {
          // User didn't authorize Contacts read permission.
          // Update UX and application accordingly
          ...
        }
      }
    }
    res.end();
  }).listen(80);
}

Review the server-side web app guide on how to access Google APIs from server-based applications.

Client-side only access

  • For applications that use Google Identity Services JavaScript library to interact with Google OAuth 2.0, you should review this documentation on handling granular permissions.
  • For applications that directly make calls using JavaScript to Google OAuth 2.0 authorization endpoints, you should review this documentation on handling granular permissions.

Test your updated application on handling granular permissions

  1. Outline all the cases that users can respond to permission requests and the expected behavior from your application. For example, if the user only authorizes two out of three requested scopes, your application should behave accordingly.
  2. Test your application with granular permission enabled. There are two ways to enable granular permissions:
    1. Check the OAuth 2.0 consent screens of your application to see if granular permissions are already enabled for your application. You can also create a new Web, Android, or iOS Google OAuth 2.0 client ID through the Google Cloud console for testing purposes as granular permission is always enabled for them.
    2. Set the parameter enable_granular_consent to true when calling the Google OAuth authorization endpoints. Some SDKs have explicit support for this parameter. For others, check the documentation to see how you can add this parameter and its value manually. If your implementation doesn't support adding the parameter, you can create a new Web, Android, or iOS Google OAuth 2.0 client ID through the Google Cloud console for testing purposes only as stated in preceding point.