はじめに
前回の続きです
追記: 結論、どうしたらrefreshされるのかは分かりませんが、どうしてdrive.authorizationに指定する必要があるのかは分かりました。
前回までの結論をおさらいすると
- list_filesがやっているのはクエリパラメータを添えて、httpリクエストを投げているだけ
- drive.authorizationに設定できるのは文字列か apply!(Hash)というメソッドを持ったObject
本章でauthorizationのapply!の方を詳しく突き詰めることができると思う
authorizationについて
確か途中でSignetかapplyメソッドを持つものかとかあって、結局文字列指定すれば良いの?
と今思ったのでもう一回頭から整理し直す。
最初
drive = Google::Apis::DriveV3::DriveService.new drive.authorization = ... # See Googleauth or Signet libraries
ここで定義。
drive.authorizationの正体
drive.authorization=から
BaseServiceのこれ
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client]
# OAuth2 credentials
def authorization=(authorization)
request_options.authorization = authorization
end
BaseServiceのrequest_optionsはこれ。 self.request_options = Google::Apis::RequestOptions.default.dup
RequestOptionsはこれ
# @!attribute [rw] authorization
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
入れたrequest_optionsを list_filesとか具体のコマンド叩くときに ApiCommand < HttpCommand作るときに指定 command.options = request_options.merge(options)
これでcommandのoptionsにhashではいる。 <= ここの理解が違った。
オープンクラスとかいうやつ
いい質問です!確認してみましょう。
コードを見ると:
# 最初にStructで定義 RequestOptions = Struct.new( :authorization, :retries, # ... ) # その後、同じ名前でクラス定義 class RequestOptions # @!attribute [rw] authorization # ... def self.default @options ||= RequestOptions.new end def merge(options) # ... end end
これはクラスの再オープン(reopening) です!
Rubyでは、Struct.newで作ったクラスを後からclassキーワードで再オープンして、メソッドを追加できます。
つまり:
1. Struct.newで基本的な構造(属性とアクセサ)を持つRequestOptionsクラスが作られる
2. その後class RequestOptionsで同じクラスを再オープンして、mergeメソッドやself.defaultメソッドを追加している
結果として、RequestOptionsは:
- Structの機能(ハッシュライクなアクセス、dupなど)を持ちつつ
- カスタムメソッド(merge、default)も持つ
というStructベースのクラスになっています。
dupする対象は、Structで作られた上にカスタムメソッドが追加されたクラスということですね!
command.optionsはこれ
# Request options
# @return [Google::Apis::RequestOptions]
attr_accessor :options
hashではない。だからオブジェクトみたいに入れられる。
つまり Google::Apis::RequestOptions が入ると。 optionsにはauthoriozationがある
でこのcommandに設定した optionsに含まれる authorizationが
httpリクエストに送られる直前で設定される。
def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String)
req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end
req_header.update(header)
end
再まとめ
drive.authorization =
=> BaseServiceの authorization= (設定できるのはSignet::OAuth2::Client)
=> これがrequest_options.authorizationに入れてる
=> request_options側では Signet::OAuth2::Client, #apply(Hash) が可能
=> 具体のlist_files用の make_simple_command(:get, 'files', options)
=> command.options = request_options.merge(options) (optionsはlist_filesで指定しない限りnil)
=> optionsがnilならrequest_optionsをそのまま入れる。
=> これによってcommand.options.authorizationとかでアクセスできる。
=> これが リクエスト直前にapply_request_headerでreq_headerを更新
=> (apply_request_headerの戻り値を受け取ってないけどrubyのハッシュは参照の値渡しだから良いみたい)
=> 知らなかった。
=> これを元に @http_res = client.run_request(method, url.to_s, body, request_header)
OK、authorizationの情報はhttpリクエスト時のheaderに関わってくる。
次の段階。
度々出てきている Signet::OAuth2::Clientやapplyとかはなぜ必要なんだろう。
とにかくapply!メソッドがrequest_headerの書き換えに使われている事は理解した。
まずは Signet::OAuth2::Client
Signet::OAuth2::Client
OAuth認証に使うみたい。
Googleが作っているのかな?
なぜこれを渡す必要があるんだろう。
確かこれだったな。
# See Googleauth or Signet libraries
Signet::OAuth2::Clientを見てみたけど apply!メソッドがないっぽい。
claudeに聞いてみた。
おっしゃる通りです!Signet::OAuth2::Clientにはapply!メソッドが定義されていません。 実は、apply!メソッドは別のgemであるgoogleauthの中のGoogle::Authモジュールで定義されています。 Signet::OAuth2::Clientは低レベルのライブラリで、googleauthがそれをラップしてapply!メソッドなどの便利な機能を追加しているんです!
ほー。ということで信じてみよう。
次なるターゲット Googleauth gem
確かに検索してみるとSignetの文字がチラチラあるな。
でapply!メソッドはあるかというと。
あ、あるな。
でどれもhashが引数として渡されている。
試しに1つ覗いてみる
module BaseClient
AUTH_METADATA_KEY = :authorization
# Updates a_hash updated with the authentication token
def apply! a_hash, opts = {}
# fetch the access token there is currently not one, or if the client
# has expired
fetch_access_token! opts if needs_access_token?
token = send token_type
a_hash[AUTH_METADATA_KEY] = "Bearer #{token}"
logger&.debug do
hash = Digest::SHA256.hexdigest token
Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})"
end
a_hash[AUTH_METADATA_KEY]
end
あー、hashの更新で、これもauthorizationヘッダについて Beaderトークンを設定しているな。
hashを渡して関数内で更新すれば、親元も更新される(rubyのハッシュは参照の値渡しなので)
どれ使えばいいの?
とにかくこのgoogleauthを使って作ったやつをnewして drive.authorizationに設定すれば良さそう
そのnewしたやつは apply!メソッドがあるやつで。
ただ?
どれを使えばいいの?
そしてそのどれがどのくらいあるの?
ただ結局access_tokenがあればそれで良い事はわかっている。
なので一番シンプルなやつからやってみよう。
それがREADMEに載っているやつだと思う。知らんけど。
あ、これか??めっちゃぽいな
The closest thing to a base credentials class is the BaseClient module. It includes functionality common to most credentials, such as applying authentication tokens to request headers, managing token expiration and refresh, handling logging, and providing updater procs for API clients. Many credentials classes inherit from Signet::OAuth2::Client (lib/googleauth/signet.rb) class which provides OAuth-based authentication. The Signet::OAuth2::Client includes the BaseClient functionality. Most credential types either inherit from Signet::OAuth2::Client or include the BaseClient module directly. Notably, Google::Auth::Credentials (lib/googleauth/credentials.rb) is not a base type or a credentials type per se. It is a wrapper for other credential classes that exposes common initialization functionality, such as creating credentials from environment variables, default paths, or application defaults. It is used and subclassed by Google's API client libraries.
どれもBaseclient moduleをincludeしていますよ、そして Signet::OAuth2::Client includes the BaseClientだそう。
元々のSignetのものをgoogleauth側で拡張しているってことなのかな?
module Signet
# OAuth2 supports OAuth2 authentication.
module OAuth2
# Signet::OAuth2::Client creates an OAuth2 client
#
# This reopens Client to add #apply and #apply! methods which update a
# hash with the fetched authentication token.
class Client
include Google::Auth::BaseClient
確かにしているわ。
理解。
でさっきのCredentials.mdにあった基本のものからやっていくか。
一番シンプルなのはこれっぽい。
Google::Auth::BearerTokenCredentials - lib/googleauth/bearer_token.rb Includes Google::Auth::BaseClient module Implements Bearer Token authentication Bearer tokens are strings representing an authorization grant Can be OAuth2 tokens, JWTs, ID tokens, or any token sent as a Bearer in an Authorization header Used when the end-user is managing the token separately (e.g., with another service) Token lifetime tracking and refresh are outside this class's scope No JSON representation for this type of credentials
Google::Auth::BearerTokenCredentials
既に取得済みのトークン文字列(Bearer token)を使って認証するクラス。
特徴: - トークン文字列をそのまま使う(OAuth2トークン、JWT、IDトークンなど何でもOK) - トークンの管理(有効期限、更新など)はこのクラスの責任外 - ユーザーが外部で別途トークンを管理している場合に使用
使い方:
credentials = Google::Auth::BearerTokenCredentials.new("ya29.a0AfH6...")
service.authorization = credentials
シンプルに「すでにあるトークンをAPIリクエストに添付するだけ」のクラス。
とclaudeに教えてもらったので、中身を見てみる。
Google::Auth::BearerTokenCredentials、君に決めた
def initialize options = {}
raise ArgumentError, "Bearer token must be provided" if options[:token].nil? || options[:token].empty?
@token = options[:token]
@expires_at = case options[:expires_at]
when Time
options[:expires_at]
when Numeric
Time.at options[:expires_at]
end
@universe_domain = options[:universe_domain] || "googleapis.com"
end
ホイホイ。という事はまずaccess_tokenをomniauthで適当に取るか。
サンプルがあったので。
あーそっか、access_tokenどこに載せようか問題で、そのまま止まってたんだった。
sessionでいいかな。
(裏でガチャガチャやり中〜〜〜)
rails cでとりあえずやった。
drive = Google::Apis::Drive::DriveService.new
drive.authorization = Google::Auth::BearerTokenCredentials.new({token: access_token})
としてdrive.list_files(q: "hoge")としたら
403 Caught error PERMISSION_DENIED: Request had insufficient authentication scopes.
と出た。
scopeが足りてないですよと。
これって今のtokenに付与されているスコープって見れないのかな?
まぁomniauth側で定義しているのが emailとprofileだからそうだと思うんだけど。
require 'net/http'
require 'json'
access_token = "ya29.a0AfH6..."
uri = URI("https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=#{access_token}")
response = Net::HTTP.get(uri)
token_info = JSON.parse(response)
puts token_info["scope"]
# => "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/calendar"
これでいけるっぽい。
やってみるか。
rails c
https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid
やっぱりそうだ。
で、今回google drive触るにはそれ専用のscopeつける必要があったはず。
ここからproject移動してスコープに何を選択してるか見てみる
何も選択してなさそう。
ということでdriveを追加する

https://www.googleapis.com/auth/drive
セキュリティ的に広すぎるかもだがやり方がわからない。教えてください。
a = drive.list_files Sending HTTP get https://www.googleapis.com/drive/v3/files? 200 a.class Google::Apis::DriveV3::FileList
できたー。
一区切り。
これってアクセストークンが切れた時の挙動って時間待つしかないのかな
偽のトークンセットすれば良いみたい。
あー401になった。ただretryしてるな。なぜだ。
まぁいっか。
で試したいこと2つ目。
authorizationにtokenの文字列直指定でもいけるのか問題。
見た感じはいけると思うが。
これまで読んできた感じ。
あ、いけたわ。そりゃそうだよね。
わーいできたー。
でも実用性はそんなない。
やっぱりAPI叩く直前に勝手にtokenリフレッシュやってくれた方が良い。
でもこれってどうするのが適切なんだろう。
大抵、アプリ側のログインで取ってきたtokenを使うことがほとんどだし、
という事はbefore_actionとかで毎回確認してからセットするみたいなことした方が良いのかな。
こっちで更新してその場しのぎしてもそれを親元のアプリの方に連携するのって
どうやるのかわからんし、多分めんどそう。というかできるのかな。
次はrefreshの機構を試す。
さっきのCredentials.mdの中に以下があった。
User Authentication 1. Google::Auth::UserRefreshCredentials < Signet::OAuth2::Client - lib/googleauth/user_refresh.rb * For user refresh token authentication (from 3-legged OAuth flow) * Authenticates on behalf of a user who has authorized the application * Handles token refresh when original access token expires * Typically obtained through web or installed application flow. The JSON form of this credential type has a "type" field with the value "authorized_user". Google::Auth::UserAuthorizer (lib/googleauth/user_authorizer.rb) and Google::Auth::WebUserAuthorizer (lib/googleauth/web_user_authorizer.rb) are used to facilitate user authentication. The UserAuthorizer handles interactive 3-Legged-OAuth2 (3LO) user consent authorization for command-line applications. The WebUserAuthorizer is a variation of UserAuthorizer adapted for Rack-based web applications that manages OAuth state and provides callback handling.
こいつ。Handles token refresh when original access token expiresとある。
さて元をのぞいてみる。
def initialize options = {}
options ||= {}
options[:token_credential_uri] ||= TOKEN_CRED_URI
options[:authorization_uri] ||= AUTHORIZATION_URI
@project_id = options[:project_id]
@project_id ||= CredentialsLoader.load_gcloud_project_id
@quota_project_id = options[:quota_project_id]
super options
end
このsuperってどこだ。誰の?
あー、元のSignetの方か。
def initialize options = {}
@authorization_uri = nil
@token_credential_uri = nil
@client_id = nil
@client_secret = nil
@code = nil
@expires_at = nil
@issued_at = nil
@issuer = nil
@password = nil
@principal = nil
@redirect_uri = nil
@scope = nil
@target_audience = nil
@state = nil
@username = nil
@access_type = nil
@granted_scopes = nil
update! options
end
てかこっちには access_tokenとかrefresh_tokenとか指定しなくて良いんだ。
うそ、あるわ、
update!の次のupdate_token!とかいうのでやってる
という事でUserRefreshCredentialsを作る。
app(dev)* drive.authorization = Google::Auth::UserRefreshCredentials.new ({
app(dev)* access_token: token,
app(dev)* refresh_token: ref,
app(dev)* client_id: ENV['GOOGLE_CLIENT_ID'],
app(dev)* client_secret: ENV['GOOGLE_CLIENT_SECRET']
app(dev)> })
こんな。
おし、list_filesいけた。
これで試しにtokenを適当にしてもう一回チャレンジして成功すればrefreshしているということ。
やってみよう。
app(dev)* drive.authorization = Google::Auth::UserRefreshCredentials.new ({
app(dev)* access_token: "hogehoge",
app(dev)* refresh_token: ref,
app(dev)* client_id: ENV['GOOGLE_CLIENT_ID'],
app(dev)* client_secret: ENV['GOOGLE_CLIENT_SECRET']
app(dev)> })
ダメだな
Caught error Unauthorized (Google::Apis::AuthorizationError)
何でだ。指定しているものが足りないのか?
そもそもどうやってrefreshしているのか。
あと少しだ。
多分、retryの発火はここでやっているはず。HttpCommandのdo_retry
def do_retry func, client
begin
Retriable.retriable tries: options.retries + 1,
max_elapsed_time: options.max_elapsed_time,
base_interval: options.base_interval,
max_interval: options.max_interval,
multiplier: options.multiplier,
on: RETRIABLE_ERRORS do |try|
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
# auth to be re-attempted without having to retry all sorts of other failures like
# NotFound, etc
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
send(func, client).tap do |result|
if block_given?
yield result, nil
end
end
end
end
rescue => e
if block_given?
yield nil, e
else
raise e
end
end
end
いかにもみたいな感じ。
Retriable.retriable tries: auth_tries,
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
on_retry: proc { |*| refresh_authorization } do
多分このonの中の例外になったら on_retryするとかじゃないの?
refresh_authorizationはというと
# Refresh the authorization authorization after a 401 error
#
# @private
# @return [void]
def refresh_authorization
# Handled implicitly by auth lib, here in case need to override
logger.debug('Retrying after authentication failure')
end
overrideしているのかと思い、googleauth覗きに行ったけどない。
振り出しか?
あれかな、project_id指定しないといけないのか?やってみるか。
結果変わらず。
なんでだよ。
これは3章か
結論
drive.authorizationに指定できるのはこれまでに3つわかった
- oauth2.0で取得した access_tokenをそのまま直指定
- Google::Auth::BearerTokenCredentials.new({token: access_token})
- Google::Auth::UserRefreshCredentials.new (これはrefreshのやり方は未定)
このauthorizationに設定できるのは apply!(hash)メソッドを持っており、
そのapply!メソッドが内部でもらったハッシュの[:authorization]に "Bearer #{token}"を入れる
てことをしてる。
次回は どうしたらrefreshできるか。それがわかれば あとは 枝葉。
list_filesとかの使い方は sourceコード見ればわかる。