Heroku上からService Account認証でGoogle APIにアクセスする

Googleの様々なAPIを叩く際、認証にOAuth 2.0を用いる。
Using OAuth 2.0 to Access Google APIs  |  Google Identity Platform  |  Google Developers
使用する場面やパターンによって以下のような6つのシナリオが想定されている。

  • Login
  • Web Server Applications
  • Client-side Applications
  • Installed Applications
  • Devices
  • Service Accounts

多くの場合は「ユーザごとに認証させて個別のtokenを発行しリクエストに利用する」という流れなのだけど、中にはたとえばURL短縮APIとか、必ずしもユーザ個別にtokenを発行させる必要がないこともある。
そういう場合には「サービス固有のtoken」だけあれば良い。ということで使えるのが「Service Accounts」という方式。
Using OAuth 2.0 for Server to Server Applications  |  Google Identity Platform  |  Google Developers
サービスアカウントに発行されたprivate keyを使って署名したJWTリクエストを生成してAPIを叩く、という仕組みのようだ。

rubygoogle-api-clientを使う例

GoogleAPIを叩くためのライブラリとして、Ruby Gemsではgoogle-api-clientというのがある。
google-api-client | RubyGems.org | your community gem host
JWTを使ったリクエストや認証なども対応してくれている。
これを使って実際にやってみる。

準備

Google API Console
にて、Application typeを"Service account"と選択してClient IDを生成する。

すると、"Email address"が発行され、private keyがダウンロードできるようになるので、これを使う。

書く

Gemfileに

source :rubygems
gem 'google-api-client', '0.6.0'

と書いてbundle installして、以下のようなスクリプトを書く。

require 'google/api_client'

client = Google::APIClient.new(:application_name => '')
key = Google::APIClient::PKCS12.load_key('/Users/sugyan/Downloads/...-privatekey.p12', 'notasecret')
client.authorization = Signet::OAuth2::Client.new(
  :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
  :audience             => 'https://accounts.google.com/o/oauth2/token',
  :scope                => 'https://www.googleapis.com/auth/urlshortener',
  :issuer               => <発行されたemail address>
  :signing_key          => key,
)
client.authorization.fetch_access_token!

shortener = client.discovered_api('urlshortener')
result = client.execute(
  :api_method  => shortener.url.insert,
  :body_object => { :longUrl => 'http://d.hatena.ne.jp/sugyan/' },
)
puts result.data.id

ダウンロードしたprivate keyのファイルを読みこんでkeyを生成し、email addressやscopeを指定し、認証。
これで、GoogleのURL短縮APIを叩いて短縮URLを得ることができる。

$ bundle exec ruby shorten.rb
http://goo.gl/zLVjD

Herokuに上げるために

で、こんなものをHeroku上で動かそうとすると、API Consoleからダウンロードしたprivate keyのファイルをgit repositoryに含める必要が出てしまう。それはイヤだ。
幸いにも、Google::APIClient::PKCS12でloadしたkeyは文字列として得られる。

$ bundle exec ruby -r 'google/api_client' -e 'puts Google::APIClient::PKCS12.load_key("/Users/sugyan/Downloads/...-privatekey.p12", "notasecret")'
-----BEGIN RSA PRIVATE KEY-----
MIICXQ...

...

-----END RSA PRIVATE KEY-----

なので、これを丸ごとheroku configで渡してしまえばいい。ついでに発行されたemail addressも。

$ heroku config:set GOOGLE_API_KEY="$(bundle exec ruby -r 'google/api_client' -e 'puts Google::APIClient::PKCS12.load_key("/Users/sugyan/Downloads/...-privatekey.p12", "notasecret")')"
$ heroku config:set GOOGLE_API_EMAILADDRESS=...@developer.gserviceaccount.com

で、これを使用してKeyを生成するようにコードを変更する。

require 'google/api_client'

client = Google::APIClient.new(:application_name => '')
key = OpenSSL::PKey::RSA.new(ENV['GOOGLE_API_KEY'])
client.authorization = Signet::OAuth2::Client.new(
  :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
  :audience             => 'https://accounts.google.com/o/oauth2/token',
  :scope                => 'https://www.googleapis.com/auth/urlshortener',
  :issuer               => ENV['GOOGLE_API_EMAILADDRESS'],
  :signing_key          => key,
)
client.authorization.fetch_access_token!

shortener = client.discovered_api('urlshortener')
result = client.execute(
  :api_method  => shortener.url.insert,
  :body_object => { :longUrl => 'http://d.hatena.ne.jp/sugyan/' },
)
puts result.data.id

これで、このへんをcommitしてherokuにpushすると、設定したconfigの文字列を使用してheroku上からService Accounts認証を使ってAPIを叩けるようになる。

$ git push heroku master
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 1.10 KiB, done.
Total 6 (delta 0), reused 0 (delta 0)
-----> Ruby app detected
-----> Installing dependencies using Bundler version 1.3.0.pre.5
       Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin --deployment
       Fetching gem metadata from http://rubygems.org/.........
       Fetching gem metadata from http://rubygems.org/..
       Installing addressable (2.3.2)
       Installing extlib (0.9.16)
       Installing multi_json (1.5.0)
       Installing autoparse (0.3.2)
       Installing multipart-post (1.1.5)
       Installing faraday (0.8.4)
       Installing jwt (0.1.5)
       Installing launchy (2.1.2)
       Installing signet (0.4.4)
       Installing uuidtools (2.1.3)
       Installing google-api-client (0.6.0)
       Using bundler (1.3.0.pre.5)
       Your bundle is complete! It was installed into ./vendor/bundle
       Cleaning up the bundler cache.

...

 * [new branch]      master -> master
$ heroku run bundle exec ruby shorten.rb
http://goo.gl/WnqP5

まとめ

…と、そういうようなことが、以下の記事に書いてあった。
http://ar.zu.my/how-to-store-private-key-files-in-heroku/