2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-02-28

はじめに

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
Gemfileに追加
gem 'grape'
gem 'grape-jbuilder'
gem 'rack-jwt'
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
config/routes.rbに追加
mount API::Root => '/'
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
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
app/apis/profile.rb
module API
  class Profile
    get '/profile', jbuilder:'profile' do
# @current_user を利用するので特に処理はない
    end
  end
end
app/views/api/token.jbuilder
json.(@token, :token, :expire)
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の説明通りに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を使ったトークン認証を実装できました。もっとスマートな方法を知っているよ!という方がいれば是非教えてほしいです!!!

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?