RailsでのAPI開発にGrapeを使っていて、シンプルな認証処理を実装してみました。
https://mikecoutermarsh.com/rails-grape-api-key-authentication/
自分で解釈する為に一部コードを変更したものをまとめてます。
How do works
- ユーザー登録時は、Username/PasswordをAPIにPOSTする
- ユーザー認証し、API Keyを返却する
- その後の全てのAPIリクエストは、API Keyと一緒にリクエストする。
How do it
API Keyモデルを作る
モデルを作成してカラムは下記のようにします。
- access_token
- expires_at
- user_id
rails g model api_key access_token:string expires_at:datetime user_id:integer active:boolean
インデックスを追加
migrationファイルが作成されていると思うので、 api_key と user_id にインデックスを追加します。
class CreateApiKeys < ActiveRecord::Migration
def change
create_table :api_keys do |t|
t.string :access_token, null: false
t.integer :user_id, null: false
t.datetime :expires_at
t.timestamps
end
add_index :api_keys, ["user_id"], name: "index_api_keys_on_user_id", unique: false
add_index :api_keys, ["access_token"], name: "index_api_keys_on_access_token", unique: true
end
end
migrationを走らせて実際に適用されているかを確認して下さい。
rake db:migrate
Tokenの作成
api_key.rbを開いて、下記のように書き換えます。
class ApiKey < ActiveRecord::Base
attr_accessible :access_token, :expires_at, :user_id
before_create :generate_access_token
before_create :set_expiration
belongs_to :user
def expired?
DateTime.now >= self.expires_at
end
private
def generate_access_token
begin
self.access_token = SecureRandom.hex
end while self.class.exists?(access_token: access_token)
end
def set_expiration
self.expires_at = DateTime.now+30
end
end
これでAPI Keyの作成時にaccess_tokenが作成され、expires_atが設定されます。
Authentication helpersをGrapeに追加
APIが呼ばれた際にユーザー認証をする処理をGrapeのhelperに書き足します。
helpers do
def authenticate!
error!('Unauthorized. Invalid or expired token.', 401) unless current_user
end
def current_user
# トークンを検索
token = ApiKey.where(access_token: params[:token]).first
if token && !token.expired?
@current_user = User.find(token.user_id)
else
false
end
end
end
GrapeにAPI Key作成処理を追加
POST /api/auth で認証します。
GET /api/ping でAPI Keyが正しいかどうかを返します。
# /api/auth
resource :auth do
desc "Creates and returns access_token if valid login"
params do
requires :email, type: String, desc: "Email address"
requires :password, type: String, desc: "Password"
end
post :login do
user = User.where(email: params[:email]).first
if user && user.authenticate(params[:password])
key = ApiKey.create(user_id: user.id)
{token: key.access_token}
else
error!('Unauthorized.', 401)
end
end
desc "Returns pong if logged in correctly"
params do
requires :token, type: String, desc: "Access token."
end
get :ping do
authenticate!
{ message: "pong" }
end
end