架構外 (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 流程,且 OAuth 用戶端類型為 Android 或 iOS,請改用建議的 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),或是搭配 REST 適用的 Google API Objective-C 用戶端程式庫使用擷取程式授權程式 (GTMFetcherAuthorizationProtocol)。

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

網頁應用程式

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

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

伺服器端 (離線) 存取權
如要使用伺服器端 (離線) 存取模式,您必須執行下列操作:
  • 架設伺服器並定義可公開存取的端點 (重新導向 URI),以接收授權碼。
  • 在 Google Cloud 控制台的「用戶端」頁面中,設定 重新導向 URI

下列程式碼片段顯示如何使用 Google Drive API 的 NodeJS 範例,在伺服器端列出使用者的 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 位址 (localhost127.0.0.1) 流程