{grape} + Rack::JWT + Jbuilderを使ってREST APIでトークン認証を実装する
Tags: rails, rails4
Published: 2017-02-28 17:54:00

---
title: {grape} + Rack::JWT + Jbuilderを使ってREST APIでトークン認証を実装する
published: true
tags: Rails, Rails4
date: 20170228T17:54Z
canonical_url: https://qiita.com/shiena/items/fa35a6a2cf4d289bc434
---
# はじめに

Rails4で作られたWebアプリにスマホ向けのAPIを追加する事になり、既存のユーザモデルを使ってトークン認証を実装してみました。私自身、Javaの経験はあるのですが、Railsはまだ日が浅いので変なコードになっているかもしれません。

# JWTを組み込むために検討したもの

## 今回組み込んだもの

* [Rack::JWT](https://github.com/eparreno/rack-jwt)
    * {grape}のhttp_basic, http_digestを参考に組み込めた！

## うまく動かせなかったもの

* [Grape::Knock](https://github.com/gottfrois/grape-knock)
    * Knockのマルチユーザ対応に追随していないため動作せず
* [Knock](https://github.com/nsarno/knock)
    * Rails力が低く、組み込み方が分からず
* [Rails, Devise, JWT and the forgotten Warden](https://medium.com/@goncalvesjoao/rails-devise-jwt-and-the-forgotten-warden-67cfcf8a0b73#.5qvqxhj44)
    * Rails力が低く、組み込んだ後の利用方法が分からず

# URL設計

以下のようなトークン発行APIと認証が必要なAPIの2つを実装してみます。

## トークン発行API

```
curl -X POST \
     -H 'Content-Type: application/json' \
     -d '{"email": "foo@example.com", "password": "secret"}' \
     http://localhost:3000/api/v1/user/token
{"token": "ここにJWT", "expire": 7200}
```

## 認証が必要なAPI

```
curl -X GET \
     -H 'Content-Type: application/json' \
     -H 'Authorization Bearer ここにJWT' \
     http://localhost:3000/api/v1/user/profile
{"email": "foo@example.com", "name": "Example person"}
```

# コードリスト

追加、変更したコードリストです。

```
+---Gemfile
+---app
|   +---apis
|   |   +---root.rb
|   |   \---profile.rb
|   \---views
|       \---api
|           +---token.jbuilder
|           \---profile.jbuilder
\---config
    +---application.rb
    \---routes.rb
```

```ruby:Gemfileに追加
gem 'grape'
gem 'grape-jbuilder'
gem 'rack-jwt'
```

```ruby:config/application.rbに追加
config.paths.add File.join('app', 'apis'), glob: File.join('**', '*.rb')
config.autoload_paths += Dir[Rails.root.join('app', 'apis', '*')]
config.middleware.use(Rack::Config) do |env|
  env['api.tilt.root'] = Rails.root.join 'app', 'views', 'api'
end
```

```ruby:config/routes.rbに追加
mount API::Root => '/'
```

```ruby:app/apis/root.rb
module API
  class Root < Grape::MiddleWare::Base
    prefix 'api'
    content_type :json, 'application/json; charset=UTF-8'
    format :json
    version 'v1', using: :path

# Rack::JWTをGrapeに組み込み
    Grape::Middleware::Auth::Strategies.add(
      :jwt_auth,
      Rack::JWT::Auth,
      ->(options) { [
        [:secret, :verify, :options, :exclude].
          select { |key| options.has_key?(key) }.
          collect { |key| [key, options[key]] }.to_h
      ] }
    )

# 認証しないURLを定義する
    namespace :user do
      mount API::Token
    end

# 認証するURLを定義する
    namespace :user do
      auth :jwt_auth, {secret: Rails.application.secrets.secret_key_base}
      before do
# Rack::JWTが追加するキーをチェック
        error!('Unauthorized.', 401) unless env['jwt.payload']
        error!('Unauthorized.', 401) unless env['jwt.payload']['data']
        error!('Unauthorized.', 401) unless env['jwt.payload']['data']['id']

        @current_user = User.find_by_id env['jwt.payload']['data']['id']

        error!('Access Denied.', 403) unless @current_user
      end

      mount API::Profile
    end
  end
end
```

```ruby:app/apis/token.rb
module API
  class Token
    params do
      requires :email, type: String
      requires :password, type: String
    end
    post '/token', jbuilder:'token' do
      user = User.find_by_email params[:email]
      error!('Unauthorized.', 401) unless user
      error!('Unauthorized.', 401) unless user.valid_password?(params[:password])

      now = Time.current.to_i
      expire = 7200
      payload = {
        data: {id: user.id},
        exp: now + expire,
        iat: now
      }
      jwt = Rack::JWT::Token.encode(payload, Rails.application.secrets_key_base)
      @token = {token: jwt, expire: expire}
    end
  end
end
```

```ruby:app/apis/profile.rb
module API
  class Profile
    get '/profile', jbuilder:'profile' do
# @current_user を利用するので特に処理はない
    end
  end
end
```

```ruby:app/views/api/token.jbuilder
json.(@token, :token, :expire)
```

```ruby:app/views/api/profile.jbuilder
json.(@current_user, :email, :name)
```

# トークン生成と認証部分について

* Gemfile
* config/application.rb
* config/routes.rb

https://github.com/ruby-grape/grape#rails の説明通りに{grape}をRailsに組み込みます。

---

* app/apis/root.rb

まず認証しないURLと認証するURLで`namespace`ブロックを分けます。
少なくともトークンを発行するURLは認証しない方に定義が必要です。

次に[Register custom middleware for authentication](https://github.com/ruby-grape/grape#register-custom-middleware-for-authentication)の説明通りにRack::JWTを組込むのですが、`auth`のブロックを書いていません。これは{grape}のミドルウェアとRack::JWTの引数が異なり、Rack::JWTにブロックを渡せないためです。その代わりにRack::JWTで設定されたキーを`before`でチェックしています。
そして`auth`の3つ目の引数(`{secret: Rails.application.secrets.secret_key_base}`の部分)は`Strategies.add`の3つ目の引数のブロックに渡され、このブロックの返り値が`Rack::JWT::Auth`のコンストラクタの2つ目の引数に渡されるので、`:secret`, `:verify`, `:options`, `:exclude`の4つから存在するものだけを返しています。

---

* app/apis/token.rb
* app/views/api/token.jbuilder

クライアントから送られてきた`email`と`password`をチェック後、Rack::JWTの`Token.encode`を使ってJWTのトークンを生成しています。エンコード用の秘密文字に`Rails.application.secrets_key_base`を使っていますが、他に定義したものでも利用できます。

---

* app/apis/profile.rb
* app/views/api/profile.jbuilder

認証するURLの説明用に{grape}とJbuilderで作ったサンプルです。

# まとめ

Rack::JWTの内部実装に依存してしまいましたが、うまく{grape}でJWTを使ったトークン認証を実装できました。もっとスマートな方法を知っているよ！という方がいれば是非教えてほしいです！！！

# 参考

* [[Rails4] Grape で API を簡単に実装 ＆ API を複数バージョンで分ける方法](http://qiita.com/hkusu/items/2ca0323cc276ab31e926)
* [Grape + Railsで、Jbuilderを使ったJSONフォーマットAPIを作る](http://qiita.com/zaru/items/fa64a4e00b76ea6ce36f)
* [Rack::JWT](https://github.com/eparreno/rack-jwt)
* [Grape::Knock](https://github.com/gottfrois/grape-knock)
* [Knock](https://github.com/nsarno/knock)
* [Rails, Devise, JWT and the forgotten Warden](https://medium.com/@goncalvesjoao/rails-devise-jwt-and-the-forgotten-warden-67cfcf8a0b73#.5qvqxhj44)