開発時に設計がライブラリに引きずられると良いことない

前置き

いま仕事で公開 API を開発していて最初の認証機能を作っています。

OAuth のクライアントクレデンシャル認証フローで認可フローがない形式でアクセストークンを発行してサーバー側の認証を行うために OAuth を採用しています。

これはあくまでもユーザーが意識しないサーバー側のためのもので、ユーザーはメールアドレス・パスワードで認証する必要があり、その部分はライブラリを使いたいという思惑がありました。

認証部分をスクラッチするのは難易度が高い以前にセキュリティのリスクが高いためです。

そこで使ったライブラリは、devise_token_auth というライブラリです。 実際は devise をラップした形のライブラリで依存関係に devise もあるので devise の機能も使えます。

https://github.com/lynndylanhurley/devise_token_auth

内部で使う API の認証には devise_token_auth を使ったトークン認証が使われており、そのまま横展開しやすい状態でした。

ですが、進めていく上でいくつか課題にあたりました。

ユーザーの認証はメールアドレス・パスワード認証だが、トークンには OAuth のアクセストークンを使う

まず最初にこの部分を勘違いしていました。

頭の中で完全に認証=devise_token_auth を使うとなっていたのですが、実際に認証後に使うトークンは OAuth のアクセストークンでした。

ここを理解せずにスパイク実装し始めて途中で気づきました・・・

じゃあ、完全に devise_token_auth はいらないのか?

メール認証するためにはメール送信やパスワード管理など devise のライブラリで使う機能は必要なので、完全になくすという方向には自分の中でなりませんでした。

ただ、実装する際に色々な課題が出てきました。

  • routes の mount_devise_token_auth_for は必要?
  • コントローラーは継承する必要がある?

今回やりたかったのは、devise の database_authenticatable, confirmable などを使って認証周りのあれこれはライブラリに任せたいということでした(他にも細かいのはたくさんあります)。ですが、内部実装を知らないと結局必要かどうか判断できないためコードを読み漁りました。

routes の mount_devise_token_auth_for は devise 用のルートを生成したり、Devise の mappings を作ってくれます。 しかし、コントローラー単位では制御できますが、アクション単位では制御できなかったり公開用に使うには難しい部分がありました。 また、Devise の mappings がないとコントローラーを継承した場合にエラーになるという部分や継承せずにやろうとすると結局色々なコードを持ってこないといけなくなるということもあり、まずは継承する形で進めました。

mount_devise_token_auth_for のアクション単位で制御できないという部分は devise_scope と組み合わせることで解消出来ました。

例えば、こんな感じです。

# Devise.mappings だけ行い、ルートの生成は全てスキップ
mount_devise_token_auth_for "User", at: "", skip: [:sessions, :registrations, :passwords, :confirmations,
                                                    :token_validations, :omniauth_callbacks, :unlocks]
# 独自ルートを用意する
devise_scope :user do
  post "/users", to: "registrations#create"
end

ですが、実装を進めていく中で継承でやる場合に大変な部分もありました。

  • 基本は コントローラーのアクションで super して、スーパークラスのメソッドをオーバライドして使うが、歴史的経緯でプロフィール情報は User で認証は UserEmailAuth という形。
  • メール認証をリンクをクリックではなく、最近よくある認証コードを発行する形にしたいような要件が出てくると結局自前コードになってしまう

最初の User 系のモデルが複数あるという問題は @resource を UserEmailAuth にすることで解決しました。

認証コードを発行する形にするという部分については、カスタマイズが必要になった場合に結局コントローラーは全てモンキーパッチ的になってしまうという部分でリスクと隣り合わせだなと思いました。

最終的には コントローラーは継承しない形になった

今回は公開 API で認証にメールアドレス認証を使いたいという目的があり、その手段として devise_token_auth を使うという形で進めてきたものの、先程の通り今はたまたまやりたいことが一緒でコードが似通っている(結構不要な部分も多かった)という状況で、今後のことを考えると結構なリスクを抱えていた気がします。

最終的には、ライブラリに必要なモデルのモジュールなどは活用しつつコントローラーは継承しない形で必要なコードを持ってくる形で必要最低限な形で今の目的にあったコードで今後もカスタマイズしやすい形となりました。

OAuth のアクセストークンで current_user も識別するので、devise_token_auth で内部的にやっている sign_in やトークンは不要なので、route も依存しない形になりスッキリしました。

まとめ

実装観点で考えると圧倒的にライブラリをそのまま使う方がレールに乗れた場合は工数が少なくすみますが、自分たちの目的からどういうインタフェースが必要でというどちらかといえばトップダウン的に考えていくと今回のケースはもっと早く気付けたのかなと思いました。

どこかでライブラリに依存せずに抽象化しておいて、いつでも差し替えられる状態にしておくみたいなポストを見た気がしましたがまさにその通りですね。。まだまだ抽象化力が足りないこの頃です。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。