REST で Web サービスを構築する場合に、いつも認証が必要なリソースの URI の設計で悩むので、今までの経験上こうしたら上手くいったというのをメモしておきます。

RESTful でもなければ、こんなの世に広めるなよ、害悪だ。っていう批判もあるかもしれませんが、Web の世界でセキュリティと上手く付き合っていくために、セッションを使って認証管理をすると何かといいことが多いので。

あとは、認証が必要な Web API に無理に REST を適用しなくてもいいんじゃないか? みたいな所もあるかと思いますが。

認証が必要な API を REST っぽく作るメモ

REST とは

REST に馴染みのない人はまずこっちをみてください。

参考

  • Webを支える技術 -HTTP、URI、HTML、そしてREST
  • 山本 陽平 (著)
  • 技術評論社
  • RESTful Webサービス
  • Leonard Richardson (著), Sam Ruby (著), 山本 陽平 (監修), 株式会社クイープ (翻訳)
  • オライリー・ジャパン

認証が必要な API の URL 設計

RESTful なサービスでは、ステートレスが望ましいとされているみたいですが、現実的には認証済みのユーザしかアクセス出来ないリソースや、権限を持っているユーザしか更新できないリソースっていうのも多くあります。

Cookie を使ったセッション管理の罪はステートをサーバ側に保持してしまう、ステートフルな状態をつくってしまうことというのは十分理解した上で、それでもやっぱりセッションで認証状態を管理する方法が分かりやすくて Web 屋には使いやすいと個人的には思っています。

そこで、REST を完全に理解したわけではないし、こういう使い方は多分 RESTful ではないんだろうけれども、セッションで認証情報を使った Web サービスを作る上でのポイントをメモしておきます。

認証が必要なリソースは、認証済みの状態を元に URI を設計する

認証した人から見た URI か誰から見ても同じリソースを表す URI か

たとえば、Twitter を思い浮かべてください。Twitter API をつかって自分のタイムラインを表示する場合、Twitter では次のような URI を使います。

http://api.twitter.com/1/statuses/home_timeline.format

この URI は認証が必要な URI になっています。この URI には”誰の”タイムラインを取得するかの情報は入っておらず、home_timeline、つまり自分のホームのタイムラインを取得する URI になっています。

“自分の”とは、認証した人という意味になります。

この URI は次のような URI になるように設計しても良いはずです。(※ 説明のための例なので、実際に Twitter にこの URI でアクセスしても繋がらないです。)

http://api.twitter.com/1/statuses/hamasyou/home_timeline.format

hamasyou のタイムラインを表したリソースです。認証した人が誰かという事は関係なく、常に同じリソースを表すようにしたものです。

ただし、認証した人 = hamasyou ではない場合には、この URI へアクセスしても、401 Unauthorized を返します。

認証が必要な URI は、認証した人から見た URI になるように設計するほうがよい

同じリソースを表す URI はいくつあっても構わず、唯一の URI である必要はないというのが REST の考え方ですので、どういうふうに URI を設計してもよいとは思います。

が、僕の経験上ですが、上のような認証が必要なリソースを表す URI は、認証した人から見た URI として設計するのがよいです。

こうしておくと何が良いかというと、セキュリティを保ったままコードがきれいに書けることが多いからです。

認証が必要なリソースは、常にアクセス権限を意識しなければならない

Ruby on Rails で作られたブログサービスを例にとります。例えば、次のようなルーティングでアクセスする機能があったとします。

/blogs/:blog_id/articles/:id.:format

これは、あるユーザが所持する :blog_id のブログの :id で表される記事を取得するルーティングです。このルーティングを定義すると

GET     /blogs/:blog_id/articles/:id.:format
POST    /blogs/:blog_id/articles
PUT     /blogs/:blog_id/articles/:id
DELETE  /blogs/:blog_id/articles/:id

という HTTP リクエストと対応する機能のルーティングが定義されます。

この時、GET 以外のリクエストはすべて :blog_id を所持するユーザからのリクエストしか受け付けたくないとします。よくある会員サービスはこういう形になると思います。

:blog_id はリクエストで送られるべきではなく、認証情報から取得するべき

上のようなリクエストを受け付けたとき、やりがちなのは次のようなコードです。

blog = Blog.find(params[:blog_id]
blog.articles.create(params[:id])

こうしてしまうと、:blog_id が認証した人の所持するブログと違う場合でも、認証さえ通っていれば別の人のブログの記事を触れてしまうことになります。

これは、正しくは次のようにする必要があります。

user = User.authenticated_user_from_session
blog = user.blog
blog.articles.create(params[:id])

authenticated_user_from_session は認証済みのユーザを取得するメソッドと考えてください。このように、きちんと認証した人に紐づくブログを取り出して、その記事を触るということをする必要があります。

認証情報から引ける情報を URI に含めると不要な処理が増える

上で見たように、セキュリティを考慮すると、認証情報から操作対象のリソースを引っ張る必要がでてきます。このとき、URI に :blog_id が含まれていると

  • URI に :blog_id が含まれているので、自分以外のブログも更新できるんだな」と考えられてしまったり、
  • URI には :blog_id が含まれているけど使わない」という実装になって、URI の表現と挙動が一致しなくなってしまったり、
  • 認証情報から引けるブログの id と URI の :blog_id を毎回比較してエラーチェックをする」といった不要な処理を書くようになってしまったりします。

RESTful ではないかもしれないけど、認証にセッションをつかうなら

このように、認証が必要なリソースへのアクセスに、認証情報から引ける情報を含めてしまうと、いろいろとめんどうくさいことになりがちです。なので、個人的におすすめするのが、次のように URI を設計する方法です。

  • 1) リソースごとに認証が必要かどうかを考える
  • 2) 認証が不要なリソースの場合、URI にはリソースへアクセスするのに必要なパラメータをすべて含めるようにする
  • 3) メソッドごとに認証が必要なリソースの場合、認証が必要なメソッドに関しては認証情報から引ける情報は含めないようにする
  • 4) または、認証が必要なリソースを表す URI を別に作成してそちらにリダイレクトする
2の例)
GET     /wiki/:page_name.:format
PUT     /wiki/:page_name
DELETE  /wiki/:page_name
3の例)
GET     /blogs/:blog_id/articles/:id.:format
POST    /articles
PUT     /articles/:id
DELETE  /articles/:id

こんなふうに考えると、認証が必要なリソースも上手く扱えるんじゃないかと思います。

他に、上手くやる方法を知っている方がいれば、是非教えていただけると助かります。いつも認証周りの設計で苦労するので。。。