authorization code grantに沿ったDoorkeeperのコードリーディング

さまざまな都合により、OAuth 2のプロバイダになるためのDoorkeeperというgemのコードを読むことがしばしばある生活を送っている。

似た名前のモジュールやクラスが多く、読むたびに混乱しているので、authorization code grantでアクセストークンを取得するときの登場するクラス/モジュールと流れをあらためて自分なりに整理した。基本的に自分用であって、網羅的ではない。

前提

2020-11-28現在での最新版であるDoorkeeper 5.5.0.rc1を読む。authorization code grantが正常に通るときのパスだけを見る。

RailsのAPIモードは無効とし、Doorkeeperの設定resource_owner_authenticatorで渡すブロックでは特定のリソースオーナーの認証に常に成功しているとする。本来は認証を実際に実行し、失敗すれば再認証させるべき。

以降の文章では、Doorkeeperが提供する名前空間のうちDoorkeeperDと省略する。

Doorkeeper用エンドポイントの登録

DoorkeeperはRails Engineであり、ルーティングを拡張するためのuse_doorkeeperというメソッドが提供されている。このメソッドでルーティングを拡張するまでの流れは次のとおり。

主に登場するクラス/モジュール

名前 概要
D::Engine Rails EngineとしてRailtieのinitializerを設定する
D::Rails::AbstractRouter Doorkeeper用ルーティング拡張クラスのためのインタフェースを表す
D::Rails::Routes 親アプリのルーティングにDoorkeeper用のエンドポイントを追加するメソッドを持つ

ルーティングの設定フロー

  • D::Engine"doorkeeper.routes"としてinitializerを登録する
    • 親アプリの初期化時にD::Rails::Routes.install!を実行する
      • ActionDispatch::Routing::Mapperuse_doorkeeperをincludeすることでルーティングの設定でuse_doorkeeperできるようにする
  • Doorkeeper利用時に親アプリのconfig/routes.rbでuse_doorkeeperする
    • D::Rails::Routes#generate_routes!を実行する
    • Railsのscopeを呼び出して、その中で D::Rails::AbstractRouter#map_routeによってDoorkeeperのエンドポイントを定義する
      • D::Rails::Routesのprivateメソッドで個々のルーティングが定義されており、それらのメソッドをsendで呼び出している

authorization code grantの認可リクエスト

authorization code grantでは、あるクライアントとして認可リクエストを送り認可コードを得る必要がある。

主に登場するクラス/モジュール

D::RequestD::OAuthそれぞれの配下に似たような名前のモジュールやクラスがあって混乱する。

コントローラ関連

名前 概要
D::AuthorizationsController /oauth/authorizeへのリクエストがルーティングされるコントローラ
D::Helpers::Controller Doorkeeperの設定をもとにした値などを取得するためのメソッドが集められたモジュール

認可リクエスト

名前 概要
D::OAuth::PreAuthorization 認可リクエストのパラメータのラッパークラス。バリデーションを実行したりscope文字列をパースする
D::Validations D::OAuth::PreAuthorizationD::OAuth::BaseRequestでのバリデーションの仕組みを提供するモジュール
D::Models::AccessTokenMixin アクセストークンに関するロジックを提供するモジュール。ORマッパーへの依存を減らすために、アクセストークンのモデルからは切り離されている
D::OAuth::Hooks::Context 認可前後のフック関数に渡すコンテキストを表すクラス
D::Server 認可サーバとして必要なリクエスト、パラメータ、現在のリソースオーナーやクライアントへアクセスするためのメソッドを提供するクラス。コントローラをコンテキストとして渡して使う
D::Request response_typeを渡して、対応する認可/トークンリクエストを処理するストラテジクラスを返すためのメソッドを提供するクラス
D::Request::Strategy リクエストをもとに認可するストラテジクラスの基底クラス。#authorizeというメソッドを提供する
D::Request::Code D::Request::Strategyを継承するauthorization code grantのストラテジ。#requestではD::OAuth::CodeRequestはインスタンスを返す。D::Request::Strategy#authorizeを呼ぶと、そのインスタンスに#authorizeを委譲する
D::OAuth::CodeRequest 認可コードをD::OAuth::Authorization::Codeのインスタンスとして生成して、認可エンドポイントのレスポンスを作成する
D::OAuth::Authorization::Code 認可コードのラッパークラス。認可コードを発行しグラントを記録するテーブルへ保存する#issue_token!を提供する
D::OAuth::CodeResponse 認可エンドポイントのレスポンスのラッパークラス。コールバックまたはネイティブアプリ向けの方法で認可コードをクライアントに渡すために必要なデータを提供する
D::GrantFlow D::GrantFlow::RegistryにOAuthのグラントの種類とDoorkeeperのストラテジークラスの対応を登録するモジュール
D::GrantFlow::Flow D::GrantFlowで登録する対応を表すクラス

承諾画面の表示

GET /oauth/authorizeを呼び出すときの流れ。

まず、リソースオーナーのデータを取得する。

  • D::AuthorizationsController#newへルーティングする
  • before_action :authenticate_resource_owner!を実行する
  • D::Helpers::Controller#current_resource_ownerを実行する
  • 親アプリのconfig/initializers/doorkeeper.rbなどでD.configureで設定する authenticate_resource_ownerのブロックを呼び出し、返り値を@current_resource_ownerへ入れる

次に、認可エンドポイントへのリクエストを検証する。

  • D::AuthorizationsController#new#pre_authを呼び出す
    • Doorkeeperの設定、認可リクエストのパラメータ、@current_resource_ownerをもとにD::OAuth::PreAuthorizationのインスタンスを作る
  • pre_auth#authorizable?を実行する
    • D::Validations#validateを実行する
      • あらかじめD::OAuth::PreAuthorizationの序盤で定義しているvalidate :client_id, ...などはこのモジュールのメソッドであり、バリデーションを登録している
      • 登録されたバリデーションを順番に実行する
        • バリデーションメソッドはvalidate_#{属性名}sendする
        • これらもD::OAuth::PreAuthorizationにあらかじめ定義してある
        • それぞれのバリデーションはOAuth 2に基づいたもの

リクエストが妥当であれば、クライアントへ承諾画面を返す。

  • authorizable?であればrender_successを実行する
    • D::Helpers::Controller#skip_authorizationを実行する
      • 認可スコープの承諾画面を表示するか否かを決める
      • Doorkeeperの設定のskip_authorizationのブロックを実行する
    • #matching_tokenを実行する
      • D::Models::AccessTokenMixin#matching_token_forですでに対象のクライアントとリソースオーナーの組み合わせで作成済みのアクセストークンを探す
    • #skip_authorization#matching_tokenのどちらかがtrueなら、すぐにauthorize_responseで作成する認可済みのレスポンスを返す
    • そうでなければ:newをレンダリングする
      • app/views/doorkeeper/authorizations/new.html.erbをレンダリングする
      • リクエストしているスコープを表示し、承諾もしくは拒否を求める画面

認可コードの発行

承諾画面で承諾をサブミットし、POST /oauth/authorizeを呼び出すときの流れ。

認可コードを生成する。

  • D::AuthorizationsController#createにルーティングする
  • #authorization_responseを呼び出す
    • pre_authをもとにD::OAuth::Hooks::Contextのインスタンスを作る
    • フックbefore_successful_authorizationを実行する
    • #strategyを呼び出す
      • #serverを呼び出す
        • D::Helpers::Controller#serverを呼び出す
        • コントローラ自身をcontextとして渡してインスタンスを作る
      • Server#authorization_requestを呼び出す
        • 引数に pre_auth.response_typeを渡す。いまは"code"
        • D::Request.authorization_strategyから"code"に対応する認可strategyクラスを取得する
          • D::GrantFlowで各グラントのstrategyクラスなどは設定済み
          • Doorkeeperの設定にあるgrantから対応するものをD::GrantFlow::Flowとして取り出す
          • request_type_strategyを呼び出してD::Request::Codeを返す
          • D::Request::Codeにserverを渡してストラテジーオブジェクトを作る
      • strategyとしてD::Request::Codeのオブジェクトが得られた
    • strategyであるD::Request::Code#authorizeを実行する
      • D::Request::Strategy#authorize#requestD::OAuth::CodeRequest#authorizeと委譲される
      • pre_authresource_ownerを引数に取ってD::OAuth::CodeRequestのインスタンスを作る
      • D::OAuth::CodeRequest#authorizeD::OAuth::Authorization::Codeのインスタンスを作り#issue_token!を呼び出す
        • 認可コードを生成して、既定のActive Recordモデルを通じてテーブルに保存する
      • D::OAuth::CodeResponseのインスタンスをpre_authD::OAuth::Authorization::Codeのインスタンスを渡して作り、returnする

認可コードをコールバックURIに付与するかネイティブアプリ用画面のURIのパラメータとして返す。

  • 認可コードを返すためにredirect_or_render authorization_responseする
    • D::OAuth::CodeResponseのインスタンスであるauthorization_responseredirectable?なら、そのURIへリダイレクトする
    • 認可コードのときは常にtrueなので、oobであればoob用のURIに、それ以外は設定済みのURIに、認可コードをパラメータとして付けてリダイレクトする
      • oobのときD::OAuth::Authorization::Code#oob_redirectをもとにリダイレクトし、app/views/doorkeeper/authorizations/show.html.erbをレンダリングする

トークンエンドポイント

主に登場するクラス/モジュール

認可エンドポイントで登場したものは省略。

名前 概要
D::TokensController /oauth/token へのリクエストがルーティングされるコントローラ
D::Request::AuthorizationCode D::Request::Strategyを継承するauthorization code grantのストラテジ。#authorizeを提供する。Strategyでの#authorize#requestへの委譲時にD::OAuth::AuthorizationCodeRequestを生成する。そのときに#grantの呼び出しを通じて認可コードの検証を実行する
D::OAuth::BaseRequest トークンエンドポイントへのリクエストの基底クラス。#authorizeでトークンレスポンスの生成と前後のフックの実行を提供する
D::OAuth::AuthorizationCodeRequest authorization code grantでのトークンエンドポイントへのリクエストを表すクラス。PKCEのchallengeの検証も担う。フックD::BaseRequest#before_successful_responseをオーバーライドしてアクセストークンを作成している
D::Models::AccessGrantMixin アクセスグラントに関するロジックを提供するモジュール。ORマッパーへの依存を減らすために、アクセスグラントのモデルからは切り離されている
D::OAuth::TokenResponse トークンエンドポイントのレスポンスのラッパークラス。ステータスコードやレスポンスのJSONを取得できる

アクセストークン取得の流れ

POST /oauth/tokenを呼び出して、アクセストークンを含むJSONをレスポンスとして得る。

  • D::TokensController#createにルーティングする
  • #authorize_responseを呼び出す
    • 認可エンドポイントと同じ流れでserverを取得しD::Server#token_requestを呼び出す
      • 引数としてparams[:grant_type]を渡すが、ここでは"authorization_code"
      • D::Request.token_strategyであらかじめ登録済みのstrategyからgrant_type_strategyとしてD::Request::AuthorizationCodeを取得してreturnする
      • そのクラスのインスタンスを得る
    • D::Request::AuthorizationCode#authorizeを呼び出す
      • D::Request::Strategy#authorize#requestD::OAuth::AuthorizationCodeRequest#authorizeD::OAuth::BaseRequest#authorizeと委譲される
      • D::Request::AuthorizationCode#requestgrant呼び出し時に認可コードをもとにoauth_access_grantsのレコードを探している
        • D::Models::AccessGrantMixins.by_tokenで実行している
      • D::OAuth::BaseRequestD::OAuth::ValidationsをincludeしているのでD::OAuth::PreAuthorizationと同じく宣言済みのバリデーションをvalid?で実行できる
      • valid?ならトークンレスポンスを返す
        • D::OAuth::AuthorizationCodeRequest#before_successful_responseで認可コードをrevokeしてアクセストークンを生成する
        • D::OAuth::TokenResponseにアクセストークンを渡してインスタンスを作る
      • トークンレスポンスのオブジェクトをreturnする
  • D::TokensController#createでトークンレスポンスから#body#statusでトークンレスポンスのJSONのステータスを取得してrenderする

所感

DoorkeeperはOAuthの各グラントに対応し、またORマッパー非依存になるような設計で作られていて、さまざまな要件のもとでOAuth 2サーバを作りたいという希望にかなうライブラリとなっている。そのぶん、やっていることが複雑であったりもするし、細かいカスタマイズを施したくなる場面もたびたびある。また、認可という場合によってはクリティカルな機能に関わるライブラリでもある。そういう点で、ただのブラックボックスとして扱うよりは、できるだけ内部を知っておいたほうがいいだろうと思う。どのライブラリにも言えることではあるが、アプリケーション開発の延長として、ライブラリの新バージョンリリース時などのタイミングでこまめにコードを読むことを継続していく。

OpenID Connectについて知るときに読んだもの

OpenID Connect (OIDC)がどういうものか、どう使うのかについて知るために読んだものについてまとめておく。前提として、OAuth 2を利用したアプリケーションの開発経験はあるとします。

OAuth 2

OIDCはOAuth 2を拡張した認証プロトコルとなっている。前提に書いたとおりOAuth 2を使ったアプリケーション(認可サーバ、クライアント両方)の開発に携わった経験はなんどかあったものの、念のため復習した。

OAuth 2 in Action

OAuth 2をさらうために、網羅度が高そうな"OAuth 2 in Action"を読んだ。邦訳は『OAuth徹底入門 セキュアな認可システムを適用するための原則と実践』。今回は気まぐれでなぜか洋書のほうを買ってしまった。

OAuth 2の各グラントタイプを、認可サーバ、クライアント、リソースサーバーそれぞれの役割と認可のフロー、それらを実現するコードで説明している。コードは基本的にJavaScriptで書かれていて、GitHubにもリポジトリ(oauthinaction/oauth-in-action-code)がある。さらに、認可サーバ、クライアント、リソースサーバそれぞれについて、よくある脆弱性とその対策を説明している。

記述がよくも悪くも冗長なので、コードやシーケンス図を読んで、わからないところがあれば本文で補完していくような読みかたがいいかもしれない。

OpenID Connect

OAuth 2 in Action

上で紹介した"OAuth 2 in Action"の後半で、OIDCについて1章割いて説明している*1。OAuthでは誰が認可したのかはクライアントからわかりえないので認証ではない、という話に始まり、他の章と同様に実装を交えてOIDCを説明している。

OAuth、OAuth認証、OpenID Connectの違いを整理して理解できる本

もう少し理解を整理するためにAuth屋さんが頒布している次の本も読んだ。

【電子版】OAuth、OAuth認証、OpenID Connectの違いを整理して理解できる本

この本のいいところは、いわゆる「OAuth認証」から始めて、OIDCがなぜ出てくるかまで次のような流れで説明しているところだと思う。

  • OAuthでは、クライアントからはユーザを認証(つまり使っているのが誰なのか判断することが)できない
    • アクセストークンはクライアントから見るとopaqueだから情報がない
  • プロフィールAPIがリソースサーバにあれば認証に近いことができる(これが「OAuth認証」)が、トークン置き換え攻撃やクライアントの脆弱性で乗っ取りが発生しうる
  • Facebookなどdebug_token APIを導入して、「OAuth認証」をよりセキュアにしているサービスもある
  • これらを踏まえて、OAuthを土台としつつセキュアな認証機能を標準化するのがOIDC

RFCなど

IDトークンの検証方法は『OAuth、OAuth認証、OpenID Connectの違いを〜』にも書いてあるが、OIDCのRFCで説明しているのでそちらも確認した。

Final: OpenID Connect Core 1.0 incorporating errata set 1 - 3.1.3.7. ID Token Validation

また、OAuth 2/OIDCの周辺技術に関して、いくつか文書を読んだ。

OIDCではBearer TokenをプロフィールAPIを含むAPIへのアクセス時のトークン送信で使い、JWTをidentity providerからIDトークンとして取得するので、それぞれ注意点やベストプラクティスをあらためて把握するためにRFCを読んだ。"OAuth Security Current Best Practice"は"OAuth 2 in Action"の脆弱性に関する部分と少し被るが、こちらも生まれうる脆弱性とその対策について広く説明している。

OAuth 2/OIDCのRFCは長大なので、ひとまずライブラリの実装と標準の仕様を比較するときなどに読む方針にしている。

OpenID Connectを実装したライブラリ

コードを読むと理解が深まりやすいので、なにか現実のライブラリを読むのはいいことだと思う。注意点は、あくまでもRFCなどが説明する仕様が正であって、実装はなんらかの差異や不足があると認識しておいたほうがよいこと。それも考慮してコードを読むとコントリビューションチャンスも生まれそう。

今回はdoorkeeper-openid_connectを使ってIDトークンの払い出しやDiscovery APIなど一通り実装し、relying partyとして認証サーバを叩いたりコードを読んで理解を深めた。

所感

はじめはなぜOIDCでIDトークンを検証したら認証したことになるのか理解しきれていなかったが、従来の「OAuth認証」で使うプロフィールAPIやdebug_token APIでやっていたようなことを、UserInfo APIと署名付きのIDトークンでやっていると考えると腑に落ちた。そういう意味では、『OAuth、OAuth認証、OpenID Connectの違いを〜』はそのあたりの観点を提供してくれたので大変助かった。とはいえ、それなりにいろいろ読んだものの、細かいパラメータや様々な拡張仕様など、わからないことはまだまだあるので、必要に応じて身につけていきたい。

*1:Chapter 13. User Authentication with OAuth 2.0

『決済サービスとキャッシュレス社会の本質』を読んだ

Eコマースに関するWebサービスを開発しているのもあって、たまに決済に関する本を読んでいて*1*2、今年は本屋に『決済サービスとキャッシュレス社会の本質』が平積みされているのを見つけたので、読むことにした。

決済サービスとキャッシュレス社会の本質 | きんざい

内容

決済事業者としてサービスを検討するときの実務や注意点を個別サービスに依存しない形で説明することで本質を導き出すとしている。

国際ブランド決済カード(いわゆるクレジットカード)のしくみに始まり、デビットカード、プリペイドカードやSUICAのような決済手段のしくみ、それに加えて今後の展望が書かれている。そのなかで、どうしてそのようなしくみになったのかについての歴史的/業界的な経緯や、新興系の決済サービスに不足している部分の指摘などが詳細に書かれている。

決済ビジネスは「超薄利多売の装置産業」であり、レアケースと思える不正利用がビジネスとしては致命的になるので、最初からそれらのケースを考慮して作っていく必要があると再三にわたって強調されている。また、国際的には、決済のインフラレイヤーについては国際標準にしたがって共通なしくみを各社が使うことでセキュリティ強化やコスト削減を進めつつ、それより上のサービスに関するレイヤーで競争や差別化を図っており、国内の民間企業もそうなっていくべきだと述べている。

著者としては、新興の決済サービス事業者の展開に対して思うところがあるようで、加盟店手数料の安さやポイント還元競争が安易に称賛される風潮に警鐘を鳴らしている。この理由としては、決済サービスで大規模な不正利用が起こったり、アクワイアラーのビジネスが立ち行かなくなることで加盟店への支払いが滞ると、金融機関などに連鎖的に影響が及ぶ「金融システミックリスク」が発生し、経済に影響を及ぼすことによる。従来のクレジットカード会社はセーフティネットとしての役割を果たすために金融システミックリスクを減らす企業努力をしており、そのコストとしてある程度の加盟店手数料を取っているのだとしている。安心安全を大前提として、事業者は責任を果たしつつキャッシュレスを進めることが重要であると述べられている。

所感

全体を通じて著者の知識と経験に基づいて書かれており、詳細かつ当事者でないと知らなさそうな経緯が細かく書かれているのは興味深かった。

決済事業に手がけるWebサービス事業者へのやり方(安価な手数料や高い割引率など特典ばらまき)に懐疑的な意見や、O社に関して業界の会合で関係者がした発言などが書かれてあり、実際にそれらの事業者に金融システミックリスクを考慮できていない点が一部あるのだろうとは思った。また、C社について政府系ファンドからの出資があったことについても懐疑的な意見が書かれていた。このあたりは出典があるような話ではないので、あくまでも著者の主張と捉えて読むのがよいと思った。書きっぷりについては、ある種の講演録のような感じがあり、同じ話がなんども出てくるのが若干冗長だった。

読書メモ

第1章 すべての決済サービスの基本といえる国際ブランド決済カードのしくみ

  • ブランド会社の大事な役割は、仮にアクワイアラー(加盟店管理会社)が倒産しても加盟店に報酬が渡るように補償して、金融システミックリスクを防ぐこと。拓銀倒産時に、その子会社をJCBが買ったエピソードなど。加盟店手数料はこういうところに使われている話から、決済を本業としない新興系サービスの安価な手数料について言及。新興系サービスの構造的に今後もつきまとう問題
  • ブランド会社がネットワークを流れる決済データの仕様やカードの仕様を決めている。IC未対応端末でのICカード不正利用の責任をアクワイアラに課すなど信頼性向上に努めている
  • FeliCaは近接型非接触ICカードに関してはISO仕様ではない。もともとSuicaで使われることを想定していたので、高速に動作する。役所が発行するようなカードにFeliCaを導入してしまうとWTOに訴えられる可能性があるので、マイナンバーカードなどはType-Bを採用している
  • 欧米は銀行を信用していないので、明細を見て自分で最低支払い金額以上の小切手を書いて、手数料を払ってでもリボ払いを使う。クレジットヒストリーを育てる意味合いもある。日本はカード会社を信用しているので、実質後払い一括デビットといえる
  • Eコマースでの不正利用対策としてCVVが使われるようになったが運用上問題が発生しがち。3Dセキュアは加盟店にカード情報を持たせないための方法

第2章 デビットカードと送金サービス

  • 米では口座開設に与信が必要なことや、リボ払い中毒の問題からデビットカードが普及しつつある
  • 日本はJ-Debitというのがあるが、あまり流行っていない。Bank PayがJ-Debitのインフラを利用している
  • デビットカードは特有の考慮事項がある
    • オーソリ時にはあった口座残高が売上確定時になくなるオーバードラフト。銀行側でオーソリ時点での金額分留保で対応することが増えている
    • オーソリ後の売上金額変更
    • オーソリと売上で電文が通るアクワイアラが変わるとき、イシュア側で突合が難しくなる。オーソリと売上を一つの電文にまとめて解決する(シングルメッセージ)。
  • 2016年のセブン銀行ATMからの18億円不正引き出し事件。セブン銀行は悪くなく、引き出しを許可した南アフリカの銀行に問題があった。セブン銀行ATMはIC対応しているし事件のときは最大引出額を下げる対応をとった。そもそも磁気カードは偽造しやすいので、ICにしていくことが大事
  • キャッシュアウトはデビットカードによって店頭で現金を引き出せるので、ATMがない地域で便利

第3章 プリペイドカードと電子マネー

  • 日本における電子マネー(Edy、Suica、nanaco、WAONなど)の発展の経緯、国内外でFeliCaの利用実績にズレがあることや、Apple PayがFeliCa対応してしまったことの功罪について、LINE Payカードなどブランドプリペイドカードについて

第4章 新たな決済サービスの動向とキャッシュレス社会の展望

  • 中国のキャッシュレス事情、新興系決済サービスやQRコード決済について、今後の展望など
  • 2010年代前半の爆買いは人民元の持ち出し制限と銀聯のデビットカード誕生によるもの。そのあとにアリペイやWeChatペイが追随した。単純にQR決済を入れればまた起こるというものではないし、中国当局の規制が入った

APIスロットリングの実現方法

ある期間でのあるWeb APIに対するアクセス回数上限を与えたときのスロットリングについて、一例ではあるがRack::Attack (v6.3.1)で採用されているしくみについて調べた。

ここでは、ある期間において特定のクライアントからのアクセスを一定回数以下に制限することをスロットリングと呼ぶことにする*1。あるAPIにスロットリングをかけるときは、ある期間において経過した時間を計算しながら、その期間中にあるクライアントからエンドポイントに来たアクセスの回数をサーバサイドのキャッシュに記録する必要がある。

結論

スロットリングしたい期間をperiod(単位は秒)とする。

periodで与えられる期間だけ有効なカウンタ」を作ることができれば、そのカウンタにアクセス回数を記録することで、あるクライアントから上限を超える回数のアクセスが来ているかどうか検証できる。つまりスロットリングが実現できる。

カウンタの実現方法

特定期間において経過した時間を計算する

剰余を使ってタイマーを作ることで実現する。

まず、アクセスが来た時点でのUNIX時間t*2を取得する。このとき、tperiodで割ったときの剰余rを計算すると、当然のことながら0 <= r < periodがつねに成り立つ。つまり、1秒ごとに数値が増え、period - 1の次は0に戻るようなタイマーができる。Rubyで書くと次のようになる。

r = t % period # 1秒ごとに増加するタイマー。値は0からperiod - 1を繰り返す

そして、periodからrの値を引くと、1秒ごとにperiodから数値が減り、1の次は値がperiodに戻るようなタイマーができる。

(period - r).to_i # 1秒ごとに減少するタイマー。値はperiodから1を繰り返す

アクセス回数の記録時に、このタイマーから得られる値ををキャッシュの有効期限として設定することで、その期間における残り時間を表現できる。

なお、Rack::Attackでは実装上の理由でタイマーの最大値に1秒のバッファを持たせている*3

特定期間のアクセス回数を記録する

除算を使って、特定の期間だけ得られるキーを作り、そのキーを使ってキャッシュにアクセス回数を記録する。

先ほど取得したUNIX時間tperiodで割ったときの商qを仮に毎秒計算してみると、剰余が0 <= r < periodになる連続した範囲ごとにqの値が同じになる。

q = (t / period).to_i # periodの期間中はつねに同じ値

つまり、ある期間の間はqの値が同じになるので、アクセス元の情報とqの組み合わせをその期間のクライアントからのアクセス回数のキーとすると、あるクライアンのある期間におけるアクセス回数データを一意に特定できる。このキャッシュの有効期限を、先ほど計算した有効期限として設定すればよい。

以上より、periodで与えられる期間だけ有効なカウンタを作ることができる。このカウンタを利用してスロットリングを実現する。

参考

  • Rack::Attack 6.3.1
    • Rack::Attack::Throttle
      • matched_by?から呼び出しているcache.countが「periodで与えられる期間だけ有効なカウンタ」での計数を実現している
    • Rack::Attack::Cache
      • key_and_expirtyで時刻に基づいて経過時間とキャッシュのキーを計算し、do_countでカウンタを操作している

*1:Rack::Attackではthrottlingと呼んでいる

*2:RubyではTIme.now.to_iで取得する

*3:https://github.com/rack/rack-attack/pull/85

ポモドーロ・テクニックを2か月間やってみての感想

2か月ほどポモドーロ・テクニックを使って仕事をしてみたので感想を書きます。

前提

仕事ではWebアプリケーション*1*2の開発が主な業務。最近、会社の勤務体制が原則として在宅勤務に移行した*3

なぜ始めたか

在宅勤務はとても便利だが、家の中でずっと同じ景色で仕事しているので、なんとなく気分として新鮮味がなくなってきていた。

また、これは在宅勤務の前からだが、仕事が進んでいない感覚に陥ることがたまにあった。実際進んでいないわけでもないのだが、これについては、いろいろな仕事をちょっとずつやっている状態だとこういう感覚になるのではという仮説があった。

上記のような経験があったので改善したいと思い、よく見るポモドーロ・テクニックに手を出してみることにした。higeponさんの記事にも影響を受けている。

第4回 プロダクティビティの鬼:継続は力なり―大器晩成エンジニアを目指して|gihyo.jp … 技術評論社

取り組みかた

原典を読む

ポモドーロ・テクニックといえば25分ずつ作業するぐらいのイメージだったので、原典を読んだ。原典はFancesco Cirilloの"The Pomodoro Technique"で、今回は邦訳を読んだ。

ポモドーロ・テクニック入門 | CCCメディアハウス - 書籍

主に「個人としての目標達成」の章を読んだ。とりあえず一人で使う予定だったので「チームとしての目標達成」は流し読みしかしていない。

具体的な方法論に加えて、テクニックについての考案者の思想がいろいろと書いてある。どんどん生まれては過ぎ去っていく時間を「ポモドーロ」という単位で抽象化して取り出すことで、時間が過ぎ去ることに対する不安感を軽減し、一定のリズムに基づいて作業を進められようになり*4、測定やふりかえりもやりやすくなるというのが本質らしい。

ここから後ろの内容はこの本に書いてある用語や方法に基づきます。

原理主義的にやる

だまされたと思って、ひとまず原理主義的にやる。「使う道具をシンプルに保つこと」が強調されていたので、あえて高度なツールなどは使っていない。原理主義的にやるときには次のものを用意する:

  • iOSビルトインの時計アプリのタイマー
  • 「今日やること」(To Do Today)シート
  • 「仕事の在庫」(Activity Inventory)シート
  • 「記録」(Record)シート

Cirillo氏はキッチンタイマーがいいと主張しているが、持っていないのでiOSのものを使う。

「今日やること」シートは大学ノートの1ページを1日分として管理している。ここに優先順位の昇順に並べたタスクの一覧、ポモドーロ完了/中断、突発的なタスクの一覧などを書いていく。ポモドーロを始めてしばらくしてペースがつかめてきたら1日の最初にタスクをポモドーロ数で見積もるようにしたほうが、ポモドーロの目的からして好ましいのだが、最初の数週間はとりあえず記録だけしていた。

今日やることシートの例
f:id:kymmt90:20200729003457p:plain:w300

「仕事の在庫」と「記録」はとりあえずNotionで管理している。両方Notionのdatabaseを使っているが、「仕事の在庫」のほうは単純なタスクリストになっていてNotionである必要もそれほどない。ただ、たびたび追加/削除するので紙ではなくソフトウェア的に管理している。「記録」は毎日の終わりにその日のタスク、見積もったポモドーロ数、実際のポモドーロ数を記録するのだが、これが原理主義的にやるとき一番面倒な作業のように思える。ただ、これがあることで、あるタスクが見積もりとどれぐらいずれているかを見て認識を補正し、やたらとポモドーロ数を消費するタスクを効率的にやる方法を考えるきっかけになる。

感想

中断や休憩を意識するようになる

原理主義的にやる場合、とにかく今やっていること以外のことを考えたり突発的なタスクが舞い込むと中断の印を「今日やること」に記録する必要があり、そのまま集中力を失うとポモドーロ中断なので、嫌でも中断という事柄について意識するようになる。具体的にはSlackが強敵で、作業中はSlackのウインドウを閉じるようにしているが、無意識にウインドウを開こうとするときがたびたびある。このことに気づけたこと自体がよかったとも言える。

また、原理主義だと休憩を25分ごとに3分から5分取ることになる。このときは逆に仕事のことを考えてはいけないので、できるだけ机から離れて家の中を歩いたり水を飲んで休むようにしている。これは結構よくて、毎回のポモドーロで仕事の新鮮味を取り戻すのに役立っている。

4セットという原則を守るのが難しい

ポモドーロは25分作業+5分程度休憩を4セット実行するのが原則*5だが、これを守るのはけっこう難しい。まず、1日のスケジュールを見て、できるだけ4セット連続で実行できるように作業を配置しないといけない*6。また、あるタスクが大きすぎるときは1日で完了できる粒度に分け、反対に小さすぎるときは他のタスクと組み合わせる必要があるので、仕事を始める前に、タスクの分割と優先順位をあれこれ考えるようになる。その代わり、計画を立てて「今日やること」シートに記録することで、確実に仕事が進んでいることが可視化されるので、仕事が進んでいない感覚の軽減には役立っている。また、4セットごとに長めの休憩や軽い散歩をしている。

一方で、事前に計画しても、突発タスクでポモドーロを無視して作業してしまい、リズムが乱れてしまうことがある。このあたりは中断要因を自分で制御できるようになることもテクニックの狙いの一つなので、うまくやっていく必要がある。

その他、先日までいわゆる機能追加のようなプロジェクトに入っていた。このようなケースでは事前にプロジェクトとしてのバックログを作っているので、その中で着手しているタスク、次に着手するタスク、コードレビューをうまく配置すれば、あとは原則にしたがい集中してポモドーロをこなしていけばいいので、かなり捗る感覚があった。

時間と戦ってはいけない

これは原典に書いてあった文句の受け売りだが、時間と戦う、すなわちポモドーロ数(=作業時間)を稼ごうとした時点で負けらしい。要は、雑なポモドーロを続けるよりは、一つ一つのポモドーロの質を高めるほうが好ましいということ。

実際、仕事が完了したかどうかと、完了したポモドーロの数は関係がない。たとえば、ポモドーロの終盤での中断が繰り返されると、仕事はそれなりに進むが、その仕事の完了ポモドーロ数は0のままということになる。そういう状態になっているときは、自分自身の状態や作業の進めかたを気にかけたほうがいいということだろうと思った。また、ポモドーロ・テクニック自体、作業時間を区切って集中するための方法としての側面が強調されがちで、実際能率を上げる効果があるが、作業の進めかたを考えたり、ふりかえったりするための手段という点もかなり考慮されているということに、実際やってみて気がついた。

おわりに

まだ続いているので、飽きるまでは続けます。

*1:https://shop-pro.jp

*2:https://colorme-repeat.jp

*3:https://pepabo.com/news/press/202006011200

*4:本の中では「時間依存の反転」と呼んでいる

*5:残存体力によっては3セットでもよい

*6:難しいときは1ポモドーロだけ分割してしまうことがある。ミーティングなどもポモドーロに算入したほうがいいのかもしれない