带外 (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 API 控制台的 OAuth 权限请求页面中注册的支持电子邮件地址。

完成迁移过程主要分为以下两个步骤:
  1. 确定您是否受到影响。
  2. 如果您受到影响,请迁移到更安全的替代方案。

确定您是否受到影响

此弃用仅适用于正式版应用(即发布状态设为 正式版的应用)。此流程仍适用于 测试发布状态的应用。

在 的 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)

如果您确定您的应用使用的是 Android 或 iOS OAuth 客户端类型的 OOB 流程,则应改用推荐的 SDK(AndroidiOS)。

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

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

在 Android 上访问 Google API

客户端访问

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

  List 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 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() }

使用访问令牌调用 API,方法是将访问令牌添加到 REST 或 gRPC 请求 (Authorization: Bearer ACCESS_TOKEN) 的标头中,或者将提取器授权者 (GTMFetcherAuthorizationProtocol) 与 适用于 REST 的 Objective-C 的 Google API 客户端库搭配使用。

请参阅客户端访问指南,了解如何在客户端访问 Google 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 example below 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 应用

如果您确定您的应用正在针对 Web 应用使用 OOB 流程,则应改用我们的某个 Google API 客户端库。此处列出了适用于不同编程语言的客户端库。

借助这些库,您可以轻松访问 Google API 并处理对 Google 端点的所有调用。

服务器端(离线)访问
服务器端(离线)访问模式要求您执行以下操作:
  • 搭建服务器并定义一个可公开访问的端点(重定向 URI),以接收授权代码。
  • 在 的 中配置 重定向 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 地址 (localhost127.0.0.1) 流程