2020年まとめ

2020年は特異な年だったというのもあり、個人的なことも含めて2020年にあったことをまとめた。

仕事

リモートワーク

2020年1月15日に日本での新型コロナウイルス感染者が確認されたあと、国内ではかなり早い段階で所属企業を含むグループ全社でリモートワークが始まった。その開始をなぜかTwitterを通じて知り、「報道で知る」という感覚を初体験した。

結局ペパボはリモートワークを原則とする勤務体制に変更された。会社関連の用事で渋谷に行ったのは3回だけだった*1

ずっと家にいるので、仕事用の机のまわりの設備を改善した。とはいえ、基本的にMacBook Pro1台だけで作業することにしているので、充電器と椅子とデスクライトを買った程度。

定額給付金はBaronに充てさせてもらった*2。会社の椅子がBaronだったのでそれに合わせた。シートタイプはクッションにした。

仕事の能率を上げる方法として古典的だがポモドーロテクニックを試していた。

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

いったん原典のとおりに厳密にやっていて、新規に開発しているときなどはハマっている感じがしてよかった。調査業などとはどうしても相性が悪い感じがしている。最近はタスクリストの書き出しとポモドーロの計測だけという緩い運用になっていることが多い。

マンション自体の作りや立地の問題で、自分にとっていまの家はリモートワークに向いていないと感じている。そのわりに家賃がやたらと高く、来年で契約更新になるという事情もあるので、2021年の上期には引越したいと考えている。

業務

サービス基盤チーム所属という肩書きで、Eコマースプラットフォーム*3のWebアプリケーションレイヤを全般的に改善するという役回りだった。所属しているチームの概要についてはhrysdがスライドにまとめてくれている

主にサービスにおける事業者側の玄関口にあたる認証/認可部分のセキュリティ向上や新しいアーキテクチャへの移行に取り組みつつ、Webアプリケーションの細かい改善をやっていた。また、コロナ禍でEコマースの需要が高まったことを背景に、一時的ではあるが久しぶりに新機能をチーム開発したりもした。

15年の歴史を持つサービスの認証/認可の設計改善についてはなかなか進めづらいものだが、今年はセキュリティ向上の作業に抱き合わせることで(?)よりよいアーキテクチャに移行していけそうに見えたので、理想とするアーキテクチャの案を上長や各ロールを担当するエンジニアと共有し、みんなで協力して今年やるべき分については開発を進められたのでよかった。しかし、この分野はとにかく気を使うので大変と感じる。OAuth 2.0の認可サーバの実装だけでも、ライブラリを入れるだけではなくSecurity Best Current PracticeをはじめとするRFCを読んだり情報収集したうえで、適切に実装するのが必須であるということを身を持って感じた。令和の時代に新規にWebアプリケーションを作るなら認証/認可は原則IDaaSに任せるのがいいのだろうなという気持ちになっている。

また、下期はWebアプリケーションを運用し続けると溜まる問題(省みられないエラー通知、古いライブラリのバージョンアップなど)の解決に発生しがちな属人性を減らして、チームや事業部レベルで解決できるように進めかたを考えて、まずはチームで実践したりしていた。ここは道半ばなので引き続き。

アウトプット

記事

個人ブログの記事は14件。だいたい月末になにか書くことがないか考えて書くという感じで、テーマがバラバラだし内容も未来の自分向けが多い。そういえば年初はHaskellを勉強していたけど、すっかり触らなくなってしまった。

2020-01-01から1年間の記事一覧 - kymmt

会社のブログにWebアプリケーションのセッション管理に関する記事を書いた。会社の名前を借りてWebに情報発信させてもらえるということもあり、個人ブログよりはよっぽど読まれたようだった。

SPA+SSR+APIで構成したWebアプリケーションのセッション管理 - ペパボテックブログ

その他はQiitaにtipsレベルの記事を数本書いたぐらい。

OSS

自分のOSSにもらったPRを対応したり、業務の関係上doorkeeper-gemのgemをよく読んだり使ったりしていたので、そのあたりで少しだけ貢献した。

仕事の関係で小さいgemを作成した。

総じてOSSをバリバリやる感じではなかった。

その他

読書

79冊読んでいた*4

Yamamoto Kōheiさんの読書記録

技術書はOAuth/OIDC関連の本が多め。Auth屋さんの本には助けられた。なんとなく量子コンピュータが気になって関連する本を数冊読んだが、手を動かさずに終わっている。

6月ぐらいにKindleでちくま新書のセールをやっていたときに畑違いの分野の本をここぞとばかりに買って読んでいた。また、Kindle Oasisを買って一般書の読書体験はかなり向上した。電子ペーパーのほうが圧倒的に紙に近く読みやすいし、片手でそれなりの画面サイズのデバイスを持って読み進められるのが個人的に体験がよく感じる。

Emacs

Emacsを使うのをやめて、Visual Studio Codeだけで開発するようになった。また、これまで雑にメモを書いたり一時的に文字列を置きたいときはEmacsの*scratch*バッファを使っていたが、いまはSpotlightでTextEdit.appを立ち上げて用を済ませている。

Emacsとの出会いは大学に入った2008年4月*5で、本格的に使い始めたのは研究室に入ってまともにプログラミングをし始めた2011年〜2012年の間だと思う*6。当時『Emacs実践入門』の初版が出たころで、開発環境整備の重要性はこの本から教わったように思う。その後、時代は移り変わり、VS Code主流の世の中でも惰性でEmacsを使っていたが、リモートワーク下での開発作業でVS CodeのLive Shareを使いたい場面があり、そのために自分がボトルネックになるのはよくないので、いい機会と思って移行してしまった。この記事もVS Codeで書いている。

Awesome Emacs Keymapを使っているので、まだ脱Emacsし切っているとはいえないが、手元のマシンからはEmacsをアンインストールした。

Try to quit using Emacs for a time · kymmt90/dotfiles@85ca748

健康

通勤がなくなったので、朝の散歩と軽い自重トレーニングは毎日やっている。一念発起して、花粉症の治療のために6月ごろからシダキュアを服用しはじめた。月1回通院があるが散歩だと思って通っている。シダキュアはHabitifyで見る限り202日連続服用できていて挫折はしていない。

悪いできごととしては口腔外科系の病気になってしまったので、来年の医療費がどうなるか読めず不安感がある。また、生活環境が変わった影響か息抜きがうまくできておらず、そのわりに精神的に焦るだけで何もできていないという自覚があるので、来年は改善したい。

所感

総じて大変な年だった。まだまだやれていないことや知らないことだらけなので来年もがんばりたい。


*1:荷物の回収、感染拡大が落ち着いていた時期に久々にチームで顔合わせ、インフル予防接種

*2:14万円弱したので足は出ている

*3:https://shop-pro.jp / https://colorme-repeat.jp

*4:漫画を含む。再読した本が計算に入ってなさそうで、実際は80冊は超えていると思う

*5:大学の演習室にあったのはLeopardの入ったMac Proで、演習の授業の初回でEmacsが導入された

*6:研究室の支給PCはWindows 7が入っていたので、最初は雰囲気でxyzzyを使っていた。そのあとNTEmacsに移行した

Railsエンジンのappディレクトリ配下のクラスを親アプリでオーバーライドする

やりたいこと

Railsエンジンのappディレクトリ配下に存在するクラス(モデルやコントローラ)のメソッドをオーバーライドしたい。

結論

RailsガイドのRailsエンジンについての記事に全部書いてある。Railsエンジンのapp配下のオーバーライドは、to_prepareを使って、親アプリの初期化が終わったあとに実行する。オーバーライドするクラスはclass_evalでリオープンする。

# config/application.rb
module TestApp
  class Application < Rails::Application
    # ...

    # アプリの初期化が終わったときに呼ばれるフック
    config.to_prepare do
      # もしZeitwerkなら`require_dependency`が非推奨なので`load`を使う
      require_dependency Rails.root.join('lib/monkey_patch/foo_bar_engine.rb')
    end
  end
end
# lib/monkey_patch/foo_bar_engine.rb
module MonkeyPatch
  module FooBarEngine
    def do_something
      # 上書きする
    end
  end
end

FooBarEngine::FooBarsController.class_eval do
  prepend ::MonkeyPatch::FooBarEngine
end

詳細

Railsガイドを読めば問題は解決するのだが、思いつく他の方法で試してみて、なぜだめだったかを見てみる。

オーバーライドに失敗する例: その1

親アプリ側のディレクトリに次のようなコードを書くと、このアクションに対応するエンドポイントにリクエストを送るとき、AbstractController::ActionNotFoundのエラーになる。

# app/controllers/foo_bar_engine/foo_bars_controller.rb
module MonkeyPatch
  module FooBarEngine
    def do_something
      # 上書きする
    end
  end
end

module FooBarEngine
  class FooBarsController
    prepend ::MonkeyPatch::FooBarEngine
  end
end

開発環境では、定数参照時にconst_missingになるとActive Supportがautoload_pathsからパスの規約などに基づいて定数を探す。autoload_pathsは例えば次のように確認できる:

[1] pry(main)> puts ActiveSupport::Dependencies.autoload_paths
/usr/src/app/app/assets
/usr/src/app/app/controllers
/usr/src/app/app/controllers/concerns
/usr/src/app/app/helpers
/usr/src/app/app/jobs
/usr/src/app/app/mailers
/usr/src/app/app/models
/usr/src/app/app/models/concerns
/usr/local/bundle/gems/letter_opener_web-1.4.0/app/assets
/usr/local/bundle/gems/letter_opener_web-1.4.0/app/controllers
/usr/local/bundle/gems/letter_opener_web-1.4.0/app/models
/usr/local/bundle/gems/devise-4.7.3/app/controllers
/usr/local/bundle/gems/devise-4.7.3/app/helpers
/usr/local/bundle/gems/devise-4.7.3/app/mailers
/usr/src/app/spec/mailers/previews
=> nil

ここではletter_opener_webやdeviseなどのRailsエンジンのapp配下もautoload_pathsの後ろのほうに入っている。

FooBarsControllerを読み込んでいないとき、アクション実行時にFooBarEngine::FooBarsControllerという定数を解決することになる。autoload_pathsに従うとRailsエンジンより先に親アプリの定義を見てしまい、中身がほぼ空のコントローラのアクションを呼び出してしまってAbstractController::ActionNotFoundになる。

オーバーライドに失敗する例: その2

config/application.rbの末尾でclass_evalでオーバーライド対象のクラスをリオープンしてオーバーライド用のモジュールをprependすると、uninitialized constant FooBarEngine::FooBarsController (NameError)のエラーになる。

# config/application.rb
module TestApp
  class Application < Rails::Application
    # ...

    # これがないとlibにパスが通らない
    config.eager_load_paths << "#{Rails.root}/lib"
  end
end

require_dependency 'lib/monkey_path/foo_bar_engine.rb'
# lib/monkey_path/foo_bar_engine.rb
module MonkeyPatch
  module FooBarEngine
    def do_something
      # オーバーライドする
    end
  end
end

FooBarEngine::FooBarsController.class_eval # この定数が見つからない
  prepend ::MonkeyPatch::FooBarEngine
end

これは、config/environment.rbでconfig/application.rbを読み込んだ時点ではアプリの初期化が終わっておらず、オートロードの準備もできていないので、Railsエンジン配下の定数を探索できないのが理由。

オーバーライドに成功する例

結論に書いたとおり、config.prepare_toフックでオーバーライドする。prepare_toはアプリの初期化が終わった時点で呼び出されるので、オートロードも可能であり、Railsエンジン配下の定数を探索することもできる。

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屋さんが頒布している次の本も読んだ。

authya.booth.pm

この本のいいところは、いわゆる「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決済を入れればまた起こるというものではないし、中国当局の規制が入った