Google Workspace におけるアクセストークンの取得方法について（備忘録）

## 概要

クライアント側 Web アプリケーション用のアクセストークン取得方法について記述する。

ここでは、ユーザーが Google 認証を行い、Google がユーザにアクセストークンをリダイレクトする流れについてまとめる。

## 参考にした資料

- [OAuth 2.0 for Client-side Web Applications](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow)

- [Client-side (JavaScript) applications](https://developers.google.com/identity/protocols/oauth2#clientside)

## 前提条件

### プロジェクトの API を有効にする

Google API を呼び出すアプリはすべて、API コンソールでそれらの API を 有効にする必要がある。

1. GoogleAPI コンソールで「[API ライブラリ](https://console.developers.google.com/apis/library)」を開く

1. もしプロンプトが表示されたら、プロジェクトを選択するか、新規プロジェクトを作成する

1. API ライブラリで有効にする API を選択し、[有効にする]ボタンをクリックする

1. もしプロンプトが表示されたら、請求を有効にする。

1. もしプロンプトが表示されたら、API の利用規約を読んで同意する

### 承認資格情報を作成する

OAuth2.0 を使用して Google にアクセスするアプリケーションには、Google の OAuth2.0 サーバーに対してアプリケーションを識別するための認証情報が必要である。ここではプロジェクトの資格情報を作成する方法について記述する。

本設定後、外部アプリケーションは資格情報を使用して、そのプロジェクトで有効にした API にアクセス可能となる。

1. 「[資格情報](https://console.developers.google.com/apis/credentials)」ページに移動する

1. [認証情報の作成] > [OAuth クライアント ID]をクリック

1. 「Web アプリケーション」のアプリタイプを選択する

1. フォームに必要事項を記入する

   - JavaScript を使用して承認された GoogleAPI リクエストを行うアプリケーションは、承認された JavaScript オリジンを指定する必要がある
   - オリジンは、アプリケーションが OAuth2.0 サーバーにリクエストを送信できるドメインを識別します。

### アクセス範囲を特定する

必要なリソースへのアクセスのみを設定する。

「[OAuth2.0 の API ドキュメント](https://developers.google.com/identity/protocols/oauth2/scopes)」には、GoogleAPI のアクセスに使用する可能性のあるスコープのリストが記述されているので、必要に応じて参照。

## OAuth2.0 アクセストークンの取得

### 手順 1: クライアントオブジェクトを構成する

OAuth2.0 を扱うのに 「[Google APIs client library for JavaScript](https://developers.google.com/api-client-library/javascript)」を使用している場合、まず初めに`gapi.auth2` と `gapi.client` の設定を行う。

> 注：Google からの推奨事項として、OAuth2.0 を使用する場合は、`JS Client Library` を使用することを強く推奨している

#### JS Client Library を使用する方法（推奨）

ここでは、後述するのコード例の抜粋を使用して説明する。設定内容について確認する。

以下のコードで、アプリケーションが後で使用する `gapi.client` オブジェクトと、アプリケーションがユーザーの認証ステータスを確認及び監視する `gapi.auth2` オブジェクトが初期化される。

`gapi.client.init` の呼び出しでは次のフィールドを指定する。

- `apiKey` と `clientId`：アプリケーションの認証資格情報を指定する。これらの値は API コンソールで取得可能。

- `scope`：アプリケーションがユーザーに代わってアクセスできるリソースに対応するスコープリストを指定する。可能な限り承認処理を促すように多く記述するのが望ましい。

- `disocoveryDocs`：アプリケーションが使用する APIDiscovery ドキュメントのリストを識別するもので、使用する GoogleAPI のドキュメントを設定することで表示させることが可能。

`gapi.client.init` の呼び出し完了後、コードは GoogleAuth 変数を設定して、GoogleAuth オブジェクトを識別するようになる。

```javascript
var GoogleAuth; // Google Auth object.
function initClient() {
  gapi.client
    .init({
      apiKey: "YOUR_API_KEY",
      clientId: "YOUR_CLIENT_ID",
      scope: "https://www.googleapis.com/auth/drive.metadata.readonly",
      discoveryDocs: [
        "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest",
      ],
    })
    .then(function () {
      GoogleAuth = gapi.auth2.getAuthInstance();

      // Listen for sign-in state changes.
      GoogleAuth.isSignedIn.listen(updateSigninStatus);
    });
}
```

#### OAuth2.0 Endpoints を使用する方法

特に確認することはない。

### 手順 2: Google の OAuth2.0 サーバにリダイレクトする

> 注：Google からの推奨事項として、OAuth2.0 を使用する場合は、`JS Client Library` を使用することを強く推奨している

#### JS Client Library を使用する方法（推奨）

以下の関数でユーザーを Google の認証サーバに誘導する。

```javascript
GoogleAuth.signIn();
```

実際には、アプリケーション側で bool 値を設定し、API を呼び出す前に signIn() 関数を呼び出すか銅貨を決定する場合がある。

以下の例でユーザー認証を開始する方法を示すが、以下の点に注意すること。

- `GoogleAuth` オブジェクトは、手順 1 で定義されたグローバル変数と同じものである

- `updateSigninStatus` 関数は、ユーザーの認証ステータスの変更を確認するリスナーで、手順 1 の例でも定義されているものと同じである

  ```javascript
  GoogleAuth.isSignedIn.listen(updateSigninStatus);
  ```

- 以下の例では、2 つの追加グローバル変数を定義している

  - `isAuthorized`：ユーザーがすでにサインインしているかどうかを示す bool 変数。アプリの読み込み時に設定可能で、ユーザーがサインインっまたはサインアウトした場合に更新できる。

  - `sendAuthorizedApiRequest`：`isAuthorized` 変数を確認し、API リクエストを行うか、ユーザーにアプリの承認を求めるかを決定する関数。

  - `currentApiRequest`：ユーザーが最後に行った API リクエストに関する情報を格納するオブジェクト。`sendAuthorizedApiRequest` 関数を呼び出すときに設定される。

  ユーザーがアプリを承認した場合、リクエストは即座に実行される。それ以外の場合は、ユーザーをサインインにリダイレクトする。サインイン後、`updateSigninStatus` 関数は `sendAuthorizedApiRequest` 関数を呼び出し、処理を再実行する。

```javascript
var isAuthorized;
var currentApiRequest;

/**
 * Store the request details. Then check to determine whether the user
 * has authorized the application.
 *   - If the user has granted access, make the API request.
 *   - If the user has not granted access, initiate the sign-in flow.
 */
function sendAuthorizedApiRequest(requestDetails) {
  currentApiRequest = requestDetails;
  if (isAuthorized) {
    // Make API request
    // gapi.client.request(requestDetails)

    // Reset currentApiRequest variable.
    currentApiRequest = {};
  } else {
    GoogleAuth.signIn();
  }
}

/**
 * Listener called when user completes auth flow. If the currentApiRequest
 * variable is set, then the user was prompted to authorize the application
 * before the request executed. In that case, proceed with that API request.
 */
function updateSigninStatus(isSignedIn) {
  if (isSignedIn) {
    isAuthorized = true;
    if (currentApiRequest) {
      sendAuthorizedApiRequest(currentApiRequest);
    }
  } else {
    isAuthorized = false;
  }
}
```

#### OAuth2.0 Endpoints を使用する方法

[ここ](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints_1)を参照。

### 手順 3: Google 側がユーザーに同意を求める

ユーザーがアプリケーションに要求されたアクセスを許可するかを決定する。

この段階では、Google がアプリケーションの名前とユーザーの認証情報を使用してアクセスの許可を要求している Google API サービスと、許可されるアクセス範囲の概要を示す同意ウェイン同を表示する。

アプリケーション側は、アクセスが許可されたかどうかを示す Google の OAuth2.0 サーバからの応答を待つため、この段階では何もする必要はない。

### 手順 4: OAuth2.0 サーバの応答を処理する

> 注：Google からの推奨事項として、OAuth2.0 を使用する場合は、`JS Client Library` を使用することを強く推奨している

#### JS Client Library を使用する方法（推奨）

JS Client Library では、Google の認証サーバーからの応答を処理する。現在のユーザーのサインイン状態を確認する処理を設定した場合、その関数はアプリケーションへのアクセスが許可されたときに呼び出される。

#### OAuth2.0 Endpoints を使用する方法

[ここ](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints_2)を参照。

## アクセストークンを取得した後の API リクエスト方法

> 注：Google からの推奨事項として、OAuth2.0 を使用する場合は、`JS Client Library` を使用することを強く推奨している

### JS Client Library を使用する方法（推奨）

アプリケーションがアクセストークンを取得した後は、JS Client Library を使用して、ユーザーに代わって API リクエストを行うことが可能。その際、JS Client Library がアクセストークンを管理するようになり、リクエストでトークンを送信するための特別な処理は必要ない。

JS Client Library は、API メソッドを呼び出す 2 つの方法をサポートしている。Discovery ドキュメントで呼び出した場合は、API は固有関数が定義される。また、`[gapi.client.request](https://github.com/google/google-api-javascript-client/blob/master/docs/reference.md#----gapiclientrequestargs--`関数を使用して API を呼び出すことも可能。

以下の例では、 Google Drive API の `about.get` 関数について、2 通りのアクセス方法を示している。

```javascript
// Example 1: Use method-specific function
var request = gapi.client.drive.about.get({ fields: "user" });

// Execute the API request.
request.execute(function (response) {
  console.log(response);
});

// Example 2: Use gapi.client.request(args) function
var request = gapi.client.request({
  method: "GET",
  path: "/drive/v3/about",
  params: { fields: "user" },
});
// Execute the API request.
request.execute(function (response) {
  console.log(response);
});
```

### OAuth2.0 Endpoints を使用する方法

[ここ](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints_3)を参照。

## サンプル

### Web 上でのサンプル

以下のページにアクセスし、[Sign In/Authorize] ボタンをクリックすることで、アクセストークンの取得から API リクエストのサンプルを実行することが可能。

- [Complete example -- Sample code demo](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#js-client-library_4)

このアプリでは、`https://www.googleapis.com/auth/drive.metadata.readonly` へアクセスするが、JavaScript アプリケーションで OAuth2.0 フローを開始する方法を示すためにのみ使用され、実際の API リクエストは行われない。

### JavaScript サンプルコード

このサンプルコードは、JavaScript 用の Google API クライアントライブラリを読み込み、OAuth2.0 フローを開始するサンプルコードである。これを実行すると、以下のいずれかのページが表示されることとなる。

- `ユーザーがアプリにサインインできるようにする 1 つのボタン`：ユーザーが以前にアプリを承認していない場合、アプリは OAuth2.0 フローを起動する。

- `ユーザーがアプリからサインアウトするか、以前にアプリに付与されたアクセスを取り消すことができる 2 つのボタン`：アプリからサインアプトしても、アプリに付与されているアクセス権は取り消されないため、アクセスを取り消す場合のためのボタンを用意されている。

なお、 Google アカウントの [権限](https://myaccount.google.com/permissions) ページからもアプリへのアクセス権は取り消すことが可能。

```javascript
<script>
  var GoogleAuth;
  var SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
  function handleClientLoad() {
    // Load the API's client and auth2 modules.
    // Call the initClient function after the modules load.
    gapi.load('client:auth2', initClient);
  }

  function initClient() {
    // In practice, your app can retrieve one or more discovery documents.
    var discoveryUrl = 'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest';

    // Initialize the gapi.client object, which app uses to make API requests.
    // Get API key and client ID from API Console.
    // 'scope' field specifies space-delimited list of access scopes.
    gapi.client.init({
        'apiKey': 'YOUR_API_KEY',
        'clientId': 'YOUR_CLIENT_ID',
        'discoveryDocs': [discoveryUrl],
        'scope': SCOPE
    }).then(function () {
      GoogleAuth = gapi.auth2.getAuthInstance();

      // Listen for sign-in state changes.
      GoogleAuth.isSignedIn.listen(updateSigninStatus);

      // Handle initial sign-in state. (Determine if user is already signed in.)
      var user = GoogleAuth.currentUser.get();
      setSigninStatus();

      // Call handleAuthClick function when user clicks on
      //      "Sign In/Authorize" button.
      $('#sign-in-or-out-button').click(function() {
        handleAuthClick();
      });
      $('#revoke-access-button').click(function() {
        revokeAccess();
      });
    });
  }

  function handleAuthClick() {
    if (GoogleAuth.isSignedIn.get()) {
      // User is authorized and has clicked "Sign out" button.
      GoogleAuth.signOut();
    } else {
      // User is not signed in. Start Google auth flow.
      GoogleAuth.signIn();
    }
  }

  function revokeAccess() {
    GoogleAuth.disconnect();
  }

  function setSigninStatus() {
    var user = GoogleAuth.currentUser.get();
    var isAuthorized = user.hasGrantedScopes(SCOPE);
    if (isAuthorized) {
      $('#sign-in-or-out-button').html('Sign out');
      $('#revoke-access-button').css('display', 'inline-block');
      $('#auth-status').html('You are currently signed in and have granted ' +
          'access to this app.');
    } else {
      $('#sign-in-or-out-button').html('Sign In/Authorize');
      $('#revoke-access-button').css('display', 'none');
      $('#auth-status').html('You have not authorized this app or you are ' +
          'signed out.');
    }
  }

  function updateSigninStatus() {
    setSigninStatus();
  }
</script>

<button id="sign-in-or-out-button"
        style="margin-left: 25px">Sign In/Authorize</button>
<button id="revoke-access-button"
        style="display: none; margin-left: 25px">Revoke access</button>

<div id="auth-status" style="display: inline; padding-left: 25px"></div><hr>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script async defer src="https://apis.google.com/js/api.js"
        onload="this.onload=function(){};handleClientLoad()"
        onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
```

## （参考）OAuth2.0 の追加承認

OAuth 2.0 プロトコルでは、アプリはスコープによって識別されるリソースにアクセスするための承認を要求する仕組みである。リソースが別途必要になった時に、そのリソースに対する承認を要求することは、エスとエクスペリエンスとみなされており、Google の認証サーバもこれをサポートしている。

以下のコード例では、既存のアクセストークンにスコープを追加する方法を示している。これにより、アプリは複数のアクセストークンを管理する必要はなくなる。

### JS Cliemt Library を使用する方法（推奨）

既存のアクセストークンにスコープを追加するには、`GoogleUser.grant(options)` 関数を呼び出し、`options` オブジェクトには、アクセスを許可する追加のスコープを入れる。

```javascript
// Space-separated list of additional scope(s) you are requesting access to.
// This code adds read-only access to the user's calendars via the Calendar API.
var NEW_SCOPES = "https://www.googleapis.com/auth/calendar.readonly";

// Retrieve the GoogleUser object for the current user.
var GoogleUser = GoogleAuth.currentUser.get();
GoogleUser.grant({ scope: NEW_SCOPES });
```

### OAuth2.0 Endpoints を使用する方法

[ここ](https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#oauth-2.0-endpoints_5)を参照。

## （参考）トークンを削除する

ユーザー登録を解除したり、アプリを削除したり、アプリに必要な API リソースが大幅に変更された場合に重要である。方法は 2 通りある。

### プログラムで取り消す方法

以下のコードを使用する。

```javascript
GoogleAuth.disconnect();
```

### アカウント設定から取り消す方法

[アカウント設定](https://myaccount.google.com/permissions) ページにて、アクセス権を取り消すことも可能。詳細については、[サポートドキュメント](https://support.google.com/accounts/answer/3466521#remove-access)を参照。

## （参考）トークンの有効期限について

付与されたトークンが機能しなくなるのは、以下の 3 点のいずれかの場合である。

- ユーザーがアプリのアクセスを取り消した場合

- トークンが 6 か月間使用されていない場合

- ユーザーアカウントに付与されたアカウント数が最大数（規定値：50）を超えた場合（この場合、古いものからトークンが無効となる）

## 最後に

ここでまとめた Google Workspace のアクセストークンの取得方法は、JavaScript アプリ（クライアント側アプリ）からアクセスするものであるが、もし Web サーバアプリからのアクセスだった場合は、以下のサイトを参考にする必要がある。

- [Web サーバアプリケーションからのアクセストークン取得方法](https://developers.google.com/identity/protocols/oauth2/web-server)