#352 Securing an API
- Download:
- source codeProject Files in Zip (95.7 KB)
- mp4Full Size H.264 Video (20 MB)
- m4vSmaller H.264 Video (9.66 MB)
- webmFull Size VP8 Video (7.81 MB)
- ogvFull Size Theora Video (25.9 MB)
先週はエピソード350で販売サイト用のバージョン管理されたAPIを構築する方法を紹介しました。/api/products
というパスにアクセスすると、JSONを介してこのアプリケーションとデータのやりとりが可能です。このAPIは完全に公開されていて誰でも利用可能で商品情報の編集や削除ができてしまいますが、APIへのアクセスには制限をかけるのが通常でしょう。これを実現する方法はいろいろありますが、どの方法を選択するかはアプリケーションの要求によります。今回のエピソードではAPIに制限をかける方法をいくつか紹介し、アプリケーションのスタイルにもっとも合った方法を選択できるようにします。
HTTPベーシック認証を使用する
もっともシンプルな方法はHTTPベーシック認証です。これをRailsで行なうのはとても簡単なので、ほとんどのAPIクライアントで問題なく対応させることができるでしょう。これを使用するにはただ単にAPIを提供するコントローラを修正して、http_basic_authentication_with
の呼び出しを追加して名前とパスワードを渡すだけです。
module Api module V1 class ProductsController < ApplicationController http_basic_authenticate_with name: "admin", password: "secret" respond_to :json # Actions omitten end end end
本番環境のアプリケーションでは、名前とパスワードをなんらかの外部設定に移動して、バージョン管理の中には保存しないようにします。複数のコントローラでこれを行なう必要がある場合は、新規のコントローラを作成して他のコントローラをそこからサブクラス化します。
curl
コマンドでこれを試してみます。APIにリクエストを送るとエラーが発生します。
$ curl http://localhost:3000/api/products HTTP Basic: Access denied.
レスポンスヘッダを見ると、401 Unauthorized
レスポンスが返されたのがわかります。
$ curl http://localhost:3000/api/products -I HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Application" Content-Type: text/html; charset=utf-8 X-UA-Compatible: IE=Edge Cache-Control: no-cache X-Request-Id: c411eeceefc39ab3964d40301530843c X-Runtime: 0.002366 Content-Length: 0 Connection: keep-alive Server: thin 1.3.1 codename Triple Espresso
正しいユーザ名とパスワードを渡すと、JSONレスポンスが返されます。
$ curl http://localhost:3000/api/products -u "admin:secret" [{"category_id":2,"created_at":"2012-05-30T20:16:58Z","id":1,"name":"Settlers of Catan","price":"29.95","released_on":"2012-04-12","updated_at":"2012-05-30T20:16:58Z"}, ...etc]
一つ注意が必要なのは、認証情報が平文で送信されるため必ずセキュア接続かダイジェスト接続を使用するようにします。
アクセストークンによる認証
APIに制限をかけるもう一つの方法は、クライアントにアクセストークンを提供する方法です。どこかにこのトークンを保存する必要があるので、そのためのapi_key
モデルを新規に作成します。
$ rails g model api_key access_token
このモデルにいくつか他の列も追加します。例えば、role
列でトークンが持つ権限を指定したり、user_id
列でトークンを所有するユーザを指定したり、expires_at
列でトークンの有効期限を指定したりできます。ここではモデルをシンプルにするために、列を一つだけ追加します。rake db:migrate
を実行して新規にapi_keys
テーブルを作成します。
このモデルで、レコードが新規に作成されるごとにランダムなアクセス用トークン文字列を生成します。これをbefore_create
コールバックで行ないます。
class ApiKey < ActiveRecord::Base before_create :generate_access_token private def generate_access_token begin self.access_token = SecureRandom.hex end while self.class.exists?(access_token: access_token) end end
このコードではRuby 1.9が提供するSecureRandom.hex
を使ってランダムな16進数の文字列を生成しています。その後に同じトークンで別のキーがないかをチェックし、存在した場合はトークンを再度生成します。データベース側でもこの列にユニーク制約を追加して、値がユニークであることを確実にすることができます。これの実際の動作をコンソールで確認します。ApiKey.create!
を呼び出すと、ランダムなトークンと共に新規レコードが作成されます。
1.9.3-p125 :001 > ApiKey.create! (0.1ms) begin transaction ApiKey Exists (0.2ms) SELECT 1 FROM "api_keys" WHERE "api_keys"."access_token" = 'afbadb4ff8485c0adcba486b4ca90cc4' LIMIT 1 Binary data inserted for `string` type on column `access_token` SQL (5.9ms) INSERT INTO "api_keys" ("access_token", "created_at", "updated_at") VALUES (?, ?, ?) [["access_token", "afbadb4ff8485c0adcba486b4ca90cc4"], ["created_at", Wed, 30 May 2012 21:17:53 UTC +00:00], ["updated_at", Wed, 30 May 2012 21:17:53 UTC +00:00]] (2.7ms) commit transaction => #<ApiKey id: 1, access_token: "afbadb4ff8485c0adcba486b4ca90cc4", created_at: "2012-05-30 21:17:53", updated_at: "2012-05-30 21:17:53">
トークンを生成してそれをクライアントに表示する方法をどうするかは作成者に委ねられますが、通常はプロフィール画面などでユーザが生成して、手でAPIツールにコピー&ペーストさせるという方法がとられます。それではAPIへのアクセスに制限をかけるために、トークンを要求するように変更します。これを行なう方法はいくつかあります。一つの方法は、URLパラメータとして追加して、それをコントローラでチェックするやり方です。これをbefore_filter
で行ないます。
module Api module V1 class ProductsController < ApplicationController before_filter :restrict_access respond_to :json # Actions omitted private def restrict_access api_key = ApiKey.find_by_access_token(params[:access_token]) head :unauthorized unless api_key end end end end
restrict_access
の中で、URLから渡されたaccess_token
でApiKey
を探します。一致するキーが見つからなければ401 Unauthorized
を返します。有効なアクセストークンが渡されるまでこのページにアクセスしても何もレスポンスが返されない状態ですが、サーバのログを見るとサーバが401
レスポンスを返しているのがわかります。
アクセストークンをURLで渡すやり方はベストな方法とは言えません。トークンに有効期限がない場合は特にそうです。URLはコピー&ペーストで人から人に渡る場合があり、秘密情報を共有される状況は好ましくありません。別のやり方として、アクセストークンをHTTPヘッダで渡す方法があります。Railsではこの機能を簡単に追加するためのコントローラメソッドが提供されています。before filterでauthenticate_or_request_with_http_token
を使用します。ProductsController
にbefore filterがあるので、そこで呼びされるrestrict_access
メソッドをauthenticate_or_request_with_http_token
を使うように修正します。
def restrict_access authenticate_or_request_with_http_token do |token, options| ApiKey.exists?(access_token: token) end end
authenticate_or_request_with_http_token
に渡されるブロックがtrue
を返したら認証は成功なので、ここでは渡されたトークンでApiKey
が存在するかチェックします。APIに対してリクエストが行なわれると、以下のようにAuthorization
ヘッダが設定されない限りアクセスが拒否されます。
$ curl http://localhost:3000/api/products -H 'Authorization: Token token="afbadb4ff8485c0adcba486b4ca90cc4"'
これらの複数の認証トークン方式を混在させて、アプリケーションのニーズにもっとも合った方法をとるようにします。
ここで紹介した方法はシンプルなものですが、より複雑な状況の場合はどうすればいいでしょうか? 例えばあるユーザが我々のアプリケーションにログインすることができて、我々のAPIを利用する別のアプリケーションにも(そのユーザが許可をした場合のみ)そのユーザとしてログインしたことにさせて認証情報にアクセスさせたいというような場合です。これはFacebookやTwitterなどのようなソーシャルネットワーク系のアプリケーションでは典型的なシナリオで、これを実現するのに最も適しているのはOAuthを使う方法です。今回のエピソードではOAuthに詳しく触れることはしませんが、ウェブサイトには多くの情報があります。基本的にはAPIにセキュリティを設定してログインする先のサイトにパスワードを保存することなくユーザのデータを保護する機能を提供します。
RailsアプリケーションでOAuthを簡単に設定できるようにするためのプロジェクトはいくつもあります。その中の一つがDoorkeeperで、まだ開発段階ではありますがチェックする価値があるので、今週のProエピソードで取り上げています。oauth2 gemも詳しく見てみる価値があります。多くのプロジェクトがこのgemをベースにして構築されているので、そのしくみを理解しておくのがいいでしょう。今回紹介したいずれのツールを使う場合も、APIとのやりとりをセキュアな接続で行なうことがとても重要なので、かならずSSLを使用するようにしてください。