带外 (OOB) 流程迁移指南

概览

2022 年 2 月 16 日,我们宣布计划通过使用更安全的 OAuth 流程来提高 Google OAuth 互动的安全性。本指南可帮助您了解从 OAuth 带外 (OOB) 流程成功迁移到受支持的替代方案所需的更改和步骤。

此举是一项保护措施,旨在防止在与 Google 的 OAuth 2.0 授权端点互动期间发生钓鱼式攻击和应用冒充攻击。

什么是 OOB?

OAuth 带外 (OOB)(也称为手动复制和粘贴选项)是一种旧版流程,旨在支持在用户批准 OAuth 同意请求后没有重定向 URI 来接受凭据的已安装客户端。OOB 流程会带来远程钓鱼式攻击风险,客户端必须迁移到替代方法,以防范此漏洞。

所有客户端类型(即 Web 应用、Android、iOS、通用 Windows 平台 [UWP]、Chrome 应用、电视和输入受限的设备、桌面应用)都将弃用 OOB 流程。

重要合规日期

  • 2022 年 2 月 28 日 - 禁止在 OOB 流程中使用新的 OAuth
  • 2022 年 9 月 5 日 - 可能会针对不合规的 OAuth 请求向用户显示面向用户的警告消息
  • 2022 年 10 月 3 日 - 针对 2022 年 2 月 28 日之前创建的 OAuth 客户端,弃用 OOB 流程
  • 2023 年 1 月 31 日 - 所有现有客户端(包括豁免客户端)都将被屏蔽

对于不合规的请求,系统会向用户显示错误消息。该消息会向用户传达应用已被屏蔽的信息,同时显示您在 Google Cloud 控制台的 OAuth 权限请求页面中注册的支持邮箱。

完成迁移过程主要有两个步骤:

  1. 确定您是否受到影响。
  2. 如果您受到影响,请迁移到更安全的替代方案。

确定您是否受到影响

此弃用仅适用于正式版应用(即发布状态设为正式版的应用)。 对于发布状态为“测试”的应用,该流程将继续有效。

在 Google Cloud 控制台的 OAuth “品牌信息”页面中查看您的发布状态,如果您在发布状态为“正式版”的项目中使用 OOB 流程,请继续执行下一步。

如何确定您的应用是否在使用 OOB 流程

检查您的应用代码或传出网络调用(如果您的应用使用的是 OAuth 库),以确定您的应用发出的 Google OAuth 授权请求是否使用了 OOB 重定向 URI 值。

检查应用代码

查看应用代码中调用 Google OAuth 授权端点的部分,并确定 redirect_uri 参数是否具有以下任何值:

  • redirect_uri=urn:ietf:wg:oauth:2.0:oob
  • redirect_uri=urn:ietf:wg:oauth:2.0:oob:auto
  • redirect_uri=oob

示例 OOB 重定向流程请求如下所示:

https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
scope=<SCOPES>&
state=<STATE>&
redirect_uri=urn:ietf:wg:oauth:2.0:oob&
client_id=<CLIENT_ID>

检查出站网络调用

检查网络调用的方法因应用客户端类型而异。

在检查网络调用时,请查找发送到 Google OAuth 授权端点的请求,并确定 redirect_uri 参数是否具有以下任一值:

  • redirect_uri=urn:ietf:wg:oauth:2.0:oob
  • redirect_uri=urn:ietf:wg:oauth:2.0:oob:auto
  • redirect_uri=oob

示例 OOB 重定向流程请求如下所示:

https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
scope=<SCOPES>&
state=<STATE>&
redirect_uri=urn:ietf:wg:oauth:2.0:oob&
client_id=<CLIENT_ID>

迁移到安全的替代方案

移动客户端(Android 和 iOS)

如果您确定自己的应用使用的是 OOB 流程,且 OAuth 客户端类型为 Android 或 iOS,则应迁移到使用推荐的 SDK(Android、iOS)。

该 SDK 可帮助您访问 Google API,并处理对 Google 的 OAuth 2.0 授权端点的所有调用。

以下文档链接提供了有关如何使用推荐的 SDK 访问 Google API 而不使用 OOB 重定向 URI 的信息。

在 Android 上访问 Google API

客户端访问

以下示例展示了如何使用推荐的 Google Identity 服务 Android 库在 Android 客户端访问 Google API。

List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_APPDATA);
AuthorizationRequest authorizationRequest = AuthorizationRequest.builder().setRequestedScopes(requestedScopes).build();
Identity.getAuthorizationClient(activity)
        .authorize(authorizationRequest)
        .addOnSuccessListener(
            authorizationResult -> {
              if (authorizationResult.hasResolution()) {
                // Access needs to be granted by the user
                PendingIntent pendingIntent = authorizationResult.getPendingIntent();
                try {
                  startIntentSenderForResult(pendingIntent.getIntentSender(),
                      REQUEST_AUTHORIZE, null, 0, 0, 0, null);
                } catch (IntentSender.SendIntentException e) {
                  Log.e(TAG, "Couldn't start Authorization UI: " + e.getLocalizedMessage());
                }
              } else {
                // Access already granted, continue with user action
                saveToDriveAppFolder(authorizationResult);
              }
            })
        .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize", e));

将 authorizationResult 传递给您定义的方法,以将内容保存到用户的云端硬盘文件夹。authorizationResult 具有返回访问令牌的 getAccessToken() 方法。

服务器端(离线)访问

以下示例展示了如何在 Android 上的服务器端访问 Google API。

  List<Scopes> requestedScopes = Arrays.asList(DriveScopes.DRIVE_APPDATA);
    AuthorizationRequest authorizationRequest = AuthorizationRequest.builder()
    .requestOfflineAccess(webClientId)
            .setRequestedScopes(requestedScopes)
            .build();
    Identity.getAuthorizationClient(activity)
            .authorize(authorizationRequest)
            .addOnSuccessListener(
                authorizationResult -> {
                  if (authorizationResult.hasResolution()) {
                    // Access needs to be granted by the user
                    PendingIntent pendingIntent = authorizationResult.getPendingIntent();
                    try {
    startIntentSenderForResult(pendingIntent.getIntentSender(),
    REQUEST_AUTHORIZE, null, 0, 0, 0, null);
                    } catch (IntentSender.SendIntentException e) {
                    Log.e(TAG, "Couldn't start Authorization UI: " + e.getLocalizedMessage());
                    }
                  } else {
                    String authCode = authorizationResult.getServerAuthCode();
                  }
                })
            .addOnFailureListener(e -> Log.e(TAG, "Failed to authorize", e));

authorizationResult 具有 getServerAuthCode() 方法,该方法会返回授权代码,您可以将该代码发送到后端以获取访问令牌和刷新令牌。

在 iOS 应用中访问 Google API

客户端访问

以下示例展示了如何在 iOS 客户端访问 Google API。

user.authentication.do { authentication, error in
  guard error == nil else { return }
  guard let authentication = authentication else { return }
  
  // Get the access token to attach it to a REST or gRPC request.
  let accessToken = authentication.accessToken
  
  // Or, get an object that conforms to GTMFetcherAuthorizationProtocol for
  // use with GTMAppAuth and the Google APIs client library.
  let authorizer = authentication.fetcherAuthorizer()
}

通过在 REST 或 gRPC 请求 (Authorization: Bearer <var>ACCESS_TOKEN</var>) 的标头中添加访问令牌,或通过将提取器授权方 (GTMFetcherAuthorizationProtocol) 与适用于 Objective-C for REST 的 Google API 客户端库搭配使用,来使用访问令牌调用 API。

查看客户端访问指南,了解如何在客户端访问 Google API。

服务器端(离线)访问

以下示例展示了如何在服务器端访问 Google API 以支持 iOS 客户端。

GIDSignIn.sharedInstance.signIn(with: signInConfig, presenting: self) { user, error in
  guard error == nil else { return }
  guard let user = user else { return }
  
  // request a one-time authorization code that your server exchanges for
  // an access token and refresh token
  let authCode = user.serverAuthCode
}

查看服务器端访问指南,了解如何从服务器端访问 Google API。

Chrome 应用客户端

如果您确定应用在 Chrome 应用客户端上使用 OOB 流程,则应迁移到使用 Chrome Identity API。

以下示例展示了如何在不使用 OOB 重定向 URI 的情况下获取所有用户联系人。

window.onload = function() {
  document.querySelector('button').addEventListener('click', function() {

  
  // retrieve access token
  chrome.identity.getAuthToken({interactive: true}, function(token) {
  
  // ..........


  // the following example shows how to use a retrieved access token with an appropriate scope
  // to call the Google People API contactGroups.get endpoint

  fetch(
    'https://people.googleapis.com/v1/contactGroups/all?maxMembers=20&key=API_KEY',
    init)
    .then((response) => response.json())
    .then(function(data) {
      console.log(data)
    });
   });
 });
};

如需详细了解如何使用 Chrome Identity API 对用户进行身份验证和调用 Google 端点,请参阅 Chrome Identity API 指南。

Web 应用

如果您确定自己的应用正在使用 OOB 流程进行 Web 应用身份验证,则应迁移到使用我们的某个 Google API 客户端库。查看适用于不同编程语言的 Google API 客户端库列表。

这些库简化了对 Google API 的访问,并处理对 Google 端点的所有调用。

服务器端(离线)访问

服务器端(离线)访问模式要求您执行以下操作:

  • 启动服务器并定义一个可公开访问的端点(重定向 URI)来接收授权代码。
  • 在 Google Cloud 控制台的“客户端”页面中配置重定向 URI

以下代码段展示了一个 NodeJS 示例,该示例使用 Google Drive API 在服务器端列出用户的 Google 云端硬盘文件,而无需使用 OOB 重定向 URI。

async function main() {
  const server = http.createServer(async function (req, res) {

  if (req.url.startsWith('/oauth2callback')) {
    let q = url.parse(req.url, true).query;

    if (q.error) {
      console.log('Error:' + q.error);
    } else {
      
      // Get access and refresh tokens (if access_type is offline)
      let { tokens } = await oauth2Client.getToken(q.code);
      oauth2Client.setCredentials(tokens);

      // Example of using Google Drive API to list filenames in user's Drive.
      const drive = google.drive('v3');
      drive.files.list({
        auth: oauth2Client,
        pageSize: 10,
        fields: 'nextPageToken, files(id, name)',
      }, (err1, res1) => {
        // TODO(developer): Handle response / error.
      });
    }
  }
}

请参阅服务器端 Web 应用指南,了解如何从服务器端访问 Google API。

客户端访问

以下 JavaScript 代码段展示了如何使用 Google API 在客户端访问用户的日历活动。


// initTokenClient() initializes a new token client with your
// web app's client ID and the scope you need access to

const client = google.accounts.oauth2.initTokenClient({
  client_id: 'YOUR_GOOGLE_CLIENT_ID',
  scope: 'https://www.googleapis.com/auth/calendar.readonly',
  
  // callback function to handle the token response
  callback: (tokenResponse) => {
    if (tokenResponse && tokenResponse.access_token) { 
      gapi.client.setApiKey('YOUR_API_KEY');
      gapi.client.load('calendar', 'v3', listUpcomingEvents);
    }
  },
});

function listUpcomingEvents() {
  gapi.client.calendar.events.list(...);
}

查看客户端 Web 应用指南,了解如何从客户端访问 Google API。

桌面客户端

如果您确定自己的应用在桌面客户端上使用 OOB 流程,则应迁移到使用环回 IP 地址(localhost 或 127.0.0.1)流程。