PowerShell で Google API を利用するためのトークンを取得する方法

# やりたいこと

ローカルの PowerShell スクリプト（`*.ps1`）から Google API を使うためのトークンを取得する。

<img src="https://docs.google.com/drawings/d/e/2PACX-1vTstmUT1u2OZ_RepNJj66V2qBQ4xXaVIgpyn7njubn4Mv4TqLHs7AKzLXE_gRNrB33sujTxy08olPOp/pub?w=1331&amp;h=1056">

# 経緯

Google Spreadsheets に登録されている情報を元に Internal の Redmine へ REST API でチケットを新規作成する処理が必要になったため。

# 前提環境

- OS は Windows
- 余計なミドルウェアを入れないで使えるものとしては PowerShell 一択

Google Apps Script（`https://script.google.com`） から直接 Redmine（`http://redmine.localhost`）に対してリクエストできたら良いのですが、Redmine 側が Internal なのでそういうワケにもいかず :sweat_smile:

# やり方

## 1. Google API の Client ID と Client Secret の取得

まず、Google API を使うためにはトークンが必要なのですが、トークンを取得するためには OAuth で認証するための

1. Client ID と
2. Client Secret

が必要なので、こちらの記事（[【Google API Console】クライアントID・クライアントシークレットを取得する - Qiita](https://qiita.com/garagara-jp/items/df461947344bb76ee25f) ）を参考に取得します。当該記事と現在の GCP の画面デザインは多少異なるかもしれませんがオペレーションに大した違いはありません。

後ほど取得した Client ID と Client Secret は使うのでメモしておきます。

## 2. PowerShell でスクリプトを作成

[Google APIのアクセストークンをPowerShellでリフレッシュトークンも考えて取得する - Qiita](https://qiita.com/Marimoiro/items/79da14a29a31ce8d02ce) を参考にスクリプトを作成しました。当該記事では Client ID、Client Secret、Refresh Token、Access Token をローカルのテキストファイルに設定・保存していますが、スクリプト実行環境を複数人が使用するケースも有り得ますし、実行ユーザー以外に参照させないためにもユーザーの環境変数に保存するように処理を変更しています。

### Gist

下記のソースは Gist に保存しています。必要であればダウンロードしてください。
[gist.github.com/takeru08ma/GoogleAPI-Get-AccessToken.ps1](https://gist.github.com/takeru08ma/a7dfd391726c1a5be96f5cbcbfcb4fd2)
※ Gist って Qiita に埋め込みできないんですね。。

### ソース

```powershell:GoogleAPI-Get-AccessToken.ps1
<#
.SYNOPSIS
  Google API を使用するするために必要なトークンを取得する。

.DESCRIPTION
  Google Cloud Platform (GCP) で、自身のアカウント（*@gmail.com など）を用いて
  Client ID、Client Secret を取得してメモしておく。
  Client ID と Client Secret の準備ができたら当スクリプトを . で読み込む。
  
  $Scope を指定して GoogleAPI-Get-AccessToken 関数を実行する。
  
  スクリプト実行時に Client ID と Client Secret の入力を求められるので
  入力する。正常に処理されると続いて認証コードの入力を求められる。

  Client ID, Client Secret, Refresh Token, Access Token はユーザーの環境変数に
  保存されます。

  環境変数:
    User:
      GOOGLE_API_CLIENT_ID
      GOOGLE_API_CLIENT_SECRET
      GOOGLE_API_REFRESH_TOKEN
      GOOGLE_API_ACCESS_TOKEN

.PARAMETER Scope
  どの範囲で Google API を利用するかスコープ（Uris）を指定する。
  例: Google Spreadsheets のデータを読み込み専用で使用するスコープの場合は、 
      https://www.googleapis.com/auth/spreadsheets.readonly になります。
      （指定がなければこれがデフォルトになります。）

.INPUTS
  処理中に求められるデータの順番。

  1. Google API の Client ID
  2. Google API の Client Secret
  3. 認証コード

  Client ID と Client Secret が正常であれば認証コードを入力します。

.LINK
  Google API OAuth2
    https://developers.google.com/identity/protocols/OAuth2
  
  Scopes
    https://developers.google.com/identity/protocols/googlescopes
    https://developers.google.com/gmail/api/auth/scopes
    https://developers.google.com/sheets/api/guides/authorizing
    https://developers.google.com/drive/api/v2/about-auth

.EXAMPLE
  . ./GoogleAPI-Get-AccessToken.ps1
  # Spreadsheets を読み取り専用の権限で使用する
  GoogleAPI-Get-AccessToken "https://www.googleapis.com/auth/spreadsheets.readonly"

.EXAMPLE
  . ./GoogleAPI-Get-AccessToken.ps1
  # Gmail の読み書きと Drive の読み取り専用で使用する
  GoogleAPI-Get-AccessToken "https://www.googleapis.com/auth/gmail.compose https://www.googleapis.com/auth/drive.readonly"
#>
function GoogleAPI-Get-AccessToken([String]$Scope = "https://www.googleapis.com/auth/spreadsheets.readonly") {
  # エラー発生時に Continue させない
  $ErrorActionPreference = "Stop"

  try {
    $RefreshToken = [System.Environment]::GetEnvironmentVariable("GOOGLE_API_REFRESH_TOKEN", "User")
    if (!$RefreshToken) {
      $Scope = [System.Web.HttpUtility]::UrlEncode($Scope)
      return GoogleAPI-Get-AccessTokenFromAuthorizationCode $Scope
    }

    $ClientID = [System.Environment]::GetEnvironmentVariable("GOOGLE_API_CLIENT_ID", "User")
    $ClientSecret = [System.Environment]::GetEnvironmentVariable("GOOGLE_API_CLIENT_SECRET", "User")
    
    $RefreshData = @{
      refresh_token = $RefreshToken;
      client_id = $ClientID;
      client_secret = $ClientSecret;
      grant_type = "refresh_token";
    }

    $Response = Invoke-WebRequest -Body $RefreshData -Uri "https://www.googleapis.com/oauth2/v4/token" -Method Post 
    $Token = $Response.Content | ConvertFrom-Json
    [System.Environment]::SetEnvironmentVariable("GOOGLE_API_ACCESS_TOKEN", $Token.access_token, "User")
    return $Token.access_token
  }
  catch [Exception] {
    $ErrorFile = "${PSScriptRoot}/" + $myInvocation.MyCommand.name + "_" + $env:USERNAME + "_Error_" + (Get-Date).ToString("yyyyMMdd-HHmmss-fff") + ".log"
    $error[0] | Out-File -File $ErrorFile -Encoding Default
    Write-Host ($myInvocation.MyCommand.name + " で例外が発生しました。エラー内容は下記パスに保存されました。`r`n${ErrorFile}")
    return $false
  }
}

function GoogleAPI-Get-AccessTokenFromAuthorizationCode([String]$Scope) {
  $ClientID = [System.Environment]::GetEnvironmentVariable("GOOGLE_API_CLIENT_ID", "User")
  # Client ID を貼り付けて実行
  if (!$ClientID) {
    $ClientID = Read-Host "Google API の Client ID を入力してください"
    if ($ClientID -eq "") {
      Write-Error "Google API の Client ID がユーザー環境変数に設定されていないため処理を中止します。"
    }
    [System.Environment]::SetEnvironmentVariable("GOOGLE_API_CLIENT_ID", $ClientID, "User")
  }

  $ClientSecret = [System.Environment]::GetEnvironmentVariable("GOOGLE_API_CLIENT_SECRET", "User")
  if (!$ClientSecret) {
    # Client Secret を貼り付けて実行
    $ClientSecret = Read-Host "Google API の Client Secret を入力してください"
    if ($ClientSecret -eq "") {
      Write-Error "Google API の Client Secret がユーザー環境変数に設定されていないため処理を中止します。"
    }
    [System.Environment]::SetEnvironmentVariable("GOOGLE_API_CLIENT_SECRET", $ClientSecret, "User")
  }

  $RedirectUri = "urn:ietf:wg:oauth:2.0:oob"
  
  Write-Host "ブラウザ認証を行います"
  
  $Uri = "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=$ClientID&redirect_uri=$RedirectUri&scope=$Scope&access_type=offline"
  Write-Host $Uri
  
  # ブラウザを開いて Client ID と Client Secret で承認を行い、認証コードを取得する。
  Start-Process $Uri

  # 認証コードを貼り付けて実行
  $AuthorizationCode = Read-Host "認証コードを入力してください"
  
  $AuthData = @{
    code = $AuthorizationCode;
    client_id = $ClientID;
    client_secret = $ClientSecret;
    redirect_uri = $RedirectUri;
    grant_type = "authorization_code";
    access_type = "offline";
  }
  
  $TokenResponse = Invoke-WebRequest -Body $AuthData -Uri "https://www.googleapis.com/oauth2/v4/token" -Method Post 
  $Token = $TokenResponse.Content | ConvertFrom-Json
  [System.Environment]::SetEnvironmentVariable("GOOGLE_API_ACCESS_TOKEN", $Token.access_token, "User")
  [System.Environment]::SetEnvironmentVariable("GOOGLE_API_REFRESH_TOKEN", $Token.refresh_token, "User")

  return $Token.access_token
}

# [Debug]
# GoogleAPI-Get-AccessToken "https://www.googleapis.com/auth/spreadsheets.readonly"
```

## 3. PowerShell で Google API を使うためのトークンを取得

下記のコマンドを実行していきます。

```powershell:PowerShell
# スクリプトを読み込み
. .\GoogleAPI-Get-AccessToken.ps1

# ヘルプを表示
Get-Help GoogleAPI-Get-AccessToken

# Spreadsheets を読み取り専用で使うためのスコープ
GoogleAPI-Get-AccessToken "https://www.googleapis.com/auth/spreadsheets.readonly"
Google API の Client ID を入力してください:     # Client ID を貼り付け
Google API の Client Secret を入力してください: # Client Secret を貼り付け
https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com&redirect_uri=urn:iet
f:wg:oauth:2.0:oob&scope=https%3a%2f%2fwww.googleapis.com%2fauth%2fspreadsheets.readonly&access_type=offline
認証コードを入力してください: 
```

- スクリプト実行中にブラウザが起動し、
- Google のアクセスリクエストの許可画面が表示されますので、
- 許可したいアカウントを指定して `許可` を選択すると、
- 認証コードが表示されます。
このコードをコピーして PowerShell に貼り付けてください。

<img src="https://docs.google.com/drawings/d/e/2PACX-1vSBzkge2wskga12alpdx8XmHZByebSFIwOu4HrpnYBYAHMZu8qm1mL818LJT9gQ43LPIzION1Y--3-L/pub?w=530&amp;h=1075">

```powershell:PowerShell
認証コードを入力してください: # 認証コードを貼り付け
```

これでユーザーの環境変数に認証に必要なトークンの情報が保存されました。

<img src="https://docs.google.com/drawings/d/e/2PACX-1vRkdBKlm43xG4jgfHJ3o7nwqbaw-07YJzC-7mDMBd1Qpi2i8pcLaee-4RVjGPJMJNn8Yv7OEQh_U74s/pub?w=930&amp;h=883">

## 参考文献

- [【Google API Console】クライアントID・クライアントシークレットを取得する - Qiita](https://qiita.com/garagara-jp/items/df461947344bb76ee25f)
- [Google APIのアクセストークンをPowerShellでリフレッシュトークンも考えて取得する - Qiita](https://qiita.com/Marimoiro/items/79da14a29a31ce8d02ce)
- [【PowerShell】環境変数を設定する - ほそぼそプログラミング日記](https://hosopro.blogspot.com/2017/01/powershell-set-environment-variable.html)