DEV Community

KOGA Mitsuhiro
KOGA Mitsuhiro

Posted on • Originally published at qiita.com

{grape} + Rack::JWT + Jbuilderを使ってREST APIでトークン認証を実装する

はじめに

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

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

今回組み込んだもの

  • Rack::JWT
    • {grape}のhttp_basic, http_digestを参考に組み込めた!

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

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
gem 'grape'
gem 'grape-jbuilder'
gem 'rack-jwt'
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
mount API::Root => '/'
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
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
module API
  class Profile
    get '/profile', jbuilder:'profile' do
# @current_user を利用するので特に処理はない
    end
  end
end
json.(@token, :token, :expire)
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の説明通りに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

クライアントから送られてきたemailpasswordをチェック後、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を使ったトークン認証を実装できました。もっとスマートな方法を知っているよ!という方がいれば是非教えてほしいです!!!

参考

Top comments (0)