將 OAuth 2.0 用於網路伺服器應用程式

本文說明網路伺服器應用程式如何使用 Google API 用戶端程式庫或 Google OAuth 2.0 端點,實作 OAuth 2.0 授權機制來存取 Google API。

OAuth 2.0 可讓使用者與應用程式共用特定資料,同時保有使用者名稱、密碼和其他資訊的隱私。舉例來說,應用程式可以使用 OAuth 2.0 取得使用者權限,將檔案儲存在使用者的 Google 雲端硬碟中。

這個 OAuth 2.0 流程專用於使用者授權。適用於可儲存機密資訊及維護狀態的應用程式。只要取得適當授權,網路伺服器應用程式就能在使用者與應用程式互動時,或使用者離開應用程式後存取 API。

網路伺服器應用程式也經常使用 服務帳戶授權 API 要求,特別是在呼叫 Cloud API 存取專案資料 (而非使用者專屬資料) 時。網路伺服器應用程式可以搭配使用者授權使用服務帳戶。

用戶端程式庫

本頁的語言專屬範例使用 Google API 用戶端程式庫實作 OAuth 2.0 授權。如要執行程式碼範例,您必須先安裝所用語言的用戶端程式庫。

使用 Google API 用戶端程式庫處理應用程式的 OAuth 2.0 流程時,用戶端程式庫會執行許多原本需要由應用程式自行處理的操作。舉例來說,這項設定會決定應用程式何時可以使用或重新整理儲存的存取權杖,以及何時必須重新取得同意聲明。用戶端程式庫也會產生正確的重新導向網址,並協助實作重新導向處理常式,將授權碼換成存取權杖。

伺服器端應用程式適用的 Google API 用戶端程式庫支援下列語言:

必要條件

為專案啟用 API

任何呼叫 Google API 的應用程式,都必須在 API 控制台中啟用這些 API。

如要為專案啟用 API,請按照下列步驟操作:

  1. 在 Google API 控制台中開啟 API 程式庫。
  2. 按照系統說明選取專案,或建立新專案。
  3. API 程式庫會列出所有可用的 API,並按照產品系列及熱門程度分組。如果清單中找不到您想啟用的 API,請使用搜尋功能,或在所屬產品系列中點選「View All」(查看全部)。
  4. 選取要啟用的 API,然後按一下「啟用」按鈕。
  5. 如果系統顯示提示,請啟用帳單功能。
  6. 如果系統顯示提示,請詳閱並接受 API 的《服務條款》。

建立授權憑證

任何使用 OAuth 2.0 存取 Google API 的應用程式,都必須具備授權憑證,向 Google 的 OAuth 2.0 伺服器識別應用程式。下列步驟說明如何為專案建立憑證。接著,應用程式就能使用憑證,存取您為該專案啟用的 API。

  1. 前往「客戶」頁面。
  2. 按一下「Create Client」(建立用戶端)。
  3. 選取「網頁應用程式」類型。
  4. 填寫表單,然後按一下「建立」。使用 PHP、Java、Python、Ruby 和 .NET 等語言和框架的應用程式,必須指定授權的重新導向 URI。重新導向 URI 是 OAuth 2.0 伺服器可傳送回應的端點。這些端點必須遵守 Google 的驗證規則。

    如要進行測試,您可以指定參照本機電腦的 URI,例如 http://localhost:8080。本文件中的所有範例都使用 http://localhost:8080 做為重新導向 URI。

    建議您設計應用程式的授權端點,確保應用程式不會向網頁上的其他資源公開授權碼。

建立憑證後,請從 API 控制台下載 client_secret.json 檔案。請將檔案安全地儲存在只有應用程式可以存取的位置。

找出存取權範圍

範圍可讓應用程式只要求存取所需資源,同時讓使用者控管授予應用程式的存取權限。因此,要求的範圍數量與取得使用者同意的可能性之間,可能存在反向關係。

開始實作 OAuth 2.0 授權前,建議您先找出應用程式需要存取權限的範圍。

我們也建議應用程式透過漸進式授權程序要求存取授權範圍,讓應用程式在相關情境下要求存取使用者資料。這項最佳做法可協助使用者更輕鬆地瞭解應用程式需要所要求存取權的原因。

OAuth 2.0 API 範圍文件包含您可能用於存取 Google API 的完整範圍清單。

語言相關規定

如要執行本文中的任何程式碼範例,您必須具備 Google 帳戶、網際網路連線和網路瀏覽器。如果您使用的是其中一個 API 用戶端程式庫,也請參閱下列各節中的語言專屬規定。

PHP

如要執行本文中的 PHP 程式碼範例,您需要:

  • PHP 8.0 以上版本,並已安裝指令列介面 (CLI) 和 JSON 擴充功能。
  • Composer 依附元件管理工具。
  • 適用於 PHP 的 Google API 用戶端程式庫:

    composer require google/apiclient:^2.15.0

詳情請參閱「適用於 PHP 的 Google API 用戶端程式庫」。

Python

如要執行本文中的 Python 程式碼範例,您需要:

  • Python 3.7 以上版本
  • pip 套件管理工具。
  • Python 2.0 適用的 Google API 用戶端程式庫版本:
    pip install --upgrade google-api-python-client
  • google-auth、google-auth-oauthlib 和 google-auth-httplib2,用於使用者授權。
    pip install --upgrade google-auth google-auth-oauthlib google-auth-httplib2
  • Flask Python 網頁應用程式框架。
    pip install --upgrade flask
  • requests HTTP 程式庫。
    pip install --upgrade requests

如果您無法升級 Python 和相關遷移指南,請參閱 Google API Python 用戶端程式庫版本資訊。

小茹

如要執行本文件中的 Ruby 程式碼範例,您需要:

  • Ruby 2.6 以上版本
  • Ruby 專用 Google 驗證程式庫:

    gem install googleauth
  • Drive 和 Calendar Google API 的用戶端程式庫:

    gem install google-apis-drive_v3 google-apis-calendar_v3
  • Sinatra Ruby 網頁應用程式架構。

    gem install sinatra

Node.js

如要執行本文中的 Node.js 程式碼範例,您需要:

  • Node.js 的維護 LTS、有效 LTS 或目前版本。
  • Google API Node.js 用戶端:

    npm install googleapis crypto express express-session

HTTP/REST

您不需要安裝任何程式庫,就能直接呼叫 OAuth 2.0 端點。

取得 OAuth 2.0 存取權杖

下列步驟說明應用程式如何與 Google 的 OAuth 2.0 伺服器互動,取得使用者同意,代表使用者執行 API 要求。應用程式必須先取得這項同意聲明,才能執行需要使用者授權的 Google API 要求。

以下簡要說明這些步驟:

  1. 應用程式會識別所需的權限。
  2. 應用程式將使用者重新導向至 Google,並附上要求的權限清單。
  3. 使用者會決定是否要將權限授予您的應用程式。
  4. 應用程式會得知使用者的決定。
  5. 如果使用者授予要求的權限,應用程式就會擷取所需權杖,代表使用者提出 API 要求。

步驟 1:設定授權參數

第一步是建立授權要求。該要求會設定參數,用於識別您的應用程式,並定義系統會要求使用者授予應用程式的權限。

  • 如果您使用 Google 用戶端程式庫進行 OAuth 2.0 驗證和授權,請建立並設定定義這些參數的物件。
  • 如果您直接呼叫 Google OAuth 2.0 端點,則會產生網址,並在該網址上設定參數。

下列分頁定義了網頁伺服器應用程式支援的授權參數。此外,各語言專屬範例也說明如何使用用戶端程式庫或授權程式庫,設定用於指定這些參數的物件:

PHP

下列程式碼片段會建立 Google\Client() 物件,定義授權要求中的參數。

該物件會使用 client_secret.json 檔案中的資訊來識別您的應用程式。(如要進一步瞭解該檔案,請參閱建立授權憑證)。這個物件也會識別應用程式要求存取權限的範圍,以及應用程式授權端點的網址,該端點會處理 Google OAuth 2.0 伺服器的回應。最後,程式碼會設定選用的 access_type 和 include_granted_scopes 參數。

舉例來說,下列程式碼會要求使用者 Google 雲端硬碟中繼資料和日曆活動的唯讀離線存取權:

use Google\Client;

$client = new Client();

// Required, call the setAuthConfig function to load authorization credentials from
// client_secret.json file.
$client->setAuthConfig('client_secret.json');

// Required, to set the scope value, call the addScope function
$client->addScope([Google\Service\Drive::DRIVE_METADATA_READONLY, Google\Service\Calendar::CALENDAR_READONLY]);

// Required, call the setRedirectUri function to specify a valid redirect URI for the
// provided client_id
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php');

// Recommended, offline access will give you both an access and refresh token so that
// your app can refresh the access token without user interaction.
$client->setAccessType('offline');

// Recommended, call the setState function. Using a state value can increase your assurance that
// an incoming connection is the result of an authentication request.
$client->setState($sample_passthrough_value);

// Optional, if your application knows which user is trying to authenticate, it can use this
// parameter to provide a hint to the Google Authentication Server.
$client->setLoginHint('hint@example.com');

// Optional, call the setPrompt function to set "consent" will prompt the user for consent
$client->setPrompt('consent');

// Optional, call the setIncludeGrantedScopes function with true to enable incremental
// authorization
$client->setIncludeGrantedScopes(true);

Python

下列程式碼片段使用 google-auth-oauthlib.flow 模組建構授權要求。

程式碼會建構 Flow 物件,並使用您在建立授權憑證後下載的 client_secret.json 檔案中的資訊來識別應用程式。該物件也會識別應用程式要求存取權限的範圍,以及應用程式驗證端點的網址,該端點會處理 Google OAuth 2.0 伺服器的回應。最後,程式碼會設定選用的 access_type 和 include_granted_scopes 參數。

舉例來說,下列程式碼會要求使用者 Google 雲端硬碟中繼資料和日曆活動的唯讀離線存取權:

import google.oauth2.credentials
import google_auth_oauthlib.flow

# Required, call the from_client_secrets_file method to retrieve the client ID from a
# client_secret.json file. The client ID (from that file) and access scopes are required. (You can
# also use the from_client_config method, which passes the client configuration as it originally
# appeared in a client secrets file but doesn't access the file itself.)
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file('client_secret.json',
    scopes=['https://www.googleapis.com/auth/drive.metadata.readonly',
            'https://www.googleapis.com/auth/calendar.readonly'])

# Required, indicate where the API server will redirect the user after the user completes
# the authorization flow. The redirect URI is required. The value must exactly
# match one of the authorized redirect URIs for the OAuth 2.0 client, which you
# configured in the API Console. If this value doesn't match an authorized URI,
# you will get a 'redirect_uri_mismatch' error.
flow.redirect_uri = 'https://www.example.com/oauth2callback'

# Generate URL for request to Google's OAuth 2.0 server.
# Use kwargs to set optional request parameters.
authorization_url, state = flow.authorization_url(
    # Recommended, enable offline access so that you can refresh an access token without
    # re-prompting the user for permission. Recommended for web server apps.
    access_type='offline',
    # Optional, enable incremental authorization. Recommended as a best practice.
    include_granted_scopes='true',
    # Optional, if your application knows which user is trying to authenticate, it can use this
    # parameter to provide a hint to the Google Authentication Server.
    login_hint='hint@example.com',
    # Optional, set prompt to 'consent' will prompt the user for consent
    prompt='consent')

小茹

使用您建立的 client_secrets.json 檔案,在應用程式中設定用戶端物件。設定用戶端物件時,您需要指定應用程式需要存取的範圍,以及應用程式授權端點的網址,該端點會處理來自 OAuth 2.0 伺服器的回應。

舉例來說,下列程式碼會要求使用者 Google 雲端硬碟中繼資料和日曆活動的唯讀離線存取權:

require 'googleauth'
require 'googleauth/web_user_authorizer'
require 'googleauth/stores/redis_token_store'

require 'google/apis/drive_v3'
require 'google/apis/calendar_v3'

# Required, call the from_file method to retrieve the client ID from a
# client_secret.json file.
client_id = Google::Auth::ClientId.from_file('/path/to/client_secret.json')

# Required, scope value 
# Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar.
scope = ['Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY',
         'Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY']

# Required, Authorizers require a storage instance to manage long term persistence of
# access and refresh tokens.
token_store = Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new)

# Required, indicate where the API server will redirect the user after the user completes
# the authorization flow. The redirect URI is required. The value must exactly
# match one of the authorized redirect URIs for the OAuth 2.0 client, which you
# configured in the API Console. If this value doesn't match an authorized URI,
# you will get a 'redirect_uri_mismatch' error.
callback_uri = '/oauth2callback'

# To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI
# from the client_secret.json file. To get these credentials for your application, visit
# https://console.cloud.google.com/apis/credentials.
authorizer = Google::Auth::WebUserAuthorizer.new(client_id, scope,
                                                token_store, callback_uri)

應用程式會使用用戶端物件執行 OAuth 2.0 作業,例如產生授權要求網址,以及將存取權杖套用至 HTTP 要求。

Node.js

下列程式碼片段會建立 google.auth.OAuth2 物件,定義授權要求中的參數。

該物件會使用 client_secret.json 檔案中的資訊來識別應用程式。如要向使用者要求權限以擷取存取權杖,請將使用者重新導向至同意聲明頁面。如要建立同意聲明頁面網址,請按照下列步驟操作:

const {google} = require('googleapis');
const crypto = require('crypto');
const express = require('express');
const session = require('express-session');

/**
 * To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI
 * from the client_secret.json file. To get these credentials for your application, visit
 * https://console.cloud.google.com/apis/credentials.
 */
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar.
const scopes = [
  'https://www.googleapis.com/auth/drive.metadata.readonly',
  'https://www.googleapis.com/auth/calendar.readonly'
];

// Generate a secure random state value.
const state = crypto.randomBytes(32).toString('hex');

// Store state in the session
req.session.state = state;

// Generate a url that asks permissions for the Drive activity and Google Calendar scope
const authorizationUrl = oauth2Client.generateAuthUrl({
  // 'online' (default) or 'offline' (gets refresh_token)
  access_type: 'offline',
  /** Pass in the scopes array defined above.
    * Alternatively, if only one scope is needed, you can pass a scope URL as a string */
  scope: scopes,
  // Enable incremental authorization. Recommended as a best practice.
  include_granted_scopes: true,
  // Include the state parameter to reduce the risk of CSRF attacks.
  state: state
});

重要事項:系統只會在第一次授權時傳回 refresh_token。詳情請參閱 這篇文章。

HTTP/REST

Google 的 OAuth 2.0 端點位於 https://accounts.google.com/o/oauth2/v2/auth。這個端點只能透過 HTTPS 存取。系統會拒絕純 HTTP 連線。

Google 授權伺服器支援下列網路伺服器應用程式查詢字串參數:

參數
client_id 必要

應用程式的用戶端 ID。您可以在 Cloud 控制台的「Clients」(用戶端) 頁面找到這個值。

redirect_uri 必要

決定使用者完成授權流程後,API 伺服器會將使用者重新導向至何處。這個值必須與 OAuth 2.0 用戶端的其中一個已授權重新導向 URI 完全相符,您可以在用戶端的 Cloud 控制台「Clients」(用戶端) 頁面設定。如果這個值與所提供 client_id 的授權重新導向 URI 不符,您會收到 redirect_uri_mismatch 錯誤。

請注意,http 或 https 配置、大小寫和結尾斜線 (「/」) 都必須相符。

response_type 必要

決定 Google OAuth 2.0 端點是否傳回授權碼。

如果是網路伺服器應用程式,請將參數值設為 code。

scope 必要

以空格分隔的範圍清單,用於識別應用程式可代表使用者存取的資源。Google 會根據這些值,向使用者顯示同意畫面。

範圍可讓應用程式只要求存取所需資源,同時讓使用者控管授予應用程式的存取權。因此,要求的範圍數量與取得使用者同意的可能性成反比。

建議您盡可能讓應用程式在相關情境中要求授權範圍存取權。在相關情境中,使用漸進式授權要求存取使用者資料,可協助使用者瞭解應用程式為何需要所要求的存取權。

access_type 建議

指出應用程式是否能在使用者未開啟瀏覽器時,重新整理存取權杖。有效的參數值為 online (預設值) 和 offline。

如果應用程式需要在使用者未開啟瀏覽器時重新整理存取權杖,請將值設為 offline。這是本文稍後介紹的存取權杖重新整理方法。這個值會指示 Google 授權伺服器在應用程式首次將授權碼換成權杖時,傳回更新權杖和存取權杖。

state 建議

指定應用程式用來在授權要求和授權伺服器回應之間維持狀態的任何字串值。使用者同意或拒絕應用程式的存取要求後,伺服器會傳回您在 redirect_uri 的網址查詢元件 (?) 中,以 name=value 組合形式傳送的確切值。

這個參數有多種用途,例如將使用者導向應用程式中的正確資源、傳送 Nonce,以及防範跨網站偽造要求。由於您的 redirect_uri 可能會遭到猜測,因此使用 state 值可提高您的保證,確保傳入連線是驗證要求的結果。如果您產生隨機字串,或對 Cookie 的雜湊或其他擷取用戶端狀態的值進行編碼,則可驗證回應,進一步確保要求和回應源自同一個瀏覽器,防範跨網站偽造要求等攻擊。如需建立及確認 state 權杖的範例,請參閱 OpenID Connect 說明文件。

include_granted_scopes 選填

可讓應用程式使用漸進式授權,在情境中要求存取其他範圍。如果將這個參數的值設為 true,且授權要求獲得核准,新的存取權杖也會涵蓋使用者先前授予應用程式存取權的任何範圍。如需範例,請參閱漸進式授權一節。

login_hint 選填

如果應用程式知道嘗試驗證的使用者身分,就可以使用這個參數,向 Google 驗證伺服器提供提示。伺服器會使用提示,在登入表單中預先填入電子郵件欄位,或選取適當的多重登入工作階段,簡化登入流程。

將參數值設為電子郵件地址或 sub 識別資訊,這相當於使用者的 Google ID。

prompt 選填

以空格分隔、區分大小寫的提示清單,用於向使用者顯示。如未指定這個參數,系統只會在專案首次要求存取權時提示使用者。詳情請參閱「 提示重新同意」。

可能的值為:

none 請勿顯示任何驗證或同意畫面。不得與其他值一併指定。
consent 提示使用者提供同意聲明。
select_account 提示使用者選取帳戶。

步驟 2:重新導向至 Google 的 OAuth 2.0 伺服器

將使用者重新導向至 Google 的 OAuth 2.0 伺服器,啟動驗證和授權程序。通常,當應用程式首次需要存取使用者資料時,就會發生這種情況。如果是漸進式授權,當應用程式首次需要存取無權存取的額外資源時,也會執行這個步驟。

PHP

  1. 產生網址,向 Google 的 OAuth 2.0 伺服器要求存取權:
    $auth_url = $client->createAuthUrl();
  2. 將使用者重新導向至 $auth_url:
    header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));

Python

以下範例說明如何使用 Flask 網頁應用程式架構,將使用者重新導向至授權網址:

return flask.redirect(authorization_url)

小茹

  1. 產生網址,向 Google 的 OAuth 2.0 伺服器要求存取權:
    auth_uri = authorizer.get_authorization_url(request: request)
  2. 將使用者重新導向至 auth_uri。

Node.js

  1. 使用步驟 1 generateAuthUrl 方法產生的網址 authorizationUrl,向 Google 的 OAuth 2.0 伺服器要求存取權。
  2. 將使用者重新導向至 authorizationUrl。
    res.redirect(authorizationUrl);

HTTP/REST

重新導向至 Google 授權伺服器的範例

以下是網址範例,其中包含換行和空格,方便閱讀。

https://accounts.google.com/o/oauth2/v2/auth?
 scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly%20https%3A//www.googleapis.com/auth/calendar.readonly&
 access_type=offline&
 include_granted_scopes=true&
 response_type=code&
 state=state_parameter_passthrough_value&
 redirect_uri=https%3A//developers.google.com/oauthplayground&
 client_id=client_id

建立要求網址後,請將使用者重新導向至該網址。

Google 的 OAuth 2.0 伺服器會驗證使用者,並取得使用者同意,讓應用程式存取要求的範圍。系統會使用您指定的重新導向網址,將回應傳回應用程式。

步驟 3:Google 提示使用者提供同意聲明

在這個步驟中,使用者會決定是否要授予應用程式要求的存取權。在這個階段,Google 會顯示同意聲明視窗,其中會列出應用程式名稱、應用程式要求存取的 Google API 服務 (需要使用者的授權憑證),以及要授予的存取權範圍摘要。使用者可以同意授予應用程式要求的一或多個範圍存取權,也可以拒絕要求。

應用程式在這個階段不需要執行任何動作,只要等待 Google 的 OAuth 2.0 伺服器回應,指出是否已授予任何存取權。我們會在下一個步驟中說明該回應。

錯誤

向 Google 的 OAuth 2.0 授權端點發出要求時,可能會顯示面向使用者的錯誤訊息,而非預期的驗證和授權流程。常見的錯誤代碼和建議解決方法如下:

admin_policy_enforced

由於 Google Workspace 管理員的政策,Google 帳戶無法授權一或多個要求的範圍。請參閱 Google Workspace 管理員說明文章「 控管哪些第三方和內部應用程式可存取 Google Workspace 資料」,進一步瞭解管理員如何限制所有範圍或機密和受限範圍的存取權,直到明確授予 OAuth 用戶端 ID 存取權為止。

disallowed_useragent

授權端點顯示在 Google OAuth 2.0 政策不允許的嵌入式使用者代理程式中。

iOS 和 macOS 開發人員在 WKWebView 中開啟授權要求時,可能會遇到這項錯誤。開發人員應改用 iOS 程式庫,例如 Google 登入 iOS 版或 OpenID Foundation 的 AppAuth iOS 版。

如果 iOS 或 macOS 應用程式在內嵌的使用者代理程式中開啟一般網頁連結,且使用者從您的網站前往 Google 的 OAuth 2.0 授權端點,網頁程式開發人員可能會遇到這項錯誤。開發人員應允許一般連結在作業系統的預設連結處理常式中開啟,包括通用連結處理常式或預設瀏覽器應用程式。SFSafariViewController 程式庫也是支援的選項。

org_internal

要求中的 OAuth 用戶端 ID 屬於某個專案,該專案限制只有特定 Google Cloud 組織中的 Google 帳戶才能存取。如要進一步瞭解這個設定選項,請參閱「設定 OAuth 同意畫面」說明文章中的「使用者類型」一節。

invalid_client

OAuth 用戶端密鑰不正確。查看 OAuth 用戶端設定,包括用於這項要求的用戶端 ID 和密鑰。

deleted_client

用來提出要求的 OAuth 用戶端已刪除。刪除作業可以手動執行,也可以在用戶端未使用 時自動執行。刪除的用戶端可在刪除後的 30 天內還原。 瞭解詳情 。

invalid_grant

重新整理存取權杖或使用漸進式授權時,權杖可能已過期或失效。請再次驗證使用者,並徵求使用者同意取得新權杖。如果持續看到這則錯誤訊息,請確認應用程式設定正確無誤,且要求中使用的權杖和參數正確。否則,使用者帳戶可能已遭刪除或停用。

redirect_uri_mismatch

授權要求中傳遞的 redirect_uri 與 OAuth 用戶端 ID 的授權重新導向 URI 不符。在 Google Cloud 控制台的「Clients」(用戶端) 頁面中,查看已授權的重新導向 URI。

redirect_uri 參數可能參照已淘汰且不再支援的 OAuth 頻外 (OOB) 流程。請參閱遷移指南,更新整合功能。

invalid_request

您的要求有誤。可能原因如下:

  • 要求格式不正確
  • 要求缺少必要參數
  • 要求採用 Google 不支援的授權方法。確認 OAuth 整合採用建議的整合方法

步驟 4:處理 OAuth 2.0 伺服器回應

OAuth 2.0 伺服器會使用要求中指定的網址,回應應用程式的存取要求。

如果使用者核准了存取要求,回應內便會提供授權碼。如果使用者未核准要求,回應會包含錯誤訊息。傳回網路伺服器的授權碼或錯誤訊息會顯示在查詢字串中,如以下範例所示:

錯誤回應:

https://oauth2.example.com/auth?error=access_denied

授權碼回應:

https://oauth2.example.com/auth?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7

OAuth 2.0 伺服器回應範例

您可以點選下列範例網址來測試這個流程,該網址會要求唯讀存取權,以便查看 Google 雲端硬碟中的檔案中繼資料,以及查看 Google 日曆活動:

https://accounts.google.com/o/oauth2/v2/auth?
 scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly%20https%3A//www.googleapis.com/auth/calendar.readonly&
 access_type=offline&
 include_granted_scopes=true&
 response_type=code&
 state=state_parameter_passthrough_value&
 redirect_uri=https%3A//developers.google.com/oauthplayground&
 client_id=client_id

完成 OAuth 2.0 流程後,瀏覽器會將您重新導向至 OAuth 2.0 Playground,這項工具可用於測試 OAuth 流程。您會看到 OAuth 2.0 Playground 已自動擷取授權碼。

步驟 5:將授權碼換成更新和存取權杖

網路伺服器收到授權碼後,即可用授權碼交換存取權杖。

PHP

如要將授權碼換成存取權杖,請使用 fetchAccessTokenWithAuthCode 方法:

$access_token = $client->fetchAccessTokenWithAuthCode($_GET['code']);

Python

在回呼頁面上,使用 google-auth 程式庫驗證授權伺服器回應。接著,使用 flow.fetch_token 方法,將回應中的授權碼換成存取權杖:

state = flask.session['state']
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
    'client_secret.json',
    scopes=['https://www.googleapis.com/auth/drive.metadata.readonly'],
    state=state)
flow.redirect_uri = flask.url_for('oauth2callback', _external=True)

authorization_response = flask.request.url
flow.fetch_token(authorization_response=authorization_response)

# Store the credentials in browser session storage, but for security: client_id, client_secret,
# and token_uri are instead stored only on the backend server.
credentials = flow.credentials
flask.session['credentials'] = {
    'token': credentials.token,
    'refresh_token': credentials.refresh_token,
    'granted_scopes': credentials.granted_scopes}

小茹

在回呼頁面上,使用 googleauth 程式庫驗證授權伺服器回應。使用 authorizer.handle_auth_callback_deferred 方法儲存授權碼,並重新導向至原本要求授權的網址。這會將結果暫時存放在使用者工作階段中,延遲交換授權碼。

  target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(request)
  redirect target_url

Node.js

如要將授權碼換成存取權杖,請使用 getToken 方法:

const url = require('url');

// Receive the callback from Google's OAuth 2.0 server.
app.get('/oauth2callback', async (req, res) => {
  let q = url.parse(req.url, true).query;

  if (q.error) { // An error response e.g. error=access_denied
    console.log('Error:' + q.error);
  } else if (q.state !== req.session.state) { //check state value
    console.log('State mismatch. Possible CSRF attack');
    res.end('State mismatch. Possible CSRF attack');
  } else { // Get access and refresh tokens (if access_type is offline)

    let { tokens } = await oauth2Client.getToken(q.code);
    oauth2Client.setCredentials(tokens);
});

HTTP/REST

如要將授權碼換成存取權杖,請呼叫 https://oauth2.googleapis.com/token 端點,並設定下列參數:

欄位
client_id 從 Cloud 控制台「用戶端」頁面取得的用戶端 ID。
client_secret 選填

從 Cloud 控制台「用戶端」頁面取得的用戶端密鑰。

code 初始要求傳回的授權碼。
grant_type 根據 OAuth 2.0 規格定義,這個欄位的值必須設為 authorization_code。
redirect_uri Cloud 控制台「用戶端」頁面中,為指定 client_id 專案列出的其中一個重新導向 URI。

雖然 DPoP 為選用,但建議使用,以提高安全性。DPoP 的安全性取決於私密金鑰是否僅限於單一裝置使用。建議您以無法從裝置複製的方式儲存金鑰,例如使用 TPM、Secure Enclave 或其他硬體支援的金鑰儲存庫。如要使用 DPoP,應用程式必須為每個傳送至權杖端點的要求,產生新的專屬 DPoP 證明 JWT,並將其新增為 HTTP 要求標頭。

標頭 必填 說明
DPoP 選用 DPoP 證明是證明擁有私密金鑰的 JWT。這是標頭,不是參數。如果提供,傳回的權杖會繫結至這個金鑰。每個要求都必須產生新的專屬證明,且必須包含與要求相符的 htm (HTTP 方法) 和 htu (HTTP URI) 憑證。

以下程式碼片段為要求範例:

POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6Ik\
 VDIiwieCI6Imw4dEZyaHgtMzR0VjNoUklDUkRZOXpDa0RscEJoRjQyVVFVZldWQVdCR\
 nMiLCJ5IjoiOVZFNGpmX09rX282NHpiVFRsY3VOSmFqSG10NnY5VERWclUwQ2R2R1JE\
 QSIsImNydiI6IlAtMjU2In19.eyJqdGkiOiItQndDM0VTYzZhY2MybFRjIiwiaHRtIj\
 oiUE9TVCIsImh0dSI6Imh0dHBzOi8vc2VydmVyLmV4YW1wbGUuY29tL3Rva2VuIiwia\
 WF0IjoxNTYyMjYyNjE2fQ.2-GxA6T8lP4vfrg8v-FdWP0A0zdrj8igiMLvqRMUvwnQg\
 4PtFLbdLXiOSsX0x7NVY-FNyJK70nfbV37xRZT3Lg

code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7&
client_id=your_client_id&
redirect_uri=https%3A//developers.google.com/oauthplayground&
grant_type=authorization_code

建構 DPoP 證明

下列步驟說明如何使用 OpenSSL 從指令列建構 DPoP 證明:

  1. 產生 EC P-256 金鑰組:
    openssl ecparam -name prime256v1 -genkey -noout -out dpop_private.pem
    openssl ec -in dpop_private.pem -pubout -out dpop_public.pem
  2. 建立 DPoP 標頭:

    標頭必須包含 typ、alg 和 jwk (公開金鑰) 聲明。x 和 y 值是 EC 公開金鑰的 Base64Url 編碼座標。使用 Base64Url 編碼這個 JSON:

    {
      "typ":"dpop+jwt",
      "alg":"ES256",
      "jwk": {
        "kty":"EC",
        "x":"YOUR_PUBLIC_KEY_X",
        "y":"YOUR_PUBLIC_KEY_Y",
        "crv":"P-256"
      }
    }
  3. 建立 DPoP 酬載:

    酬載必須包含 jti (要求的專屬 ID)、htm (HTTP 方法,例如 POST)、htu (HTTP URI,例如 https://oauth2.googleapis.com/token) 和 iat (核發時間)。如果您在先前要求的回應中,從伺服器的 DPoP-Nonce 標頭收到 Nonce,則必須在 nonce 宣告中加入該 Nonce 值。nonce 憑證不一定要用於授權碼交換,只有在先前收到 DPoP-Nonce 標頭時才需要使用。使用 Base64Url 編碼這個 JSON:

    {
      "jti":"JTI_VALUE",
      "htm":"POST",
      "htu":"https://oauth2.googleapis.com/token",
      "iat":YOUR_JWT_ISSUED_TIME,
      "nonce":"SERVER_PROVIDED_NONCE"
    }

    jti 值會因廣告交易平台類型而異:

    • 如果是授權碼交換,jti 必須是授權碼的 Base64Url 編碼 SHA256 雜湊:"jti":"BASE64URL(SHA256(AUTHORIZATION_CODE))"。
    • 如果是更新權杖交換,jti 必須是每個要求的不重複 ID:"jti":"YOUR_UNIQUE_PER_REQUEST_IDENTIFIER"。
  4. 簽署證明:

    使用半形句號 (.) 串連編碼標頭和酬載,然後使用 ES256 以私密金鑰簽署結果。請注意,JWS 要求簽章必須採用原始 R | S 串連格式 (P-256 為 64 位元組)。如果直接使用 OpenSSL,則必須將預設的 ASN.1 DER 編碼簽章轉換為這個原始格式。

如果交換成功,系統會傳回包含權杖的 200 OK 回應。如果在交換期間使用有效的 DPoP 證明,Google 傳回的更新權杖會與金鑰 DPoP 繫結,但存取權杖不會與 DPoP 繫結。存取權杖會保留 Bearer 的 token_type,而非 DPoP。此外,Google 會在回應中傳回 DPoP-Nonce HTTP 標頭。用戶端必須快取這個 Nonce,並在後續要求的 DPoP 證明 nonce 宣告中加入這個 Nonce (例如將更新權杖換成新的存取權杖,或呼叫受 DPoP 保護的 API 時)。使用這個預先核發的 Nonce,即可避免下次要求時發生額外的往返失敗 (use_dpop_nonce)。

使用 DPoP 繫結更新權杖提出權杖交換要求時,必須附上 DPoP 證明。

如果預期出現的 DPoP 標頭遺失、無效,或證明使用的金鑰與繫結至權杖的金鑰不同,就會發生交換失敗。在這些情況下,伺服器會傳回 400 Bad Request 錯誤。如果 DPoP 證明含有不相符的 htm 或 htu 宣告、過期的 iat、重複使用的 jti 或無效簽章,Google 會傳回 invalid_dpop_proof 錯誤代碼。如果需要 DPoP nonce (例如在交換更新權杖期間),且 DPoP 證明缺少 nonce 宣告,或伺服器不接受 nonce 值 (例如已過期、已使用或不正確),Google 會傳回 use_dpop_nonce 錯誤代碼,以及包含新 nonce 的 DPoP-Nonce HTTP 標頭,供您在後續要求中使用。其他失敗情況可能會傳回 invalid_grant。

Google 會傳回 JSON 物件來回應這項要求,其中包含短期存取權杖和更新權杖。請注意,只有在應用程式向 Google 授權伺服器發出初始要求時,將 access_type 參數設為 offline,系統才會傳回更新權杖。

回應包含下列欄位:

欄位
access_token 應用程式傳送的權杖,用於授權 Google API 要求。
expires_in 存取權杖的剩餘生命週期 (以秒為單位)。
refresh_token 可用來取得新存取權杖的權杖。更新權杖在使用者撤銷存取權或更新權杖到期前都有效。如果使用 DPoP,更新權杖會繫結至用於簽署 DPoP 證明資料的私密金鑰。 同樣地,只有在向 Google 授權伺服器發出初始要求時,將 access_type 參數設為 offline,這項回應才會顯示這個欄位。
refresh_token_expires_in 更新權杖的剩餘效期 (以秒為單位)。只有在使用者授予限時存取權時,系統才會設定這個值。
scope access_token 授予的存取權範圍,以空格分隔、區分大小寫的字串清單表示。
token_type 傳回的權杖類型。即使使用 DPoP,這個值一律為 Bearer。

下列程式碼片段顯示使用 DPoP 時,成功的回應標頭和主體範例:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DPoP-Nonce: AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI

{
  "access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
  "expires_in": 3920,
  "token_type": "Bearer",
  "scope": "https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly",
  "refresh_token": "1//xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
}

錯誤

使用授權碼交換存取權杖時,您可能會遇到下列錯誤,而非預期的回應。本節列出常見的錯誤代碼和建議解決方案。

invalid_grant

提供的授權碼無效或格式錯誤。請重新啟動 OAuth 程序,再次提示使用者同意,藉此要求新的授權碼。

步驟 6:檢查使用者授予的範圍

要求多項權限 (範圍) 時,使用者可能不會授予應用程式所有權限。應用程式必須驗證實際授予的範圍,並妥善處理部分權限遭拒的情況,通常是停用依賴這些遭拒範圍的功能。

不過也有例外狀況。如果 Google Workspace Enterprise 應用程式具有全網域授權,或標示為可信任,就會略過精細權限同意畫面。使用者不會看到這類應用程式的精細權限同意畫面。而是會取得所有要求的範圍,或完全無法取得。

詳情請參閱「如何處理精細權限」。

PHP

如要檢查使用者授予的範圍,請使用 getGrantedScope() 方法:

// Space-separated string of granted scopes if it exists, otherwise null.
$granted_scopes = $client->getOAuth2Service()->getGrantedScope();

// Determine which scopes user granted and build a dictionary
$granted_scopes_dict = [
  'Drive' => str_contains($granted_scopes, Google\Service\Drive::DRIVE_METADATA_READONLY),
  'Calendar' => str_contains($granted_scopes, Google\Service\Calendar::CALENDAR_READONLY)
];

Python

傳回的 credentials 物件具有 granted_scopes 屬性,這是使用者授予應用程式的範圍清單。

credentials = flow.credentials
flask.session['credentials'] = {
    'token': credentials.token,
    'refresh_token': credentials.refresh_token,
    'granted_scopes': credentials.granted_scopes}

下列函式會檢查使用者授予應用程式的範圍。

def check_granted_scopes(credentials):
  features = {}
  if 'https://www.googleapis.com/auth/drive.metadata.readonly' in credentials['granted_scopes']:
    features['drive'] = True
  else:
    features['drive'] = False

  if 'https://www.googleapis.com/auth/calendar.readonly' in credentials['granted_scopes']:
    features['calendar'] = True
  else:
    features['calendar'] = False

  return features

小茹

一次要求多個範圍時,請透過 credentials 物件的 scope 屬性,檢查哪些範圍已獲得授權。

# User authorized the request. Now, check which scopes were granted.
if credentials.scope.include?(Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY)
  # User authorized read-only Drive activity permission.
  # Calling the APIs, etc
else
  # User didn't authorize read-only Drive activity permission.
  # Update UX and application accordingly
end

# Check if user authorized Calendar read permission.
if credentials.scope.include?(Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY)
  # User authorized Calendar read permission.
  # Calling the APIs, etc.
else
  # User didn't authorize Calendar read permission.
  # Update UX and application accordingly
end

Node.js

一次要求多個範圍時,請透過 tokens 物件的 scope 屬性,檢查哪些範圍已獲得授權。

// User authorized the request. Now, check which scopes were granted.
if (tokens.scope.includes('https://www.googleapis.com/auth/drive.metadata.readonly'))
{
  // User authorized read-only Drive activity permission.
  // Calling the APIs, etc.
}
else
{
  // User didn't authorize read-only Drive activity permission.
  // Update UX and application accordingly
}

// Check if user authorized Calendar read permission.
if (tokens.scope.includes('https://www.googleapis.com/auth/calendar.readonly'))
{
  // User authorized Calendar read permission.
  // Calling the APIs, etc.
}
else
{
  // User didn't authorize Calendar read permission.
  // Update UX and application accordingly
}

HTTP/REST

如要檢查使用者是否已授予應用程式特定範圍的存取權,請查看存取權杖回應中的 scope 欄位。access_token 授予的存取權杖範圍,以空格分隔、區分大小寫的字串清單表示。

舉例來說,下列存取權杖回應範例表示使用者已授予應用程式唯讀雲端硬碟活動和日曆活動權限:

  {
    "access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
    "expires_in": 3920,
    "token_type": "Bearer",
    "scope": "https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly",
    "refresh_token": "1//xEoDL4iW3cxlI7yDbSRFYNG01kVKM2C-259HOF2aQbI"
  }

呼叫 Google API

PHP

請完成下列步驟,使用存取權杖呼叫 Google API:

  1. 如要將存取權杖套用至新的 Google\Client 物件 (例如將存取權杖儲存在使用者工作階段中),請使用 setAccessToken 方法:
    $client->setAccessToken($access_token);
  2. 為要呼叫的 API 建構服務物件。如要建構服務物件,請將已授權的 Google\Client 物件提供給要呼叫的 API 建構函式。舉例來說,如要呼叫 Drive API,請按照下列步驟操作:
    $drive = new Google\Service\Drive($client);
  3. 使用 服務物件提供的介面,向 API 服務提出要求。舉例來說,如要列出已驗證使用者 Google 雲端硬碟中的檔案,請執行下列操作:
    $files = $drive->files->listFiles(array());

Python

取得存取權杖後,應用程式就能使用該權杖,代表特定使用者帳戶或服務帳戶授權 API 要求。使用使用者專屬的授權憑證,為要呼叫的 API 建構服務物件,然後使用該物件發出已授權的 API 要求。

  1. 為要呼叫的 API 建構服務物件。如要建構服務物件,請呼叫 googleapiclient.discovery 程式庫的 build 方法,並提供 API 的名稱和版本,以及使用者憑證: 舉例來說,如要呼叫 Drive API 第 3 版:
    from googleapiclient.discovery import build
    
    drive = build('drive', 'v2', credentials=credentials)
  2. 使用服務物件提供的介面,向 API 服務提出要求。舉例來說,如要列出已驗證使用者 Google 雲端硬碟中的檔案,請執行下列操作:
    files = drive.files().list().execute()

小茹

取得存取權杖後,應用程式就能使用該權杖,代表指定的使用者帳戶或服務帳戶提出 API 要求。使用使用者專屬的授權憑證,為要呼叫的 API 建構服務物件,然後使用該物件發出已授權的 API 要求。

  1. 為要呼叫的 API 建構服務物件。舉例來說,如要呼叫第 3 版的 Drive API,請使用以下程式碼:
    drive = Google::Apis::DriveV3::DriveService.new
  2. 設定服務的憑證:
    drive.authorization = credentials
  3. 使用服務物件提供的介面,向 API 服務提出要求。舉例來說,如要列出已驗證使用者 Google 雲端硬碟中的檔案,請執行下列操作:
    files = drive.list_files

或者,您也可以在方法中提供 options 參數,逐一授權:

files = drive.list_files(options: { authorization: credentials })

Node.js

取得存取權杖並設定為 OAuth2 物件後,請使用該物件呼叫 Google API。應用程式可以使用該權杖,代表特定使用者帳戶或服務帳戶授權 API 要求。為要呼叫的 API 建構服務物件。舉例來說,下列程式碼會使用 Google Drive API 列出使用者雲端硬碟中的檔案名稱。

const { google } = require('googleapis');

// 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) => {
  if (err1) return console.log('The API returned an error: ' + err1);
  const files = res1.data.files;
  if (files.length) {
    console.log('Files:');
    files.map((file) => {
      console.log(`${file.name} (${file.id})`);
    });
  } else {
    console.log('No files found.');
  }
});

HTTP/REST

應用程式取得存取權杖後,如果已獲得 API 所需的存取範圍,您就能使用權杖,代表特定使用者帳戶呼叫 Google API。如要這麼做,請在向 API 提出的要求中加入 access_token 查詢參數或 Authorization HTTP 標頭 Bearer 值,藉此加入存取權杖。建議您盡可能使用 HTTP 標頭,因為查詢字串通常會顯示在伺服器記錄中。在大多數情況下,您可以使用用戶端程式庫設定對 Google API 的呼叫 (例如呼叫 Drive Files API)。

您可以在 OAuth 2.0 Playground 試用所有 Google API 並查看其範圍。

HTTP GET 範例

使用 Authorization: Bearer HTTP 標頭呼叫 drive.files 端點 (Drive Files API) 的範例如下。請注意,您必須指定自己的存取權杖:

GET /drive/v2/files HTTP/1.1
Host: www.googleapis.com
Authorization: Bearer access_token

以下是使用 access_token 查詢字串參數,為已驗證使用者呼叫相同 API 的範例:

GET https://www.googleapis.com/drive/v2/files?access_token=access_token

curl 範例

您可以使用 curl 指令列應用程式測試這些指令。以下是使用 HTTP 標頭選項 (建議) 的範例:

curl -H "Authorization: Bearer access_token" https://www.googleapis.com/drive/v2/files

或者,您也可以使用查詢字串參數選項:

curl https://www.googleapis.com/drive/v2/files?access_token=access_token

完整範例

以下範例會在使用者驗證並同意應用程式存取其雲端硬碟中繼資料後,列印使用者 Google 雲端硬碟中 JSON 格式的檔案清單。

PHP

如要執行這個範例,請按照下列步驟操作:

  1. 在 API 控制台中,將本機電腦的網址新增至重新導向網址清單。例如,新增 http://localhost:8080。
  2. 建立新目錄並切換至該目錄。例如:
    mkdir ~/php-oauth2-example
    cd ~/php-oauth2-example
  3. 使用 Composer 安裝適用於 PHP 的 Google API 用戶端程式庫:
    composer require google/apiclient:^2.15.0
  4. 建立 index.php 和 oauth2callback.php 檔案,並加入以下內容。
  5. 使用 PHP 內建的測試網路伺服器執行範例:
    php -S localhost:8080 ~/php-oauth2-example

index.php

<?php
require_once __DIR__.'/vendor/autoload.php';

session_start();

$client = new Google\Client();
$client->setAuthConfig('client_secret.json');

// User granted permission as an access token is in the session.
if (isset($_SESSION['access_token']) && $_SESSION['access_token'])
{
  $client->setAccessToken($_SESSION['access_token']);
  
  // Check if user granted Drive permission
  if ($_SESSION['granted_scopes_dict']['Drive']) {
    echo "Drive feature is enabled.";
    echo "</br>";
    $drive = new Drive($client);
    $files = array();
    $response = $drive->files->listFiles(array());
    foreach ($response->files as $file) {
        echo "File: " . $file->name . " (" . $file->id . ")";
        echo "</br>";
    }
  } else {
    echo "Drive feature is NOT enabled.";
    echo "</br>";
  }

   // Check if user granted Calendar permission
  if ($_SESSION['granted_scopes_dict']['Calendar']) {
    echo "Calendar feature is enabled.";
    echo "</br>";
  } else {
    echo "Calendar feature is NOT enabled.";
    echo "</br>";
  }
}
else
{
  // Redirect users to outh2call.php which redirects users to Google OAuth 2.0
  $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php';
  header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}
?>

oauth2callback.php

<?php
require_once __DIR__.'/vendor/autoload.php';

session_start();

$client = new Google\Client();

// Required, call the setAuthConfig function to load authorization credentials from
// client_secret.json file.
$client->setAuthConfigFile('client_secret.json');
$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST']. $_SERVER['PHP_SELF']);

// Required, to set the scope value, call the addScope function.
$client->addScope([Google\Service\Drive::DRIVE_METADATA_READONLY, Google\Service\Calendar::CALENDAR_READONLY]);

// Enable incremental authorization. Recommended as a best practice.
$client->setIncludeGrantedScopes(true);

// Recommended, offline access will give you both an access and refresh token so that
// your app can refresh the access token without user interaction.
$client->setAccessType("offline");

// Generate a URL for authorization as it doesn't contain code and error
if (!isset($_GET['code']) && !isset($_GET['error']))
{
  // Generate and set state value
  $state = bin2hex(random_bytes(16));
  $client->setState($state);
  $_SESSION['state'] = $state;

  // Generate a url that asks permissions.
  $auth_url = $client->createAuthUrl();
  header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
}

// User authorized the request and authorization code is returned to exchange access and
// refresh tokens.
if (isset($_GET['code']))
{
  // Check the state value
  if (!isset($_GET['state']) || $_GET['state'] !== $_SESSION['state']) {
    die('State mismatch. Possible CSRF attack.');
  }

  // Get access and refresh tokens (if access_type is offline)
  $token = $client->fetchAccessTokenWithAuthCode($_GET['code']);

  /** Save access and refresh token to the session variables.
    * ACTION ITEM: In a production app, you likely want to save the
    *              refresh token in a secure persistent storage instead. */
  $_SESSION['access_token'] = $token;
  $_SESSION['refresh_token'] = $client->getRefreshToken();
  
  // Space-separated string of granted scopes if it exists, otherwise null.
  $granted_scopes = $client->getOAuth2Service()->getGrantedScope();

  // Determine which scopes user granted and build a dictionary
  $granted_scopes_dict = [
    'Drive' => str_contains($granted_scopes, Google\Service\Drive::DRIVE_METADATA_READONLY),
    'Calendar' => str_contains($granted_scopes, Google\Service\Calendar::CALENDAR_READONLY)
  ];
  $_SESSION['granted_scopes_dict'] = $granted_scopes_dict;
  
  $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/';
  header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL));
}

// An error response e.g. error=access_denied
if (isset($_GET['error']))
{
  echo "Error: ". $_GET['error'];
}
?>

Python

本範例使用 Flask 架構。這個指令會在 http://localhost:8080 執行網頁應用程式,讓您測試 OAuth 2.0 流程。前往該網址後,應該會看到五個連結:

  • 呼叫 Drive API:如果使用者授予權限,這個連結會指向嘗試執行範例 API 要求的頁面。如有需要,系統會啟動授權流程。如果成功,頁面會顯示 API 回應。
  • 呼叫 Calendar API 的模擬頁面:這個連結指向模擬頁面,如果使用者授予權限,該頁面會嘗試執行範例 Calendar API 要求。如有需要,系統會啟動授權流程。如果成功,頁面會顯示 API 回應。
  • 直接測試驗證流程:這個連結指向的頁面會嘗試引導使用者完成授權流程。應用程式會要求權限,代表使用者提交授權的 API 要求。
  • 撤銷目前憑證:這個連結指向的頁面會 撤銷使用者已授予應用程式的權限。
  • 清除 Flask 工作階段憑證:這個連結會清除儲存在 Flask 工作階段中的授權憑證。這樣一來,您就能瞭解如果已授予應用程式權限的使用者嘗試在新工作階段中執行 API 要求,會發生什麼情況。此外,如果使用者撤銷授予應用程式的權限,而應用程式仍嘗試使用已撤銷的存取權杖授權要求,您也可以查看應用程式會收到的 API 回應。
# -*- coding: utf-8 -*-

import os
import flask
import json
import requests

import google.oauth2.credentials
import google_auth_oauthlib.flow
import googleapiclient.discovery

# This variable specifies the name of a file that contains the OAuth 2.0
# information for this application, including its client_id and client_secret.
CLIENT_SECRETS_FILE = "client_secret.json"

# The OAuth 2.0 access scope allows for access to the
# authenticated user's account and requires requests to use an SSL connection.
SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly',
          'https://www.googleapis.com/auth/calendar.readonly']
API_SERVICE_NAME = 'drive'
API_VERSION = 'v2'

app = flask.Flask(__name__)
# Note: A secret key is included in the sample so that it works.
# If you use this code in your application, replace this with a truly secret
# key. See https://flask.palletsprojects.com/quickstart/#sessions.
app.secret_key = 'REPLACE ME - this value is here as a placeholder.'

@app.route('/')
def index():
  return print_index_table()

@app.route('/drive')
def drive_api_request():
  if 'credentials' not in flask.session:
    return flask.redirect('authorize')

  features = flask.session['features']

  if features['drive']:
    # Load client secrets from the server-side file.
    with open(CLIENT_SECRETS_FILE, 'r') as f:
        client_config = json.load(f)['web']

    # Load user-specific credentials from browser session storage.
    session_credentials = flask.session['credentials']

    # Reconstruct the credentials object.
    credentials = google.oauth2.credentials.Credentials(
        refresh_token=session_credentials.get('refresh_token'),
        scopes=session_credentials.get('granted_scopes'),
        token=session_credentials.get('token'),
        client_id=client_config.get('client_id'),
        client_secret=client_config.get('client_secret'),
        token_uri=client_config.get('token_uri'))

    drive = googleapiclient.discovery.build(
        API_SERVICE_NAME, API_VERSION, credentials=credentials)

    files = drive.files().list().execute()

    # Save credentials back to session in case access token was refreshed.
    flask.session['credentials'] = credentials_to_dict(credentials)

    return flask.jsonify(**files)
  else:
    # User didn't authorize read-only Drive activity permission.
    return '<p>Drive feature is not enabled.</p>'

@app.route('/calendar')
def calendar_api_request():
  if 'credentials' not in flask.session:
    return flask.redirect('authorize')

  features = flask.session['features']

  if features['calendar']:
    # User authorized Calendar read permission.
    # Calling the APIs, etc.
    return ('<p>User granted the Google Calendar read permission. '+
            'This sample code does not include code to call Calendar</p>')
  else:
    # User didn't authorize Calendar read permission.
    # Update UX and application accordingly
    return '<p>Calendar feature is not enabled.</p>'

@app.route('/authorize')
def authorize():
  # Create flow instance to manage the OAuth 2.0 Authorization Grant Flow steps.
  flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
      CLIENT_SECRETS_FILE, scopes=SCOPES)

  # The URI created here must exactly match one of the authorized redirect URIs
  # for the OAuth 2.0 client, which you configured in the API Console. If this
  # value doesn't match an authorized URI, you will get a 'redirect_uri_mismatch'
  # error.
  flow.redirect_uri = flask.url_for('oauth2callback', _external=True)

  authorization_url, state = flow.authorization_url(
      # Enable offline access so that you can refresh an access token without
      # re-prompting the user for permission. Recommended for web server apps.
      access_type='offline',
      # Enable incremental authorization. Recommended as a best practice.
      include_granted_scopes='true')

  # Store the state so the callback can verify the auth server response.
  flask.session['state'] = state

  return flask.redirect(authorization_url)

@app.route('/oauth2callback')
def oauth2callback():
  # Specify the state when creating the flow in the callback so that it can
  # verified in the authorization server response.
  state = flask.session['state']

  flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
      CLIENT_SECRETS_FILE, scopes=SCOPES, state=state)
  flow.redirect_uri = flask.url_for('oauth2callback', _external=True)

  # Use the authorization server's response to fetch the OAuth 2.0 tokens.
  authorization_response = flask.request.url
  flow.fetch_token(authorization_response=authorization_response)

  # Store credentials in the session.
  # ACTION ITEM: In a production app, you likely want to save these
  #              credentials in a persistent database instead.
  credentials = flow.credentials
  
  credentials = credentials_to_dict(credentials)
  flask.session['credentials'] = credentials

  # Check which scopes user granted
  features = check_granted_scopes(credentials)
  flask.session['features'] = features
  return flask.redirect('/')
  
@app.route('/revoke')
def revoke():
  if 'credentials' not in flask.session:
    return ('You need to <a href="/authorize">authorize</a> before ' +
            'testing the code to revoke credentials.')

  # Load client secrets from the server-side file.
  with open(CLIENT_SECRETS_FILE, 'r') as f:
      client_config = json.load(f)['web']

  # Load user-specific credentials from the session.
  session_credentials = flask.session['credentials']

  # Reconstruct the credentials object.
  credentials = google.oauth2.credentials.Credentials(
      refresh_token=session_credentials.get('refresh_token'),
      scopes=session_credentials.get('granted_scopes'),
      token=session_credentials.get('token'),
      client_id=client_config.get('client_id'),
      client_secret=client_config.get('client_secret'),
      token_uri=client_config.get('token_uri'))

  revoke = requests.post('https://oauth2.googleapis.com/revoke',
      params={'token': credentials.token},
      headers = {'content-type': 'application/x-www-form-urlencoded'})

  status_code = getattr(revoke, 'status_code')
  if status_code == 200:
    # Clear the user's session credentials after successful revocation
    if 'credentials' in flask.session:
        del flask.session['credentials']
        del flask.session['features']
    return('Credentials successfully revoked.' + print_index_table())
  else:
    return('An error occurred.' + print_index_table())

@app.route('/clear')
def clear_credentials():
  if 'credentials' in flask.session:
    del flask.session['credentials']
  return ('Credentials have been cleared.<br><br>' +
          print_index_table())

def credentials_to_dict(credentials):
  return {'token': credentials.token,
          'refresh_token': credentials.refresh_token,
          'granted_scopes': credentials.granted_scopes}

def check_granted_scopes(credentials):
  features = {}
  if 'https://www.googleapis.com/auth/drive.metadata.readonly' in credentials['granted_scopes']:
    features['drive'] = True
  else:
    features['drive'] = False

  if 'https://www.googleapis.com/auth/calendar.readonly' in credentials['granted_scopes']:
    features['calendar'] = True
  else:
    features['calendar'] = False

  return features

def print_index_table():
  return ('<table>' + 
          '<tr><td><a href="/authorize">Test the auth flow directly</a></td>' +
          '<td>Go directly to the authorization flow. If there are stored ' +
          '    credentials, you still might not be prompted to reauthorize ' +
          '    the application.</td></tr>' +
          '<tr><td><a href="/drive">Call Drive API directly</a></td>' +
          '<td> Use stored credentials to call the API, you still might not be prompted to reauthorize ' +
          '    the application.</td></tr>' +
          '<tr><td><a href="/calendar">Call Calendar API directly</a></td>' +
          '<td> Use stored credentials to call the API, you still might not be prompted to reauthorize ' +
          '    the application.</td></tr>' + 
          '<tr><td><a href="/revoke">Revoke current credentials</a></td>' +
          '<td>Revoke the access token associated with the current user ' +
          '    session. After revoking credentials, if you go to the test ' +
          '    page, you should see an <code>invalid_grant</code> error.' +
          '</td></tr>' +
          '<tr><td><a href="/clear">Clear Flask session credentials</a></td>' +
          '<td>Clear the access token currently stored in the user session. ' +
          '    After clearing the token, if you <a href="/authorize">authorize</a> ' +
          '    again, you should go back to the auth flow.' +
          '</td></tr></table>')

if __name__ == '__main__':
  # When running locally, disable OAuthlib's HTTPs verification.
  # ACTION ITEM for developers:
  #     When running in production *do not* leave this option enabled.
  os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

  # This disables the requested scopes and granted scopes check.
  # If users only grant partial request, the warning would not be thrown.
  os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = '1'

  # Specify a hostname and port that are set as a valid redirect URI
  # for your API project in the Google API Console.
  app.run('localhost', 8080, debug=True)

小茹

這個範例使用 Sinatra 架構。

require 'googleauth'
require 'googleauth/web_user_authorizer'
require 'googleauth/stores/redis_token_store'

require 'google/apis/drive_v3'
require 'google/apis/calendar_v3'

require 'sinatra'

configure do
  enable :sessions

  # Required, call the from_file method to retrieve the client ID from a
  # client_secret.json file.
  set :client_id, Google::Auth::ClientId.from_file('/path/to/client_secret.json')

  # Required, scope value
  # Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar.
  scope = ['Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY',
           'Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY']

  # Required, Authorizers require a storage instance to manage long term persistence of
  # access and refresh tokens.
  set :token_store, Google::Auth::Stores::RedisTokenStore.new(redis: Redis.new)

  # Required, indicate where the API server will redirect the user after the user completes
  # the authorization flow. The redirect URI is required. The value must exactly
  # match one of the authorized redirect URIs for the OAuth 2.0 client, which you
  # configured in the API Console. If this value doesn't match an authorized URI,
  # you will get a 'redirect_uri_mismatch' error.
  set :callback_uri, '/oauth2callback'

  # To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI
  # from the client_secret.json file. To get these credentials for your application, visit
  # https://console.cloud.google.com/apis/credentials.
  set :authorizer, Google::Auth::WebUserAuthorizer.new(settings.client_id, settings.scope,
                          settings.token_store, callback_uri: settings.callback_uri)
end

get '/' do
  # NOTE: Assumes the user is already authenticated to the app
  user_id = request.session['user_id']

  # Fetch stored credentials for the user from the given request session.
  # nil if none present
  credentials = settings.authorizer.get_credentials(user_id, request)

  if credentials.nil?
    # Generate a url that asks the user to authorize requested scope(s).
    # Then, redirect user to the url.
    redirect settings.authorizer.get_authorization_url(request: request)
  end
  
  # User authorized the request. Now, check which scopes were granted.
  if credentials.scope.include?(Google::Apis::DriveV3::AUTH_DRIVE_METADATA_READONLY)
    # User authorized read-only Drive activity permission.
    # Example of using Google Drive API to list filenames in user's Drive.
    drive = Google::Apis::DriveV3::DriveService.new
    files = drive.list_files(options: { authorization: credentials })
    "<pre>#{JSON.pretty_generate(files.to_h)}</pre>"
  else
    # User didn't authorize read-only Drive activity permission.
    # Update UX and application accordingly
  end

  # Check if user authorized Calendar read permission.
  if credentials.scope.include?(Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY)
    # User authorized Calendar read permission.
    # Calling the APIs, etc.
  else
    # User didn't authorize Calendar read permission.
    # Update UX and application accordingly
  end
end

# Receive the callback from Google's OAuth 2.0 server.
get '/oauth2callback' do
  # Handle the result of the oauth callback. Defers the exchange of the code by
  # temporarily stashing the results in the user's session.
  target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(request)
  redirect target_url
end

Node.js

如要執行這個範例,請按照下列步驟操作:

  1. 在 API 控制台中,將本機電腦的網址新增至重新導向網址清單。例如,新增 http://localhost。
  2. 請確認您已安裝 Node.js 的維護 LTS、有效 LTS 或目前版本。
  3. 建立新目錄並切換至該目錄。例如:
    mkdir ~/nodejs-oauth2-example
    cd ~/nodejs-oauth2-example
  4. 使用 npm 安裝 Node.js 適用的 Google API 用戶端程式庫:
    npm install googleapis
  5. 建立 main.js 檔案,並加入以下內容。
  6. 執行範例:
    node .\main.js

main.js

const http = require('http');
const https = require('https');
const url = require('url');
const { google } = require('googleapis');
const crypto = require('crypto');
const express = require('express');
const session = require('express-session');

/**
 * To use OAuth2 authentication, we need access to a CLIENT_ID, CLIENT_SECRET, AND REDIRECT_URI.
 * To get these credentials for your application, visit
 * https://console.cloud.google.com/apis/credentials.
 */
const oauth2Client = new google.auth.OAuth2(
  YOUR_CLIENT_ID,
  YOUR_CLIENT_SECRET,
  YOUR_REDIRECT_URL
);

// Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar.
const scopes = [
  'https://www.googleapis.com/auth/drive.metadata.readonly',
  'https://www.googleapis.com/auth/calendar.readonly'
];

/* Global variable that stores user credential in this code example.
 * ACTION ITEM for developers:
 *   Store user's refresh token in your data store if
 *   incorporating this code into your real app.
 *   For more information on handling refresh tokens,
 *   see https://github.com/googleapis/google-api-nodejs-client#handling-refresh-tokens
 */
let userCredential = null;

async function main() {
  const app = express();

  app.use(session({
    secret: 'your_secure_secret_key', // Replace with a strong secret
    resave: false,
    saveUninitialized: false,
  }));

  // Example on redirecting user to Google's OAuth 2.0 server.
  app.get('/', async (req, res) => {
    // Generate a secure random state value.
    const state = crypto.randomBytes(32).toString('hex');
    // Store state in the session
    req.session.state = state;

    // Generate a url that asks permissions for the Drive activity and Google Calendar scope
    const authorizationUrl = oauth2Client.generateAuthUrl({
      // 'online' (default) or 'offline' (gets refresh_token)
      access_type: 'offline',
      /** Pass in the scopes array defined above.
        * Alternatively, if only one scope is needed, you can pass a scope URL as a string */
      scope: scopes,
      // Enable incremental authorization. Recommended as a best practice.
      include_granted_scopes: true,
      // Include the state parameter to reduce the risk of CSRF attacks.
      state: state
    });

    res.redirect(authorizationUrl);
  });

  // Receive the callback from Google's OAuth 2.0 server.
  app.get('/oauth2callback', async (req, res) => {
    // Handle the OAuth 2.0 server response
    let q = url.parse(req.url, true).query;

    if (q.error) { // An error response e.g. error=access_denied
      console.log('Error:' + q.error);
    } else if (q.state !== req.session.state) { //check state value
      console.log('State mismatch. Possible CSRF attack');
      res.end('State mismatch. Possible CSRF attack');
    } else { // Get access and refresh tokens (if access_type is offline)
      let { tokens } = await oauth2Client.getToken(q.code);
      oauth2Client.setCredentials(tokens);

      /** Save credential to the global variable in case access token was refreshed.
        * ACTION ITEM: In a production app, you likely want to save the refresh token
        *              in a secure persistent database instead. */
      userCredential = tokens;
      
      // User authorized the request. Now, check which scopes were granted.
      if (tokens.scope.includes('https://www.googleapis.com/auth/drive.metadata.readonly'))
      {
        // User authorized read-only Drive activity permission.
        // 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) => {
          if (err1) return console.log('The API returned an error: ' + err1);
          const files = res1.data.files;
          if (files.length) {
            console.log('Files:');
            files.map((file) => {
              console.log(`${file.name} (${file.id})`);
            });
          } else {
            console.log('No files found.');
          }
        });
      }
      else
      {
        // User didn't authorize read-only Drive activity permission.
        // Update UX and application accordingly
      }

      // Check if user authorized Calendar read permission.
      if (tokens.scope.includes('https://www.googleapis.com/auth/calendar.readonly'))
      {
        // User authorized Calendar read permission.
        // Calling the APIs, etc.
      }
      else
      {
        // User didn't authorize Calendar read permission.
        // Update UX and application accordingly
      }
    }
  });

  // Example on revoking a token
  app.get('/revoke', async (req, res) => {
    // Build the string for the POST request
    let postData = "token=" + userCredential.access_token;

    // Options for POST request to Google's OAuth 2.0 server to revoke a token
    let postOptions = {
      host: 'oauth2.googleapis.com',
      port: '443',
      path: '/revoke',
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData)
      }
    };

    // Set up the request
    const postReq = https.request(postOptions, function (res) {
      res.setEncoding('utf8');
      res.on('data', d => {
        console.log('Response: ' + d);
      });
    });

    postReq.on('error', error => {
      console.log(error)
    });

    // Post the request with data
    postReq.write(postData);
    postReq.end();
  });


  const server = http.createServer(app);
  server.listen(8080);
}
main().catch(console.error);

HTTP/REST

這個 Python 範例使用 Flask 架構和 Requests 程式庫,示範 OAuth 2.0 網路流程。建議您使用適用於 Python 的 Google API 用戶端程式庫,完成這項流程。(Python 分頁中的範例會使用用戶端程式庫)。

import json
import flask
import requests

app = flask.Flask(__name__)

# To get these credentials (CLIENT_ID CLIENT_SECRET) and for your application, visit
# https://console.cloud.google.com/apis/credentials.
CLIENT_ID = '123456789.apps.googleusercontent.com'
CLIENT_SECRET = 'abc123'  # Read from a file or environmental variable in a real app

# Access scopes for two non-Sign-In scopes: Read-only Drive activity and Google Calendar.
SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly'

# Indicate where the API server will redirect the user after the user completes
# the authorization flow. The redirect URI is required. The value must exactly
# match one of the authorized redirect URIs for the OAuth 2.0 client, which you
# configured in the API Console. If this value doesn't match an authorized URI,
# you will get a 'redirect_uri_mismatch' error.
REDIRECT_URI = 'http://example.com/oauth2callback'

@app.route('/')
def index():
  if 'credentials' not in flask.session:
    return flask.redirect(flask.url_for('oauth2callback'))

  credentials = json.loads(flask.session['credentials'])

  if credentials['expires_in'] <= 0:
    return flask.redirect(flask.url_for('oauth2callback'))
  else: 
    # User authorized the request. Now, check which scopes were granted.
    if 'https://www.googleapis.com/auth/drive.metadata.readonly' in credentials['scope']:
      # User authorized read-only Drive activity permission.
      # Example of using Google Drive API to list filenames in user's Drive.
      headers = {'Authorization': 'Bearer {}'.format(credentials['access_token'])}
      req_uri = 'https://www.googleapis.com/drive/v2/files'
      r = requests.get(req_uri, headers=headers).text
    else:
      # User didn't authorize read-only Drive activity permission.
      # Update UX and application accordingly
      r = 'User did not authorize Drive permission.'

    # Check if user authorized Calendar read permission.
    if 'https://www.googleapis.com/auth/calendar.readonly' in credentials['scope']:
      # User authorized Calendar read permission.
      # Calling the APIs, etc.
      r += 'User authorized Calendar permission.'
    else:
      # User didn't authorize Calendar read permission.
      # Update UX and application accordingly
      r += 'User did not authorize Calendar permission.'

  return r

@app.route('/oauth2callback')
def oauth2callback():
  if 'code' not in flask.request.args:
    state = str(uuid.uuid4())
    flask.session['state'] = state
    # Generate a url that asks permissions for the Drive activity
    # and Google Calendar scope. Then, redirect user to the url.
    auth_uri = ('https://accounts.google.com/o/oauth2/v2/auth?response_type=code'
                '&client_id={}&redirect_uri={}&scope={}&state={}').format(CLIENT_ID, REDIRECT_URI,
                                                                          SCOPE, state)
    return flask.redirect(auth_uri)
  else:
    if 'state' not in flask.request.args or flask.request.args['state'] != flask.session['state']:
      return 'State mismatch. Possible CSRF attack.', 400

    auth_code = flask.request.args.get('code')
    data = {'code': auth_code,
            'client_id': CLIENT_ID,
            'client_secret': CLIENT_SECRET,
            'redirect_uri': REDIRECT_URI,
            'grant_type': 'authorization_code'}

    # Exchange authorization code for access and refresh tokens (if access_type is offline)
    r = requests.post('https://oauth2.googleapis.com/token', data=data)
    flask.session['credentials'] = r.text
    return flask.redirect(flask.url_for('index'))

if __name__ == '__main__':
  import uuid
  app.secret_key = str(uuid.uuid4())
  app.debug = False
  app.run()

重新導向 URI 驗證規則

Google 會對重新導向 URI 套用下列驗證規則,協助開發人員維護應用程式安全防護。重新導向 URI 必須遵守這些規則。如需這些規則中使用的網域、主機、路徑、查詢、配置和使用者資訊定義,請參閱 RFC 3986 第 3 節。

驗證規則
配置

重新導向 URI 必須使用 HTTPS 配置,而非一般 HTTP。Localhost URI (包括 localhost IP 位址 URI) 不受這項規則限制。

主機

主機不得為原始 IP 位址。Localhost IP 位址不受這項規則限制。

網域
  • 主機 TLD (頂層網域) 必須屬於公開尾碼清單。
  • 主機網域不得為 “googleusercontent.com”。
  • 除非應用程式擁有網域,否則重新導向 URI 不得包含網址縮短程序網域 (例如 goo.gl)。此外,如果擁有縮短網域的應用程式選擇重新導向至該網域,則該重新導向 URI 的路徑必須包含 “/google-callback/” 或結尾為 “/google-callback”。
  • Userinfo

    重新導向 URI 不得包含 userinfo 子元件。

    路徑

    重新導向 URI 不得包含路徑遍歷 (也稱為目錄回溯),這類路徑會以 “/..” 或 “\..” 或其網址編碼表示。

    查詢

    重新導向 URI 不得包含開放式重新導向。

    Fragment

    重新導向 URI 不得包含片段元件。

    角色 重新導向 URI 不得包含特定字元,包括:
    • 萬用字元 ('*')
    • 不可列印的 ASCII 字元
    • 無效的百分比編碼 (任何不符合網址編碼格式的百分比編碼,即百分比符號後方未加上兩個十六進位數字)
    • 空值字元 (編碼的 NULL 字元,例如 %00、%C0%80)

    增量授權

    在 OAuth 2.0 通訊協定中,應用程式會要求授權存取資源,而資源是由範圍識別。在需要時才要求資源授權,是提供最佳使用者體驗的做法。為實現這項做法,Google 的授權伺服器支援增量授權。這項功能可讓您視需要要求範圍,如果使用者授予新範圍的權限,則會傳回授權碼,可用於交換包含使用者授予專案的所有範圍的權杖。

    舉例來說,如果應用程式可讓使用者試聽音樂曲目及建立混音,則在登入時可能只需要極少資源,或許只要登入者的姓名即可。不過,如要儲存完成的混音,則需要存取他們的 Google 雲端硬碟。大多數使用者會認為,只有在應用程式實際需要存取 Google 雲端硬碟時,才要求存取權,這樣比較自然。

    在這種情況下,應用程式可能會在登入時要求 openid 和 profile 範圍來執行基本登入,然後在第一次要求儲存混音時要求 https://www.googleapis.com/auth/drive.file 範圍。

    如要實作漸進式授權,請完成要求存取權杖的一般流程,但請務必在授權要求中加入先前授予的範圍。這樣一來,應用程式就不必管理多個存取權杖。

    透過漸進式授權取得的存取權杖,適用下列規則:

    • 這個權杖可用於存取與新合併授權中任何範圍相對應的資源。
    • 使用合併授權的更新權杖取得存取權杖時,存取權杖代表合併授權,可用於回應中包含的任何 scope 值。
    • 合併授權包含使用者授予 API 專案的所有範圍,即使授權是從不同用戶端要求也一樣。舉例來說,如果使用者透過應用程式的電腦用戶端授予一個範圍的存取權,然後透過行動用戶端授予另一個範圍的存取權,則合併授權會包含這兩個範圍。
    • 如果您撤銷代表合併授權的權杖,系統會同時撤銷代表相關聯使用者存取該授權所有範圍的權限。

    「步驟 1:將授權重新導向設定為 Google 的 OAuth 2.0 伺服器」中的特定語言程式碼範例,全都使用漸進式授權。下列程式碼範例也顯示您需要新增的程式碼,以便使用漸進式授權。

    PHP

    $client->setIncludeGrantedScopes(true);

    Python

    在 Python 中,將 include_granted_scopes 關鍵字引數設為 true,確保授權要求包含先前授予的範圍。include_granted_scopes 很可能不會是您設定的「唯一」關鍵字引數,如下例所示。

    authorization_url, state = flow.authorization_url(
        # Enable offline access so that you can refresh an access token without
        # re-prompting the user for permission. Recommended for web server apps.
        access_type='offline',
        # Enable incremental authorization. Recommended as a best practice.
        include_granted_scopes='true')

    小茹

    auth_client.update!(
      :additional_parameters => {"include_granted_scopes" => "true"}
    )

    Node.js

    const authorizationUrl = oauth2Client.generateAuthUrl({
      // 'online' (default) or 'offline' (gets refresh_token)
      access_type: 'offline',
      /** Pass in the scopes array defined above.
        * Alternatively, if only one scope is needed, you can pass a scope URL as a string */
      scope: scopes,
      // Enable incremental authorization. Recommended as a best practice.
      include_granted_scopes: true
    });

    HTTP/REST

    GET https://accounts.google.com/o/oauth2/v2/auth?
      client_id=your_client_id&
      response_type=code&
      state=state_parameter_passthrough_value&
      scope=https%3A//www.googleapis.com/auth/drive.metadata.readonly%20https%3A//www.googleapis.com/auth/calendar.readonly&
      redirect_uri=https%3A//developers.google.com/oauthplayground&
      prompt=consent&
      include_granted_scopes=true

    重新整理存取權杖 (離線存取)

    存取權杖會定期過期,並成為相關 API 要求的無效憑證。如果您要求離線存取與權杖相關聯的範圍,則可以重新整理存取權杖,而不提示使用者授予權限 (包括使用者不在場時)。

    • 如果您使用 Google API 用戶端程式庫,只要將用戶端物件設為離線存取,該物件就會視需要重新整理存取權杖。
    • 如果您未使用用戶端程式庫,則必須在將使用者重新導向至 Google 的 OAuth 2.0 伺服器時,將 access_type HTTP 查詢參數設為 offline。在這種情況下,當您交換授權碼以取得存取權杖時,Google 的授權伺服器會傳回更新權杖。接著,如果存取權杖過期 (或在任何其他時間),您可以使用更新權杖取得新的存取權杖。

    如果應用程式需要在使用者不在場時存取 Google API,就必須要求離線存取權。舉例來說,如果應用程式會執行備份服務或在預定時間執行動作,就必須能在使用者不在場時重新整理存取權杖。預設的存取樣式稱為 online。

    伺服器端網頁應用程式、已安裝的應用程式和裝置都會在授權程序中取得更新權杖。更新權杖通常不會用於用戶端 (JavaScript) 網頁應用程式。

    PHP

    如果應用程式需要離線存取 Google API,請將 API 用戶端的存取權類型設為 offline:

    $client->setAccessType("offline");

    使用者授予所要求範圍的離線存取權後,您就能繼續使用 API 用戶端,在使用者離線時代表使用者存取 Google API。用戶端物件會視需要重新整理存取權杖。

    Python

    在 Python 中,將 access_type 關鍵字引數設為 offline,確保您能重新整理存取權杖,不必再次提示使用者授予權限。access_type 很可能不會是您設定的「唯一」關鍵字引數,如下例所示。

    authorization_url, state = flow.authorization_url(
        # Enable offline access so that you can refresh an access token without
        # re-prompting the user for permission. Recommended for web server apps.
        access_type='offline',
        # Enable incremental authorization. Recommended as a best practice.
        include_granted_scopes='true')

    使用者授予所要求範圍的離線存取權後,您就能繼續使用 API 用戶端,在使用者離線時代表使用者存取 Google API。用戶端物件會視需要重新整理存取權杖。

    小茹

    如果應用程式需要離線存取 Google API,請將 API 用戶端的存取權類型設為 offline:

    auth_client.update!(
      :additional_parameters => {"access_type" => "offline"}
    )

    使用者授予所要求範圍的離線存取權後,您就能繼續使用 API 用戶端,在使用者離線時代表使用者存取 Google API。用戶端物件會視需要重新整理存取權杖。

    Node.js

    如果應用程式需要離線存取 Google API,請將 API 用戶端的存取權類型設為 offline:

    const authorizationUrl = oauth2Client.generateAuthUrl({
      // 'online' (default) or 'offline' (gets refresh_token)
      access_type: 'offline',
      /** Pass in the scopes array defined above.
        * Alternatively, if only one scope is needed, you can pass a scope URL as a string */
      scope: scopes,
      // Enable incremental authorization. Recommended as a best practice.
      include_granted_scopes: true
    });

    使用者授予所要求範圍的離線存取權後,您就能繼續使用 API 用戶端,在使用者離線時代表使用者存取 Google API。用戶端物件會視需要重新整理存取權杖。

    存取權杖會過期。如果存取權杖即將到期,這個程式庫會自動使用更新權杖取得新的存取權杖。如要確保一律儲存最新權杖,最簡單的方法是使用權杖事件:

    oauth2Client.on('tokens', (tokens) => {
      if (tokens.refresh_token) {
        // store the refresh_token in your secure persistent database
        console.log(tokens.refresh_token);
      }
      console.log(tokens.access_token);
    });

    這個權杖事件只會在第一次授權時發生,而且您必須在呼叫 generateAuthUrl 方法時,將 access_type 設為 offline,才能接收更新權杖。如果您已授予應用程式必要權限,但未設定接收更新權杖的適當限制,則必須重新授權應用程式,才能接收新的更新權杖。

    如要稍後再設定 refresh_token,可以使用 setCredentials 方法:

    oauth2Client.setCredentials({
      refresh_token: `STORED_REFRESH_TOKEN`
    });

    用戶端取得更新權杖後,下次呼叫 API 時,系統會自動取得及更新存取權杖。

    HTTP/REST

    如要重新整理存取權杖,應用程式會向 Google 的授權伺服器 (https://oauth2.googleapis.com/token) 傳送 HTTPS POST 要求,並在要求主體中加入下列參數:

    名稱 值
    client_id 從 API 控制台取得的用戶端 ID。
    client_secret 選填

    從 API 控制台取得的用戶端密鑰。

    grant_type 根據 OAuth 2.0 規格中的定義,這個欄位的值必須設為 refresh_token。
    refresh_token 授權碼交換傳回的更新權杖。

    雖然 DPoP 為選用,但建議使用,以提高安全性。如要搭配更新權杖使用 DPoP,應用程式必須為每個傳送至權杖端點的要求,產生新的專屬 DPoP 證明 JWT。這項證明必須使用與初始授權碼交換期間相同的私密金鑰簽署。應用程式會將證明新增為 HTTP 要求標頭。

    標頭 必填 說明
    DPoP 選用 DPoP 證明是證明擁有私密金鑰的 JWT。這是標頭,不是參數。如果提供,傳回的權杖會繫結至這個金鑰。每個要求都必須產生新的專屬證明,且必須包含與要求相符的 htm (HTTP 方法) 和 htu (HTTP URI) 憑證。

    以下程式碼片段為要求範例:

    POST /token HTTP/1.1
    Host: oauth2.googleapis.com
    Content-Type: application/x-www-form-urlencoded
    DPoP: DPOP_PROOF_JWT
    
    client_id=your_client_id&
    refresh_token=refresh_token&
    grant_type=refresh_token

    如要搭配更新權杖使用 DPoP,您必須為要求產生新的專屬 DPoP 證明 JWT。如需產生金鑰組和建構 JWT 的逐步操作說明,請參閱「建構 DPoP 證明」。

    如果交換成功,系統會傳回包含新存取權杖的 200 OK 回應。使用 DPoP 時,token_type 為 Bearer。如果回應成功,代表更新權杖的 DPoP 證明已獲接受。Google 也可能在回應中傳回新的 DPoP-Nonce HTTP 標頭;如果傳回,用戶端必須快取這個 Nonce,並在後續要求的 DPoP 證明 nonce 宣告中加入這個 Nonce。

    如果 DPoP 標頭遺失、無效或使用不正確的金鑰,就會發生交換失敗。如要進一步瞭解特定 DPoP 錯誤代碼和處理 Nonce,請參閱「交換失敗」。

    下列程式碼片段顯示使用 DPoP 時,成功的回應標頭和主體範例:

    HTTP/1.1 200 OK
    Content-Type: application/json; charset=utf-8
    DPoP-Nonce: AN3XwJjZsjnb0ZuWkRlek8QU7wY-Zhf-5IP6tO0tORz0KgtDT1Bo8FX-w4nz3r5lnepI
    
    {
      "access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
      "expires_in": 3920,
      "scope": "https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/calendar.readonly",
      "token_type": "Bearer"
    }

    請注意,系統會限制核發的更新權杖數量,包括每個用戶端/使用者組合的限制,以及每位使用者在所有用戶端中的限制。請將更新權杖儲存在長期儲存空間,並在有效期間內持續使用。如果應用程式要求的更新權杖過多,可能會達到這些限制,導致舊的更新權杖失效。

    撤銷權杖

    在某些情況下,使用者可能想撤銷授予應用程式的存取權。使用者可以前往 帳戶設定撤銷存取權。詳情請參閱「可存取您帳戶的第三方網站和應用程式」支援文件的「移除網站或應用程式存取權」一節。

    應用程式也可以透過程式輔助方式撤銷已獲得的存取權。如果使用者取消訂閱、移除應用程式,或應用程式所需的 API 資源大幅變更,則程式輔助撤銷功能就相當重要。換句話說,移除程序的一部分可能包括 API 要求,以確保先前授予應用程式的權限已移除。

    PHP

    如要透過程式輔助撤銷權杖,請呼叫 revokeToken():

    $client->revokeToken();

    Python

    如要透過程式輔助方式撤銷權杖,請向 https://oauth2.googleapis.com/revoke 發出要求,並將權杖設為參數,然後設定 Content-Type 標頭:

    requests.post('https://oauth2.googleapis.com/revoke',
        params={'token': credentials.token},
        headers = {'content-type': 'application/x-www-form-urlencoded'})

    小茹

    如要透過程式輔助方式撤銷權杖,請向 oauth2.revoke 端點發出 HTTP 要求:

    uri = URI('https://oauth2.googleapis.com/revoke')
    response = Net::HTTP.post_form(uri, 'token' => auth_client.access_token)

    權杖可以是存取權杖或更新權杖。如果權杖是存取權杖,且有對應的更新權杖,則更新權杖也會一併撤銷。

    如果撤銷作業處理成功,回應的狀態碼會是 200。如果發生錯誤,系統會傳回狀態碼 400 和錯誤代碼。

    Node.js

    如要透過程式輔助方式撤銷權杖,請向 /revoke 端點發出 HTTPS POST 要求:

    const https = require('https');
    
    // Build the string for the POST request
    let postData = "token=" + userCredential.access_token;
    
    // Options for POST request to Google's OAuth 2.0 server to revoke a token
    let postOptions = {
      host: 'oauth2.googleapis.com',
      port: '443',
      path: '/revoke',
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData)
      }
    };
    
    // Set up the request
    const postReq = https.request(postOptions, function (res) {
      res.setEncoding('utf8');
      res.on('data', d => {
        console.log('Response: ' + d);
      });
    });
    
    postReq.on('error', error => {
      console.log(error)
    });
    
    // Post the request with data
    postReq.write(postData);
    postReq.end();

    權杖參數可以是存取權杖或更新權杖。如果權杖是存取權杖,且有對應的更新權杖,則更新權杖也會一併撤銷。

    如果撤銷作業處理成功,回應的狀態碼會是 200。如果發生錯誤,系統會傳回狀態碼 400 和錯誤代碼。

    HTTP/REST

    如要透過程式輔助方式撤銷權杖,應用程式會向 https://oauth2.googleapis.com/revoke 提出要求,並將權杖做為參數:

    curl -d -X -POST --header "Content-type:application/x-www-form-urlencoded" \
            https://oauth2.googleapis.com/revoke?token={token}

    權杖可以是存取權杖或更新權杖。如果權杖是存取權杖,且有對應的更新權杖,則更新權杖也會一併撤銷。

    如果撤銷作業成功處理,回應的 HTTP 狀態碼會是 200。如果發生錯誤,系統會傳回 HTTP 狀態碼 400 和錯誤代碼。

    以時間為依據的存取權

    使用者可透過限時存取權,授予應用程式在有限時間內存取資料的權限,以便完成操作。在同意聲明流程中,部分 Google 產品提供限時存取權,使用者可以選擇授予限時存取權。例如 Data Portability API,可一次性轉移資料。

    使用者授予應用程式限時存取權後,更新權杖會在指定時間過後失效。請注意,在特定情況下,更新權杖可能會提早失效,詳情請參閱這些案例。在這種情況下,授權碼交換回應中傳回的 refresh_token_expires_in 欄位,代表更新權杖到期前的剩餘時間。

    導入跨帳戶防護功能

    為保護使用者帳戶,您應採取的額外步驟是使用 Google 的跨帳戶防護服務,導入跨帳戶防護功能。這項服務可讓您訂閱安全性事件通知,為應用程式提供使用者帳戶重大異動的相關資訊。接著,您可以根據決定如何回應事件,使用這些資訊採取行動。

    Google 跨帳戶防護服務會傳送多種事件類型到您的應用程式,以下列舉幾例:

    • https://schemas.openid.net/secevent/risc/event-type/sessions-revoked
    • https://schemas.openid.net/secevent/oauth/event-type/token-revoked
    • https://schemas.openid.net/secevent/risc/event-type/account-disabled

    如要進一步瞭解如何實作跨帳戶防護,以及查看可用事件的完整清單,請參閱「 使用跨帳戶防護保護使用者帳戶 」頁面。