管理 Google Chat 应用的精细 OAuth 权限

使用用户身份验证的聊天应用必须支持精细的 OAuth 权限,以允许用户授予所请求范围的子集。例如,用户可能会授予对其名称的访问权限,但拒绝授予对其日历的访问权限。

处理精细 OAuth 权限取决于您构建 Chat 应用的方式:

Apps 脚本

如果您使用 Apps 脚本构建 Chat 应用,Apps 脚本会自动处理细化 OAuth 权限。不过,请确保您的代码能够处理用户未授予所有请求范围的情况。具体方法取决于您的 Apps 脚本是使用 Apps 脚本扩展 Google Chat 的 Google Workspace 插件,还是使用 Apps 脚本和互动事件构建的独立 Chat 应用。

可扩展 Chat 的 Google Workspace 插件

如果您将 Chat 扩展应用构建为使用 Apps 脚本扩展 Google Chat 的 Google Workspace 插件,请按照在 Apps 脚本中处理 OAuth 精细权限中的说明进行操作。

独立 Apps 脚本 Chat 应用

如果您使用 Apps 脚本和互动事件构建 Chat 应用,则 在 Apps 脚本中处理精细的 OAuth 权限中的说明适用,但需要考虑以下事项:

ScriptApp.requireScopes 如果未授予指定范围,则停止脚本执行,但用户会在 Chat 中看到配置卡片,而不是 OAuth 权限请求页面。配置卡片始终会提示用户授予所有请求的范围,而不仅仅是未授予的范围。

如需提供各个授权范围级别的检查,请使用 ScriptApp.getAuthorizationInfo 检查授权情况,并在必要时使用私信请求授权。

以下示例展示了如何检查特定权限(例如日历访问权限),如果缺少该权限,则返回包含所需授权网址的私信。

Apps 脚本

/**
* Responds to a MESSAGE event in Google Chat.
* Checks for required permissions and if missing asks for them.
*
* @param {Object} event the event object from Chat
* @return {Object} JSON response
*/
function onMessage(event) {
  // Check if the script has the necessary permissions.
  // In this example, the script checks for the "calendar.events" scope.
  var requiredScopes = ['https://www.googleapis.com/auth/calendar.events'];
  var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL, requiredScopes);

  // If permissions are missing, return a message with the authorization URL.
  if (authInfo.getAuthorizationStatus() === ScriptApp.AuthorizationStatus.REQUIRED) {
    var authUrl = authInfo.getAuthorizationUrl();
    return {
      "text": "This action requires authorization. Please <" + authUrl + "|click here to authorize>.",
      "privateMessageViewer": {
        "name": event.user.name
      }
    };
  }

  // Permission granted; proceed with the application logic.
  // ...
}

HTTP 端点

如果您使用 HTTP 端点构建 Chat 应用,则该应用应支持精细的 OAuth 权限。

可扩展 Chat 的 Google Workspace 插件

如果您将 Chat 应用构建为 Google Workspace 插件(例如,如果该应用扩展了其他 Google Workspace 应用,如 Google 云端硬盘或 Gmail),请配置清单文件和代码以处理精细的 OAuth 权限:

  1. 在插件的清单文件中,将 granularOauthPermissionSupport 字段设置为 OPT_IN。如需详细了解 granularOauthPermissionSupport 字段,请参阅迁移到精细的 OAuth 权限流程

    JSON

    {
      "oauthScopes": [
        "https://www.googleapis.com/auth/chat.messages",
        "https://www.googleapis.com/auth/calendar.events"
      ],
      "addOns": {
        "common": {
          "name": "My Chat App",
          "logoUrl": "https://lh3.googleusercontent.com/..."
        },
        "chat": {},
        "httpOptions": {
          "granularOauthPermissionSupport": "OPT_IN"
        }
      }
    }
    
  2. 如需查看用户授予了哪些范围,请在代码中检查 authorizationEventObject.authorizedScopes 字段。如果缺少必需的范围,请返回 requesting_google_scopes 操作,以提示用户授予缺少的范围。

    Node.js

    // Check for authorized scopes.
    const authorizedScopes = req.body.authorizationEventObject.authorizedScopes || [];
    if (!authorizedScopes.includes('https://www.googleapis.com/auth/chat.messages')) {
      // Respond with a request for the missing scope.
      res.send({
        'requesting_google_scopes': {
          'scopes': ['https://www.googleapis.com/auth/chat.messages']
        }
      });
      return;
    }
    

    Python

    from flask import jsonify, request
    
    # Check for authorized scopes.
    event_data = request.get_json()
    authorized_scopes = event_data.get('authorizationEventObject', {}).get('authorizedScopes', [])
    if 'https://www.googleapis.com/auth/chat.messages' not in authorized_scopes:
        # Respond with a request for the missing scope.
        return jsonify({
            'requesting_google_scopes': {
                'scopes': ['https://www.googleapis.com/auth/chat.messages']
            }
        })
    

    Java

    import com.google.gson.JsonArray;
    import com.google.gson.JsonObject;
    import java.util.List;
    
    // Check for authorized scopes.
    List<String> authorizedScopes = event.getAuthorizationEventObject().getAuthorizedScopes();
    if (!authorizedScopes.contains("https://www.googleapis.com/auth/chat.messages")) {
      // Respond with a request for the missing scope.
      JsonObject requestingGoogleScopes = new JsonObject();
      JsonArray scopes = new JsonArray();
      scopes.add("https://www.googleapis.com/auth/chat.messages");
      requestingGoogleScopes.add("scopes", scopes);
    
      JsonObject response = new JsonObject();
      response.add("requesting_google_scopes", requestingGoogleScopes);
      return response.toString();
    }
    

    如需请求与该插件关联的所有范围,请将 all_scopes 设置为 true

    Node.js

    res.send({
      'requesting_google_scopes': { 'all_scopes': true }
    });
    

    Python

    from flask import jsonify
    
    return jsonify({
        'requesting_google_scopes': { 'all_scopes': True }
    })
    

    Java

    import com.google.gson.JsonObject;
    
    JsonObject requestingGoogleScopes = new JsonObject();
    requestingGoogleScopes.addProperty("all_scopes", true);
    
    JsonObject response = new JsonObject();
    response.add("requesting_google_scopes", requestingGoogleScopes);
    return response.toString();
    

如需查看详细说明,请参阅为 HTTP Google Workspace 加载项管理精细权限

独立 HTTP Chat 应用

如果您的 Chat 应用是独立的 HTTP 服务(而非 Google Workspace 加载项),则需要自行管理 OAuth 2.0 流程。

当您检索存储的令牌或交换授权代码时,请检查授予了哪些范围。如果缺少必需的范围,请提示用户授权。

Node.js

// 1. List authorized scopes.
const fs = require('fs');
const tokens = JSON.parse(fs.readFileSync('token.json'));
const grantedScopes = tokens.scope.split(' ');

// 2. Detect missing scopes.
const requiredScopes = ['https://www.googleapis.com/auth/chat.messages'];
const missingScopes = requiredScopes.filter(scope => !grantedScopes.includes(scope));

if (missingScopes.length > 0) {
  // 3. Request missing scopes.
  const authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: missingScopes,
    include_granted_scopes: true
  });
  res.redirect(authUrl);
}

// To request all scopes instead of just the missing ones:
const allScopesAuthUrl = oauth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: requiredScopes,
  include_granted_scopes: true
});

Python

from flask import redirect
from google.oauth2.credentials import Credentials

# 1. List authorized scopes.
credentials = Credentials.from_authorized_user_file('token.json')
granted_scopes = set(credentials.scopes)

# 2. Detect missing scopes.
required_scopes = {'https://www.googleapis.com/auth/chat.messages'}
missing_scopes = required_scopes - granted_scopes

if missing_scopes:
    # 3. Request missing scopes.
    flow.scope = list(missing_scopes)
    auth_url, _ = flow.authorization_url(
        access_type='offline',
        include_granted_scopes=True
    )
    return redirect(auth_url)

# To request all scopes instead of just the missing ones:
flow.scope = list(required_scopes)
all_scopes_auth_url, _ = flow.authorization_url(
    access_type='offline',
    include_granted_scopes='true'
)

Java

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeRequestUrl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

// 1. List authorized scopes.
// The "user" string is the user ID for which to load credentials.
Credential credential = flow.loadCredential("user");
Collection<String> grantedScopes = credential.getScopes();

// 2. Detect missing scopes.
// The `requiredScopes` variable contains a list of the OAuth scopes
// that your app requires to function. Define this variable with the
// scopes needed by your application.
List<String> requiredScopes = Arrays.asList("https://www.googleapis.com/auth/chat.messages");
List<String> missingScopes = new ArrayList<>();
for (String scope : requiredScopes) {
  if (!grantedScopes.contains(scope)) {
    missingScopes.add(scope);
  }
}

if (!missingScopes.isEmpty()) {
  // 3. Request missing scopes.
  GoogleAuthorizationCodeRequestUrl urlBuilder = new GoogleAuthorizationCodeRequestUrl(
      clientId, redirectUri, missingScopes)
      .setAccessType("offline")
      .set("include_granted_scopes", "true");
  String authUrl = urlBuilder.build();
  response.sendRedirect(authUrl);
}

// To request all scopes instead of just the missing ones:
GoogleAuthorizationCodeRequestUrl allScopesUrlBuilder = new GoogleAuthorizationCodeRequestUrl(
    clientId, redirectUri, requiredScopes)
    .setAccessType("offline")
    .set("include_granted_scopes", "true");
String allScopesAuthUrl = allScopesUrlBuilder.build();

如需了解详情,请参阅精细的 OAuth 权限