GoogleデータへのアクセスのためのシンプルなコマンドラインスクリプトにおけるOAuthの利用

2019年1月22日



Googleのウェブサイトからいくつかのデータを取得する簡単なスクリプトを作成する必要がありました。プライベートなデータを取得していたため、それを行うための認証が必要でした。難しくないにもかかわらず、私を導くための十分なドキュメントがなかったため、予想以上に多くの作業が必要であることが分かりました。関連性の低いドキュメントをたくさん見て、進むべき道を見つけ出す必要がありました。そのため、解決策を見つけた後、将来再びこれを行う必要がある場合と、同様のことをしたいと考えている他の誰かを助けるために、私が行ったことの簡単な説明を書くことにしました。

私は最初に2015年にこれを行いました。1年後くらいに、それは壊れ、修正する帯域幅がありませんでした。最終的に2019年に修正しました。私が使用していたライブラリは(より良い方向に)変更されていましたが、ドキュメントはまだ不足していました。そのため、この記事を更新しました。

まず免責事項です。これは私が考え出したもので、現時点では私にとって機能します。私が望むことを行うための最良の方法かどうかを徹底的に調査したわけではありません(ただし、行っている間は徹底的な調査のように感じられました)。その点をご理解ください。(より良い方法があれば教えてください。)

私は、それが私の慣れたスクリプティング言語であるため、これらすべてをRubyで行いました。また、Ruby用のGoogleのAPIライブラリも使用しました。しかし、全体的なフローの大部分は他の言語でも同じなので、Ruby以外を使用している場合でも、私が行ったことの多くは依然として関連性があると思います。Rubyの例に加えて、できる限り言語に依存しない視点で説明しようとします。

YouTubeのプライベートデータにアクセスする必要があります。[1] プライベートデータであるため、Googleに認証し、そのプライベートデータにアクセスできるようにスクリプトに必要な認証を設定する必要があります。このスクリプトを一切手動で介入せずに実行したいので、少なくともラップトップにログインした後、スクリプト自体がアクセスできるような認証メカニズムを使用したいと思います。

私がたどった成功した方法を説明する前に、行き詰まった道を説明する必要があります。この簡単な作業を非常に困難にしたことの1つは、私が読んだドキュメントの大部分で、ブラウザを誘導するWebアプリケーションを作成したいと想定していたことです。(古風な方法だからだと思いますが)ブラウザを必要としないシンプルなコマンドラインアプリケーションが欲しかったのです。最初にこれを試したとき、Googleの認証と認可に関するガイドを読み、Googleが目指していると思われるOAuth 2.0を使用することにしました。次にGoogleはOAuth認証のいくつかのシナリオを示しましたが、自然な(複雑な場合もある)方法はサービスアカウントを使用することでした。これらは、公開鍵/秘密鍵ペアを使用して行われた認証によるサーバー間アクセスをサポートしています。これを機能させるためにかなりの時間を費やし、最終的にGoogleに正常にアクセスすることができましたが、そこで壁にぶち当たりました。サービスアカウントを使用すると、Googleに新しいユーザーを効果的に作成します。次に、そのユーザーが個人データにアクセスできるようにするメカニズムが必要です。Googleでドメインを実行している場合、サービスアカウントがドメインのデータにアクセスすることを承認する方法があります。しかし、私のもののような直接的なGoogleアカウントのデータにアクセスするためのそのようなメカニズムは見つかりませんでした。ドキュメントには、分析などの一部のプロパティに対して実行できることが示唆されていましたが、YouTubeデータなどに対して機能するような一般的なメカニズムはありませんでした。

2019年に再度試したとき、サービスアカウントを再度試しました。今回は、私が望む方法でそれらを使用することがはるかに簡単に見えました。機能すると確信できる呼び出しを行うことができましたが、失敗し続けました。最終的に、ドキュメントのサービスアカウントがYouTubeでは機能しないという行を見つけました。解決策を考え出すのに何時間も費やし、そのようなハードウォールにぶち当たるのはいつもイライラします。この記事が、数人の人々をそのような努力から救う以上のことをしなければ、書く価値があります。

認証のアウトラインフロー

私が機能させた方法は、Googleがモバイルおよびデスクトップアプリ向けのOAuth 2.0と呼んでいるものに基づいていますが、手動で介入したり、ブラウザを使用したりすることなく(ほとんど)実行できるように適応する必要がありました。

これがどのように機能するかを説明するために、YouTubeリストを取得するための簡単なリクエストから始めます。スクリプトがGoogleデータを取得するためのリクエストを行うときはいつでも、リクエストにアクセストークンを含める必要があります。Googleのドキュメントでは、このようなHTTPリクエストが示されています。

GET /plus/v1/people/me HTTP/1.1
Authorization: Bearer 1/fFBGRNJru1FQd44AzqT3Zg
Host: googleapis.com

アクセストークンは、ランダムに見える文字の束です。これは短時間(約1時間)有効です。アクセストークンはスクリプトが作業を行うために必要なものですが、それは単に質問につながります。そもそもアクセストークンをどのように取得するのですか?

アクセストークンを取得する1つの方法は、別の種類のトークンである更新トークンを持つことです。アクセストークンとは異なり、更新トークンは長時間有効です。それらは取り消された場合、後の更新トークンによって置き換えられた場合、またはGoogleが激怒した場合にのみ期限切れになります。私は数年間、Googleアナリティクスにアクセスするために同じものを使用しています。私たちのスクリプトの目的のために、更新トークンはまさに仕事です。更新トークンを取得したら、スクリプトが手動で介入せずにアクセスできる安全な場所に保存できます。スクリプトを実行するときに更新トークンにアクセスし、最初のステップとして更新トークンを使用して真新しいアクセストークンを取得できます。その後、スクリプトの実行の残りの部分でアクセストークンを使用します(スクリプトがアクセストークンの有効時間よりも長く実行されない限り—そしてRubyでさえそれほど遅くはありません)。

更新トークンの取得方法を説明する前に、それらに関するもう1つのことがあります。各更新トークン(およびそれらが取得するアクセストークン)には、制限された認可スコープがあります。つまり、アクセスを許可するデータを指定します。YouTubeデータの読み取りのみに有効な更新トークンを作成できます。悪人がこのトークンを取得した場合、私のカレンダーデータを読み取ったり、YouTubeデータを変更したりすることはできません。さまざまなスコープを持つ異なるトークンを持つことは、各トークンで何を行うかを制限するのに役立ち、より安全になり(トークンを安全に保存する方法について心配するのも少なくなります)。

更新トークンを取得するには、ブラウザを使用してGoogleにログインし、自分として認証する必要があります。ほとんどの人と同様に、ラップトップでGoogleに常時ログインしているブラウザインスタンスを持っているため、これは問題ではありません。私が行うのは、私が望む認可スコープを指定するように構築されたGoogle URLにアクセスすることです。Googleアカウントにログインした状態でそれを行うと、Googleはワンタイム認証コードを返します。次に、そのコードを使用して別のURLにアクセスすると、Googleは必要な更新トークンを渡します。これは手動のステップですが、めったにこれを行う必要がないため、それで問題ありません。

これらすべてを行う前に、さらに1つのことを行う必要があります。つまり、APIを使用し、APIアクセスが到達するアプリケーションへのアクセスを許可するようにGoogleを設定することです。これも手動のタスクですが、(Googleが本当に激怒しない限り)一度だけ行う必要があります。

それでは、私が実行する必要がある手順を示します。

  • APIアクセスのためのGoogleの設定—ログインしたブラウザを使用した1回限りの手動アクション
  • ワンタイム認証コードの取得—ログインしたブラウザが必要、めったに行われない
  • 認証コードを更新トークンと交換する—API、めったに行われない
  • 更新トークンを使用して新しいアクセストークンを取得する—APIのみ、スクリプトを実行するたびに1回実行される
  • Googleを呼び出す際にアクセストークンを使用する—APIのみ、Google APIを呼び出すたびに実行される

Googleの設定

GoogleアカウントでAPIを使用するには、Googleにアクセスして設定する必要があります。必要な場所はGoogle Cloud Consoleです。コンソールに既にプロジェクトが定義されていましたが、まだ持っていない場合はそれを行う必要があります。

最初に必要なことは、YouTubeデータAPIを有効にすることです。「APIとサービスを有効にする」という上部のリンクをクリックします。

そのリンクをクリックすると、追加して有効にするAPIを検索できます。

次に、資格情報を整理する必要があります。これには、「資格情報」タブ(左側)をクリックします。まだ資格情報を持っていない場合は、「資格情報の作成」ボタンを使用して作成します。クライアントの種類を選択できます。ここでは「その他」を選択します。次に、クライアントIDとクライアントシークレットが表示されます。その資格情報の鉛筆アイコンをクリックすることで、後でこの情報にアクセスできます。これらの情報は後でコードで使用します。

最後に、適切なAPIスコープをプロジェクトに追加します。これには、上部の「OAuth同意画面」というリンクをクリックします。「Google APIのスコープ」セクションまでスクロールし、「スコープの追加」ボタンをクリックして../auth/youtube.readonlyスコープを追加します。

ワンタイム認証コードの取得

ワンタイム認証コードを取得するには、Googleにログインした状態で、特別に作成されたGoogle URLにアクセスする必要があります。次に、Googleは認証コードを返します。Googleのドキュメントと、私が遭遇したさまざまなサンプルでは、Webアプリを介してこれを行うことが説明されています。通常のWebアプリケーションフローの中で、Webアプリケーションは認証が必要であることを認識し、ユーザーをGoogleに送信します。

Googleは認証コードをウェブアプリに直接返すことができます。必要なのは、ローカルマシンでサーバーを実行し、そのURL(例:localhost:1234)をGoogleに伝えるだけです。その後、GoogleはそのURLにGETリクエストを送信し、認証コードをURLのパラメータとして含めます。コードはそのパラメータを簡単に取得できます。このポートでこのリクエストを受け取るためのウェブサーバーはそれほど複雑なものではありません。この1つのリクエストに応答できれば十分です。このレベルのシンプルなサーバーでは、Sinatra(Rubyの軽量ウェブサーバーフレームワーク)も必要ありません。何年も前にPrag Daveと一緒にRubyの入門クラスで、数分でシンプルなウェブサーバーを作成したことを覚えています。しかし、私はそれをするのが面倒でした。

代わりに、プログラムに必要なGoogleのURLを作成し、そのURLをコンソールに出力しました。そして、それをブラウザにコピー&ペーストします。Googleは(私が何をしているかを確認するためのちょっとした処理の後)、ウェブページに認証コードを表示します。その後、このコードをスクリプトにコピー&ペーストします。自動化されたメカニズムほどスムーズではありませんが、めったに実行しないので問題ありません。

コードを見てみましょう。私は、些細ではないコマンドラインスクリプトを複数のクラスに分割し、コマンドラインのやり取りを処理するクラスと、バックグラウンドで作業を行う「エンジン」クラスを分離します。これは基本的にSeparated Presentationの活用です。コマンドラインとコアコードを個別に作業する方が簡単だと感じるからです。このケースではほとんど価値がありませんが、これは役に立つ習慣だと考えています。

資格情報を操作するために、Google資格情報クラスを作成します。

class GoogleCredentials…

  def initialize(application_name: nil, refresh_key: , scopes: nil,
      client_secret: nil, client_id: nil)
    @application_name = application_name
    @refresh_key = refresh_key
    @scopes = scopes
    @client_secret = client_secret
    @client_id = client_id
  end

必要なデータすべてを入力して、ファクトリメソッドで資格情報オブジェクトを作成できます。

class GoogleCredentials…

  def self.for_youtube
    return self.new(
      application_name: 'Youtube Analytics',
      refresh_key: 'yt-analyze',
      scopes: ['https://www.googleapis.com/auth/youtube.readonly'],
      client_id: '12434.apps.googleusercontent.com',
      client_secret: '1234secretstring'
      )
  end

名前にもかかわらず、client_secretはこのコンテキストではそれほど秘密ではなく、むしろユーザーIDのようなものです。

このデータのほとんどはGoogleとのやり取りに必要なものです。例外はrefresh_keyで、これは取得したリフレッシュトークンを保存するために使用するキーです。

認証コードを取得するには、それにアクセスするためのGoogle URLを作成する必要があります。これはauthorization_urlメソッドで行います。

class GoogleCredentials…

  def authorization_url 
    params = {
      scope: @scopes.join(" "),
      redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
      response_type: 'code',
      client_id: @client_id
    }
    url = {
      host: 'accounts.google.com',
      path: '/o/oauth2/v2/auth',
      query: URI.encode_www_form(params)
    }

    return URI::HTTPS.build(url)
  end

コマンドラインの処理にはThorライブラリ[2]を使用します。

class CLI…

  class CLI < Thor
    include Thor::Actions

    def initialize *args
      super(*args)
      @engine = GoogleCredentials.for_youtube
    end
    
    desc "url", "display the google auth url to hit in the browser"
    def url
      puts @engine.authorization_url
    end      

これで、コマンドラインでruby cli.rb urlを実行すると、次のようなURLがコードによって出力されます。

https://#/o/oauth2/auth?
  scope=https://www.googleapis.com/auth/youtube.readonly&
  redirect_uri=urn:ietf:wg:oauth:2.0:oob&
  response_type=code&
  client_id=12434.apps.googleusercontent.com

読みやすくするために、改行と空白を追加し、URLエスケープをデコードしました。また、client_idは架空のものです。

URLのパラメータは次のとおりです。

  • scope: アクセスするAPIの範囲。この場合は、YouTube Data APIへの読み取り専用アクセスを希望します。
  • redirect_uri: ウェブアプリで通常どおりこれを使用する場合、Googleはブラウザを別のURL(通常はlocalhostのポート)にリダイレクトし、そこにレスポンスを配置します。この値を使用することで、Googleにブラウザに表示させてコピー&ペーストすることを指示します。
  • response_type: 一時的な認証コードを返してほしいです。
  • client_id: これは、Google Developers Consoleとの以前のやり取りで取得します。

そのURLをブラウザに貼り付けると(最終的に)、Googleから輝く認証コードを表示するウェブページが表示されます。

認証コードを更新トークンと交換する

認証コードを取得したので、2番目の操作を開始して、リフレッシュトークンを取得できます。これには、Googleの認証リソースに再度アクセスし、今回取得した認証コードを提供し、それをclient_secret(Google APIに対して私を識別するコード)と組み合わせます。このステップでは、Googleにログインする必要も、ブラウザを使用する必要もありません。

この時点で、別の問題に直面しなければなりません。リフレッシュトークンはどこに保存するべきでしょうか?これは私だけが使用するスクリプトなので、次のような形でソースコードに保存できます。

def refresh_token
  '1234567890WOxNS_gTztCGW3OBTKcSoKfLXDPc5TA7xz4MEudVrK5jSpoR30zcRFq6'
end

しかし、コードを広くコピーされ、他の人と共有されることが多いリポジトリにコードを保存したいので、これは好きではありません。実際、一般的なセキュリティに関するアドバイスとして、**リポジトリコードツリーの中に決して秘密を保存しない**ことが挙げられています。秘密を含むファイルを誤ってコミットするのは非常に簡単であり、一度コミットしてしまうと削除することはほぼ不可能です。私は自然とかなり不注意なので、避けられないミスが永続的な損害を引き起こさないように工夫しようとします。

別のオプションとして、トークンをソースツリー外のファイルにダンプすることです。私のハードドライブは暗号化されているため、これはかなり安全です。特に、保護しているのは私のYouTube視聴習慣の暗い秘密だけだからです。もう少し用心深いのであれば、そのファイルを暗号化することもできますが、そうすると、スクリプトを使用するたびにパスワードを入力したくないため、そのファイルの暗号化キーはどこに保存するかという問題が出てきます。

Macで実行しているので、Macの組み込みキーチェーンを使用することにしました。これはログイン時に自動的に開き、securityコマンドラインアプリケーションを使用してアクセスできます。Ubuntuマシンで実行する必要がある場合は別の方法を考える必要がありますが、必要になったときに対応します。

リフレッシュトークンを取得するには、先に取得したワンタイム認証コードを使用して新しいトークンをリクエストし、リフレッシュトークンを取り出し、キーチェーンに保存する必要があります。(「トークン」と言っているのは、Googleはアクセストークンとリフレッシュトークンの両方を返すためです。)

これらのトークンをリクエストするには、Googleに再度アクセスしますが、今回はGoogle APIのRubyクライアントライブラリを使用するのが最善だと感じています。トークンを取得するためのコードを以下に示します。

class CLI…

  desc "refresh", "put in auth code, save refresh code"
  def refresh
    auth_code = ask "paste in the authorization code"
    @engine.renew_refresh_token auth_code
  end

class GoogleCredentials…

  def renew_refresh_token auth_code
    token = get_new_refresh_token(auth_code)
    puts "new token: #{token}"
    save_refresh_token token
  end

  def get_new_refresh_token auth_code
    client = Signet::OAuth2::Client.new(
      token_credential_uri: 'https://www.googleapis.com/oauth2/v3/token',
      code: auth_code,
      client_id: @client_id,
      client_secret: client_secret,
      redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
      grant_type: 'authorization_code'
      )
    client.fetch_access_token!
    return client.refresh_token
  end

このコードはまず、必要なデータすべてを使用してSignet OAuth2クライアントオブジェクトをインスタンス化し、アクセストークンのフェッチを指示します。それが完了すると、リフレッシュトークンを要求して保存できます。

トークンをMacのキーチェーンに保存します。

class GoogleCredentials…

  def save_refresh_token arg
    cmd = "security add-generic-password -a '#{@refresh_key}' -s '#{@refresh_key}' -w '#{arg}'"
    system cmd
  end

securityコマンドは、値を保存する際にサービス(-s)とアカウント(-a)の両方を必要とします。キーバリューストアとしてのみ使用したいので、両方に同じ値を使用します。

更新トークンを使用してアクセストークンを取得する

上記の認証ロジックはまれなものです。私は数年に一度しか呼び出さないことを期待しており、実際、過去数年で2回しか実行していません。次に必要になるまでには、ライブラリが変更されていなければいいのですが。新しいスコープにアクセスする必要がある場合は、新しいファクトリメソッドを宣言するだけです。

これで資格情報オブジェクトができたので、それを活用して何か有用なことをするだけです(この場合はプレイリストを出力します)。

リフレッシュトークンを使用するには、リフレッシュトークンを使用してUserRefreshCredentialsを作成し、fetch_access_token!を使用してGoogleと通信し、Google APIを呼び出すために必要なアクセストークンをロードする必要があります。そのためのコードを以下に示します。

class GoogleCredentials…

  def load_user_refresh_credentials
    @credentials = Google::Auth::UserRefreshCredentials.new(
      client_id: @client_id,
      scope: @scopes,
      client_secret: @client_secret,
      refresh_token: refresh_token,
      additional_parameters: { "access_type" => "offline" })
    @credentials.fetch_access_token!
    return @credentials
  end
  def refresh_token
    @refresh_token ||= `security find-generic-password -wa #{@refresh_key}`.chomp
    @refresh_token
  end

Google APIから動画のリストを取得する

この記事を最初に書いたとき、GoogleにアクセスするためのRubyライブラリは特にわかりにくかったです。ランタイムコード生成を使用していたため、どのようなメソッドを呼び出せるかを調べるためにpryを使用する必要がありました。しかし、今ではビルドのステップとしてコード生成を行い、生成されたクラスをファーストクラスアーティファクトとして保存します。これにより、どのようなメソッドがあるかを確認できるようになり、作業がはるかに簡単になります。また、rubydocでオンラインのAPIドキュメントも提供できるようになります。

YouTubeと通信するには、YouTubeサービスを使用する必要があります。認証と認可を処理するために、ユーザーリフレッシュ資格情報を提供するだけです。

auth_client = GoogleCredentials.for_youtube.load_user_refresh_credentials
youtube = Google::Apis::YoutubeV3::YouTubeService.new
youtube.authorization = auth_client

これで、プレイリストのアイテムを一覧表示するメソッドなど、このYouTubeオブジェクトのメソッドを呼び出すことができます。

youtube.list_playlists('snippet', max_results: 50, mine: true)

この呼び出しはListPlaylistResponseオブジェクトを返します。これは単純なデータオブジェクトであり、OOの専門家である私が通常嫌うような貧弱なデータオブジェクトですが、Data Transfer Objectとして機能するため、このコンテキストでは問題ありません。


脚注

1: これは正確には私がしようとしていたことではありませんが、OAuthの部分に焦点を当てているため、実際のタスクをできる限り簡素化しました。

2: Rubyには多くのコマンドラインツールキットがあります。それらをきちんと調査したわけではありませんが、Thorは私のニーズに比較的よく合っているようです。気にしない多くの機能がありますが、必要な簡単なものについては、その複雑さを邪魔しません。

重要な改訂

2019年1月22日: 最新のライブラリに合わせて記事を更新しました。

2016年2月27日: ライブラリの変更により記事は非推奨となりました。

2015年1月26日: 初公開