はじめに
Rails 5 APIモード + Grapeで実装された既存のWeb APIに、かんたんなトークンベース認証の機構を追加するサンプル。
なお、今回の記事でベースとする既存のWeb APIは、先日書いた Rails 5 APIモード + Grape + Grape::Entityで作るWeb API - Qiita で作成したものとする。
記事内の方針の項にも書いたけど、複雑な事をしたいなら素直に devise_token_auth を使ったほうが良さそうだ。
前提条件
検証環境は以下の通り。
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.14.2
BuildVersion: 18C54
$ ruby -v
ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-darwin18]
$ rails -v
Rails 5.2.1.1
また、ベースのプロジェクトとして、Rails 5 APIモード + Grape + Grape::Entityで作るWeb API - Qiita で作成したRails + Grape製のWeb APIを利用する。
方針
- Rails標準の
has_secure_token
を利用。 -
Userモデル
を追加し、そこにtoken
カラムを追加する。- メールアドレスとパスワードによる認証も追加する。
- Rails標準の
has_secure_password
を利用。
- かんたんなトークンベース認証を実現するので、トークンに有効期限を設けたり、1ユーザが複数のトークンを持つといった複雑なことはしない。
- 複雑な事をしたいなら、 devise_token_auth 等を使うほうが早そう。
Userモデル
Gemfileの編集、bcrypt gemのインストール
has_secure_password
や has_secure_token
を使うために、 bcrypt-ruby gemのインストールが必要だ。
Gemfile
を開き、17行目付近にある bcrypt
のコメントアウトを外す。
# Gemfile
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'
その後、bundle install。
$ bundle install
Userモデルの生成と動作確認
次に、Userモデルを生成する。
$ rails g model User email:string name:string password_digest:string token:string
生成された db/migrate/20181203043642_create_users.rb
を編集する。
# db/migrate/20181203043642_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.string :name, null: false
t.string :token
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :token, unique: true
end
end
-
token
以外の各カラムにNOT NULL制約を追加-
token
カラムにはNOT NULL制約を付加しない 1
-
-
email
カラムにUNIQUE制約を追加 -
token
カラムにUNIQUE制約を追加
次に、 app/models/user.rb
を編集する。 has_secure_password
と has_secure_token
の追記をした後、各種バリデーションの処理を記述する。
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
has_secure_token
validates :email, presence: true
validates :email, uniqueness: true
validates :name, presence: true
validates :password_digest, presence: true
validates :token, uniqueness: true
end
DBのマイグレーションを行う。
$ rails db:migrate
rails consoleで動作確認をする。
user = User.new(name: 'mktakuya', email: 'mktakuya@example.com', password: 'password')
user.save
#=> true
user.token
#=> "何らかの文字列"
user.authenticate('password')
#=> #<User id: 1, name: "mktakuya", ... >
user.authenticate('wrong_password')
#=> false
認証まわりの実装
ログインと新規登録
まずは、メールアドレスとパスワードによるログインと、新規会員登録の仕組みを作る。
app/api/v1/users.rb
を新規作成し、 signin
と signup
を作成する。
# app/api/v1/users.rb
module V1
class Users < Grape::API
resources :users do
desc 'signin'
params do
requires :email, type: String
requires :password, type: String
end
post '/signin' do
@user = User.find_by(email: params[:email])
if @user.authenticate(params[:password])
@user
else
error!('Unauthorized. Invalid email or password.', 401)
end
end
desc 'signup'
params do
requires :email, type: String
requires :password, type: String
requires :name, type: String
end
post '/signup' do
@user = User.new(declared(params))
if @user.save
@user
else
@user.errors.full_messages
end
end
end
end
end
root.rb
で V1::Users
をマウントするのを忘れずに。
# app/api/v1/root.rb
module V1
class Root < Grape::API
# 省略
mount V1::Users
mount V1::Authors
mount V1::Books
end
end
curlコマンドでテストしてみる。以後の通信では、レスポンスで降ってきたtokenを利用して通信を行う。
# 新規登録成功
$ curl -X POST -H 'Content-Type: application/json' http://localhost:3000/v1/users/signup -d '{"email": "hello@example.com", "password": "password", "name": "hello"}'
{
"id": 1,
"email": "hello@example.com",
"password_digest": "何らかの文字列",
"name": "hello",
"token": "何らかの文字列",
"created_at": "2018-12-21T09:02:08.779Z",
"updated_at": "2018-12-21T09:02:08.779Z"
}
# ログイン成功
$ curl -X POST -H 'Content-Type: application/json' http://localhost:3000/v1/users/signin -d '{"email": "hello@example.com", "password": "password"}'
{
"id": 1,
"email": "hello@example.com",
"password_digest": "$2a$10$0Jj6J2x4ui1ZmvMcQvNUB.SoDjrpJTZSq8LaJ8YGKIsXZlchj5f.K",
"name": "hello",
"token": "hff2Lpo8AXNhRHykrTw7rCZe",
"created_at": "2018-12-21T09:02:08.779Z",
"updated_at": "2018-12-21T09:02:08.779Z"
}
ログイン成功時に password_digest
やら *_at
やらが降ってきて不穏だけどそれはこの記事の本質ではないのでよしとする。ちゃんとやりたいなら、 UserWithTokenEntity
などを作って、必要な情報とtokenのみ返す感じでしてやれば良い。
認証ヘルパーの実装
認証ヘルパーの実装を行う。今回実装するのは2つ。 current_user
と authenticate!
だ。 current_user
はログイン中のユーザのインスタンスを返す。また、 authenticate!
は current_user
がnilのときに401エラーを返す。
ふつう、こういったヘルパーの類は helpers
などという名前空間を切って記述するが、今回は簡単のため app/api/v1/root.rb
内に helpersブロックで記述する。あとあとヘルパーが増えてきて見通しが悪くなってきたら、 公式ドキュメントのHelpersの項目 を参考に名前空間を切ってファイルを分けてやれば良い。
# app/api/v1/root.rb
module V1
class Root < Grape::API
version :v1
format :json
TOKEN_PREFIX = /^Bearer\s/ # Tokenは "Bearer " というPrefixをもつ
TOKEN_REGEX = /^Bearer\s(.+)/ # Tokenは "Bearer 何らかの文字列" という形式である
helpers do
def authenticate!
error!('Unauthorized. Invalid or expired token.', 401) unless current_user
end
def current_user
return nil unless headers['Authorization'] && headers['Authorization'].match(TOKEN_REGEX)
token = headers['Authorization'].gsub(TOKEN_PREFIX, '')
User.find_by(token: token)
end
end
mount V1::Users
mount V1::Authors
mount V1::Books
end
end
使ってみる
認証付きのAPIの例として、著者(Authors)の作成は認証済みのユーザじゃないと出来ないというように変更してみようと思う。といっても簡単で、単に該当箇所の処理の最初で authenticate!
ヘルパーを呼び出してやれば良い。また今回は扱わないが、 Author
に誰かこのAuthor
を追加したか (created_by
) といった情報を追加するとなったら、 current_user
を渡してやれば良い。
# app/api/v1/authors.rb
module V1
class Authors < Grape::API
resources :authors do
desc 'Create an author'
params do
requires :name, type: String
end
post '/' do
authenticate! # ← 追加
@author = Author.new(name: params[:name])
if @author.save
status 201
present @author, with: V1::Entities::AuthorEntity
else
status 400
present @author.errors
end
end
end
end
end
curlでテスト。tokenは、先程のログイン処理の時に手に入れたものを使う。
# 成功
$ curl -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer hff2Lpo8AXNhRHykrTw7rCZe' http://localhost:3000/v1/authors -d '{"name": "水嶋ヒロ"}'
{
"id": 25,
"name": "水嶋ヒロ"
}
# 間違ったトークンを設定
$ curl -X POST -H 'Content-Type: application/json' -H 'Authorization: Bearer hikakin-tv-everyday' http://localhost:3000/v1/authors -d '{"name": "三島由紀夫"}'
{
"error": "Unauthorized. Invalid or expired token."
}
# トークン設定無し
$ curl -X POST -H 'Content-Type: application/json' http://localhost:3000/v1/authors -d '{"name": "ひかわ博一"}'
{
"error": "Unauthorized. Invalid or expired token."
}
おわりに
以上で、Rails 5 APIモード + GrapeによるAPIにかんたんなトークンベース認証の機構を追加することができた。
実際にモバイルアプリ等から利用する際は、以下のような感じになるだろう。
- メールアドレス・パスワードによる認証でトークンを取得し、アプリ内の領域(UserDefaults等)に保存
- 以後、そのトークンを利用して通信を行う。