架構外 (OOB) 流程遷移指南

總覽

我們在 2022 年 2 月 16 日宣布,計劃採用更安全的 OAuth 流程,提升 Google OAuth 互動的安全性。本指南可協助您瞭解必要的變更和步驟,順利從 OAuth 頻外 (OOB) 流程遷移至支援的替代方案。

這項措施旨在防範與 Google OAuth 2.0 授權端點互動時,發生網路釣魚和應用程式冒用攻擊。

什麼是 OOB?

OAuth 頻外 (OOB) (也稱為手動複製及貼上選項) 是一種舊版流程,旨在支援沒有重新導向 URI 的已安裝用戶端,以便在使用者核准 OAuth 同意聲明要求後接受憑證。OOB 流程有遠端網路釣魚風險,用戶端必須改用其他方法,防範這項安全漏洞。

所有應用程式類型 (即網頁應用程式、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 流程搭配 Android 或 iOS OAuth 應用程式類型,請改用建議的 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()
}

使用存取權杖呼叫 API,方法是在 REST 或 gRPC 要求的標頭中加入存取權杖 (Authorization: Bearer <var>ACCESS_TOKEN</var>),或搭配適用於 Objective-C for REST 的 Google API 用戶端程式庫使用擷取器授權工具 (GTMFetcherAuthorizationProtocol)。

請參閱用戶端存取指南,瞭解如何在用戶端存取 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 指南。

網頁應用程式

如果您判斷應用程式使用網頁應用程式的 OOB 流程,請改用 Google API 用戶端程式庫。請參閱不同程式設計語言的 Google API 用戶端程式庫清單。

這些程式庫可簡化 Google API 的存取作業,並處理對 Google 端點的所有呼叫。

伺服器端 (離線) 存取權

如要使用伺服器端 (離線) 存取模式,請完成下列步驟:

下列程式碼片段顯示 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.
      });
    }
  }
}

請參閱伺服器端網頁應用程式指南,瞭解如何從伺服器端存取 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(...);
}

請參閱用戶端網頁應用程式指南,瞭解如何從用戶端存取 Google API。

電腦用戶端

如果您判斷應用程式在電腦用戶端上使用 OOB 流程,請改用回送 IP 位址 (localhost 或 127.0.0.1) 流程。