9/14-[翻訳]Rails API + JWT auth + VueJS SPA
■翻訳記事
当記事は、下記記事の翻訳となります。
「Rails API + JWT auth + VueJS SPA Part1」
当記事は、下記記事の翻訳となります。
「Rails API + JWT auth + VueJS SPA Part1」
■
これは、初級の開発者が認証API呼び出しでVueJS SPAを構築するよう設計されたガイドです。
APIファーストのRESTフルRailsバックエンドの構築から始めます。
APIファーストとは、同じAPIエンドポイントを異なる
APIファーストとは、同じAPIエンドポイントを異なる
- Web / JSクライアント
- モバイルアプリケーション
- サードパーティのAPI
バックエンドのツール:
- Rails 5.2.0
- Ruby 2.4.4
- gem bcrypt 1.3.12
- gem jwt_sessions 2.1.0
- gem redis 4.0.1
- gem rack-cors 1.0.2
この正確なバージョンに縛られているわけではありません。単に、ローカル環境にインストールしたバージョンをリストしています。それでは、ToDoアプリを作成しましょう。
バックエンド
■
$rails app rails new silver-octo-invention --api -T
を作成します。派手なプロジェクト名はGitHubによって自動生成されます。
-Tオプションは、デフォルトのテストフレームワークであるMinitestを除外します。 RSpecを使用します。
■
このようにGemfileを調整します
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.4.4'
gem 'rails', '~> 5.2.0'
gem 'sqlite3'
gem 'puma', '~> 3.11'
gem 'redis', '~> 4.0'
gem 'bcrypt', '~> 3.1.7'
gem 'jwt_sessions', '~> 2'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'rack-cors'
group :development, :test do
gem 'pry-byebug', '~> 3.4'
gem 'pry-rails', '~> 0.3.4'
gem 'rspec-rails', '~> 3.7'
gem 'factory_bot_rails', '~> 4.8'
end
group :development do
gem 'listen', '>= 3.0.5', '< 3.2'
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
end
■
bundle installを実行する
■
rails g rspec:installを実行する
下記が生成される
.rspec
spec/spec_helper.rb
spec/rails_helper.rb
■
Userモデルを作成しましょう。
まず、必要最小限のモデルフィールドから始めます。
rails g model user email:string password_digest:string
■
マイグレーションファイルにnull:false設定を追加します。
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :email, null: false
t.string :password_digest, null: false
t.timestamps
end
end
end
■
rails db:createを実行する
rails db:migrateを実行する
■
has_secure_passwordメソッドをUserモデルに追加します。
rails db:createを実行する
rails db:migrateを実行する
■
has_secure_passwordメソッドをUserモデルに追加します。
class User < ApplicationRecord
has_secure_password
end
■
ToDoを作成しましょう。下記コマンドを実行します。
rails g model todo title:string user:references && rails db:migrate
■
コントローラーを作成しましょう。
最初に、JWTSessions :: RailsAuthorizationをApplicationControllerに含める必要があります。
モジュールは、安全なエンドポイントを保護する承認アクションを提供します。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include JWTSessions::RailsAuthorization
end
■
不正なリクエストには例外処理が必要なので、すぐに追加しましょう。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include JWTSessions::RailsAuthorization
rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized
private
def not_authorized
render json: { error: 'Not authorized' }, status: :unauthorized
end
end
■
ちなみに、JWTを使用するには、初期構成を指定する必要があります。
JWTSessions gemはデフォルトでHS256アルゴリズムを使用し、提供される暗号化キーが必要です。
JWTSessions gemはデフォルトでHS256アルゴリズムを使用し、提供される暗号化キーが必要です。
また、デフォルトでは、gemはトークンストアとしてredisを使用するため、機能するredis-serverインスタンスが必要になります。ただし、メモリをトークンストアとして選択する可能性があります(テスト環境で役立つ場合があります)。特定の設定の詳細については、READMEを参照してください。
# config/initializers/jwt_sessions.rb
JWTSessions.encryption_key = 'secret'
■
次に、サインアップエンドポイントを作成します。
トークンベースのセッションとSPAでは、クライアントのトークンを保存する場所として、CookieとlocalStorageの2つの最も一般的なオプションがあります。
トークンを格納する場所を決定するのは開発者の責任です。決定を下す際は注意してください。CookieはCSRFに対して脆弱であり、localStorageはXSS攻撃に対して脆弱です。
CSRFの脆弱性は解決可能です。私は通常、最も安全なトークンストアとしてhttpのみのcookieを好みます。
jwt_sessions gem自体は、Cookieがトークンストアとして選択された場合のトークンのセット(アクセス、更新、CSRF)を提供します。
これを踏まえて、gemによって提供されるCSRFトークンと一緒にcookieを使用してみましょう(gemは、JWTが要求cookieによって渡されるときに、CSRF検証を自動的に管理します)。
Gem内のセッションは、トークンのペア(アクセスと更新)として表されます。アクセストークンの有効期間は短く(デフォルトは1時間)、更新の有効期間は比較的長い(2週間)。有効期限は設定可能です。更新トークンは、有効期限が切れたアクセスを更新するために使用されます。
リフレッシュトークンを外部APIサービスまたはモバイルアプリケーションに渡すことは理にかなっていますが、JSクライアントは通常、貴重なリフレッシュトークンを格納するのに十分に安全ではありません。JSに渡す情報と渡さない情報を決定するのは開発者の責任です。 jwt_sessions gemは、期限切れの古いトークンを渡すことで新しいアクセストークンを発行する可能性を提供するため、更新トークンをJSクライアントに渡さないようにすることができます。更新トークンとアクセストークンの両方が相互にリンクされているため、JSクライアントからアクセスが盗まれたかどうかを簡単に検出し、リークされたセッションをフラッシュできます。(2人のユーザー—元のユーザーと攻撃者は、最終的に同じ更新トークンを指す2つの異なるアクセストークンを持ちます)。
それでは、実際に、サインアップエンドポイントを作成しましょう。エンドポイントは、ユーザーを作成し、JWTペイロードをアセンブルし、Cookieを介して応答とともに渡すとともに、応答本文を介してCSRFトークンを渡す必要があります。
class SignupController < ApplicationController
def create
user = User.new(user_params)
if user.save
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
tokens = session.login
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)
render json: { csrf: tokens[:csrf] }
else
render json: { error: user.errors.full_messages.join(' ') }, status: :unprocessable_entity
end
end
private
def user_params
params.permit(:email, :password, :password_confirmation)
end
end
サインアップを確実に機能させるためのRspecファイル。
# spec/support/response_helper.rb
module ResponseHelper
def response_json
JSON.parse(response.body) rescue {}
end
end
# spec/spec_helper.rb
require_relative 'support/response_helper'
RSpec.configure do |config|
# ...
config.include ResponseHelper
# ...
end
# spec/controllers/signup_controller_spec.rb
require 'rails_helper'
RSpec.describe SignupController, type: :controller do
describe 'POST #create' do
let(:user_params) { { email: 'test@email.com', password: 'password', password_confirmation: 'password' } }
it 'returns http success' do
post :create, params: user_params
expect(response).to be_successful
expect(response_json.keys).to eq ['csrf']
expect(response.cookies[JWTSessions.access_cookie]).to be_present
end
it 'creates a new user' do
expect do
post :create, params: user_params
end.to change(User, :count).by(1)
end
end
end
■14
これで、サインインコントローラーを構築できます。
# app/controllers/signin_controller.rb
class SigninController < ApplicationController
def create
user = User.find_by!(email: params[:email])
if user.authenticate(params[:password])
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
tokens = session.login
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)
render json: { csrf: tokens[:csrf] }
else
not_authorized
end
end
end
サインインのRSpec
# spec/controllers/signin_controller_spec.rb
RSpec.describe SigninController, type: :controller do
describe 'POST #create' do
let(:password) { 'password' }
let(:user) { create(:user) }
let(:user_params) { { email: user.email, password: password } }
it 'returns http success' do
post :create, params: user_params
expect(response).to be_successful
expect(response_json.keys).to eq ['csrf']
expect(response.cookies[JWTSessions.access_cookie]).to be_present
end
it 'returns unauthorized for invalid params' do
post :create, params: { email: user.email, password: 'incorrect' }
expect(response).to have_http_status(401)
end
end
end
■
ここでは、更新のエンドポイントを示します。 Webクライアントのエンドポイントを構築しているときに、古いアクセス権を持つ新しいアクセス権を更新します。後で、別のエンドポイントのセットを作成して、リフレッシュトークンを介して動作する他のAPIコンシューマー(モバイルなど)で使用できますが、この場合、危険を冒してリフレッシュトークンを外の世界に広く見せることはしません。
期限切れのアクセストークンのみが更新に使用されることを想定しているため、refresh_by_access_payloadメソッド内で、ブロックがunauth例外で渡されます。
必要に応じて、ブロック内でサポートチームに通知したり、セッションをフラッシュしたり、ブロックをスキップしてこの種のアクティビティを無視したりできます。JWTライブラリは有効期限クレームを自動的にチェックし、期限切れのアクセストークンの例外を回避するために、claimless_payloadメソッドを使用します。
# app/controllers/refresh_controller.rb
class RefreshController < ApplicationController
before_action :authorize_refresh_by_access_request!
def create
session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
tokens = session.refresh_by_access_payload do
raise JWTSessions::Errors::Unauthorized, 'Malicious activity detected'
end
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)
render json: { csrf: tokens[:csrf] }
end
end
Specファイルは、
# spec/controllers/refresh_controller_spec.rb
RSpec.describe RefreshController, type: :controller do
let(:access_cookie) { @tokens[:access] }
let(:csrf_token) { @tokens[:csrf] }
describe "POST #create" do
let(:user) { create(:user) }
context 'success' do
before do
# set expiration time to 0 to create an already expired access token
JWTSessions.access_exp_time = 0
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
@tokens = session.login
JWTSessions.access_exp_time = 3600
end
it do
request.cookies[JWTSessions.access_cookie] = access_cookie
request.headers[JWTSessions.csrf_header] = csrf_token
post :create
expect(response).to be_successful
expect(response_json.keys.sort).to eq ['csrf']
expect(response.cookies[JWTSessions.access_cookie]).to be_present
end
end
context 'failure' do
before do
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
@tokens = session.login
end
it do
request.cookies[JWTSessions.access_cookie] = access_cookie
request.headers[JWTSessions.csrf_header] = csrf_token
post :create
expect(response).to have_http_status(401)
end
end
end
end
■
もう少しです。これが、todosコントローラを構築する時です。
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
before_action :authorize_access_request!
before_action :set_todo, only: [:show, :update, :destroy]
# GET /todos
def index
@todos = current_user.todos
render json: @todos
end
# GET /todos/1
def show
render json: @todo
end
# POST /todos
def create
@todo = current_user.todos.build(todo_params)
if @todo.save
render json: @todo, status: :created, location: @todo
else
render json: @todo.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /todos/1
def update
if @todo.update(todo_params)
render json: @todo
else
render json: @todo.errors, status: :unprocessable_entity
end
end
# DELETE /todos/1
def destroy
@todo.destroy
end
private
def set_todo
@todo = current_user.todos.find(params[:id])
end
def todo_params
params.require(:todo).permit(:title)
end
end
これを機能させるにはcurrent_userも必要なので、追加しましょう。 トークンが承認されたら、ペイロードを調べて、格納することに決めたものを何でもフェッチできます。今回の場合はuser_idです。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include JWTSessions::RailsAuthorization
rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized
private
def current_user
@current_user ||= User.find(payload['user_id'])
end
def not_authorized
render json: { error: 'Not authorized' }, status: :unauthorized
end
end
todoのSpecです
# specs/controller/todos_controller_spec.rb
RSpec.describe TodosController, type: :controller do
let(:user) { create(:user) }
let(:valid_attributes) {
{ title: 'new title' }
}
let(:invalid_attributes) {
{ title: nil }
}
before do
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload)
@tokens = session.login
end
describe 'GET #index' do
let!(:todo) { create(:todo, user: user) }
it 'returns a success response' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
get :index
expect(response).to be_successful
expect(response_json.size).to eq 1
expect(response_json.first['id']).to eq todo.id
end
# usually there's no need to test this kind of stuff
# within the resources endpoints
# the quick spec is here only for the presentation purposes
it 'unauth without cookie' do
get :index
expect(response).to have_http_status(401)
end
end
describe 'GET #show' do
let!(:todo) { create(:todo, user: user) }
it 'returns a success response' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
get :show, params: { id: todo.id }
expect(response).to be_successful
end
end
describe 'POST #create' do
context 'with valid params' do
it 'creates a new Todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
expect {
post :create, params: { todo: valid_attributes }
}.to change(Todo, :count).by(1)
end
it 'renders a JSON response with the new todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
post :create, params: { todo: valid_attributes }
expect(response).to have_http_status(:created)
expect(response.content_type).to eq('application/json')
expect(response.location).to eq(todo_url(Todo.last))
end
it 'unauth without CSRF' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
post :create, params: { todo: valid_attributes }
expect(response).to have_http_status(401)
end
end
context 'with invalid params' do
it 'renders a JSON response with errors for the new todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
post :create, params: { todo: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to eq('application/json')
end
end
end
describe 'PUT #update' do
let!(:todo) { create(:todo, user: user) }
context 'with valid params' do
let(:new_attributes) {
{ title: 'Super secret title' }
}
it 'updates the requested todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
put :update, params: { id: todo.id, todo: new_attributes }
todo.reload
expect(todo.title).to eq new_attributes[:title]
end
it 'renders a JSON response with the todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
put :update, params: { id: todo.to_param, todo: valid_attributes }
expect(response).to have_http_status(:ok)
expect(response.content_type).to eq('application/json')
end
end
context 'with invalid params' do
it 'renders a JSON response with errors for the todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
put :update, params: { id: todo.to_param, todo: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to eq('application/json')
end
end
end
describe 'DELETE #destroy' do
let!(:todo) { create(:todo, user: user) }
it 'destroys the requested todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
expect {
delete :destroy, params: { id: todo.id }
}.to change(Todo, :count).by(-1)
end
end
end
■
トークンベースのセッションとSPAでは、クライアントのトークンを保存する場所として、CookieとlocalStorageの2つの最も一般的なオプションがあります。
トークンを格納する場所を決定するのは開発者の責任です。決定を下す際は注意してください。CookieはCSRFに対して脆弱であり、localStorageはXSS攻撃に対して脆弱です。
CSRFの脆弱性は解決可能です。私は通常、最も安全なトークンストアとしてhttpのみのcookieを好みます。
jwt_sessions gem自体は、Cookieがトークンストアとして選択された場合のトークンのセット(アクセス、更新、CSRF)を提供します。
これを踏まえて、gemによって提供されるCSRFトークンと一緒にcookieを使用してみましょう(gemは、JWTが要求cookieによって渡されるときに、CSRF検証を自動的に管理します)。
Gem内のセッションは、トークンのペア(アクセスと更新)として表されます。アクセストークンの有効期間は短く(デフォルトは1時間)、更新の有効期間は比較的長い(2週間)。有効期限は設定可能です。更新トークンは、有効期限が切れたアクセスを更新するために使用されます。
リフレッシュトークンを外部APIサービスまたはモバイルアプリケーションに渡すことは理にかなっていますが、JSクライアントは通常、貴重なリフレッシュトークンを格納するのに十分に安全ではありません。JSに渡す情報と渡さない情報を決定するのは開発者の責任です。 jwt_sessions gemは、期限切れの古いトークンを渡すことで新しいアクセストークンを発行する可能性を提供するため、更新トークンをJSクライアントに渡さないようにすることができます。更新トークンとアクセストークンの両方が相互にリンクされているため、JSクライアントからアクセスが盗まれたかどうかを簡単に検出し、リークされたセッションをフラッシュできます。(2人のユーザー—元のユーザーと攻撃者は、最終的に同じ更新トークンを指す2つの異なるアクセストークンを持ちます)。
それでは、実際に、サインアップエンドポイントを作成しましょう。エンドポイントは、ユーザーを作成し、JWTペイロードをアセンブルし、Cookieを介して応答とともに渡すとともに、応答本文を介してCSRFトークンを渡す必要があります。
class SignupController < ApplicationController
def create
user = User.new(user_params)
if user.save
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
tokens = session.login
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)
render json: { csrf: tokens[:csrf] }
else
render json: { error: user.errors.full_messages.join(' ') }, status: :unprocessable_entity
end
end
private
def user_params
params.permit(:email, :password, :password_confirmation)
end
end
サインアップを確実に機能させるためのRspecファイル。
# spec/support/response_helper.rb
module ResponseHelper
def response_json
JSON.parse(response.body) rescue {}
end
end
# spec/spec_helper.rb
require_relative 'support/response_helper'
RSpec.configure do |config|
# ...
config.include ResponseHelper
# ...
end
# spec/controllers/signup_controller_spec.rb
require 'rails_helper'
RSpec.describe SignupController, type: :controller do
describe 'POST #create' do
let(:user_params) { { email: 'test@email.com', password: 'password', password_confirmation: 'password' } }
it 'returns http success' do
post :create, params: user_params
expect(response).to be_successful
expect(response_json.keys).to eq ['csrf']
expect(response.cookies[JWTSessions.access_cookie]).to be_present
end
it 'creates a new user' do
expect do
post :create, params: user_params
end.to change(User, :count).by(1)
end
end
end
■14
これで、サインインコントローラーを構築できます。
# app/controllers/signin_controller.rb
class SigninController < ApplicationController
def create
user = User.find_by!(email: params[:email])
if user.authenticate(params[:password])
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
tokens = session.login
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)
render json: { csrf: tokens[:csrf] }
else
not_authorized
end
end
end
サインインのRSpec
# spec/controllers/signin_controller_spec.rb
RSpec.describe SigninController, type: :controller do
describe 'POST #create' do
let(:password) { 'password' }
let(:user) { create(:user) }
let(:user_params) { { email: user.email, password: password } }
it 'returns http success' do
post :create, params: user_params
expect(response).to be_successful
expect(response_json.keys).to eq ['csrf']
expect(response.cookies[JWTSessions.access_cookie]).to be_present
end
it 'returns unauthorized for invalid params' do
post :create, params: { email: user.email, password: 'incorrect' }
expect(response).to have_http_status(401)
end
end
end
■
ここでは、更新のエンドポイントを示します。 Webクライアントのエンドポイントを構築しているときに、古いアクセス権を持つ新しいアクセス権を更新します。後で、別のエンドポイントのセットを作成して、リフレッシュトークンを介して動作する他のAPIコンシューマー(モバイルなど)で使用できますが、この場合、危険を冒してリフレッシュトークンを外の世界に広く見せることはしません。
期限切れのアクセストークンのみが更新に使用されることを想定しているため、refresh_by_access_payloadメソッド内で、ブロックがunauth例外で渡されます。
必要に応じて、ブロック内でサポートチームに通知したり、セッションをフラッシュしたり、ブロックをスキップしてこの種のアクティビティを無視したりできます。JWTライブラリは有効期限クレームを自動的にチェックし、期限切れのアクセストークンの例外を回避するために、claimless_payloadメソッドを使用します。
# app/controllers/refresh_controller.rb
class RefreshController < ApplicationController
before_action :authorize_refresh_by_access_request!
def create
session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
tokens = session.refresh_by_access_payload do
raise JWTSessions::Errors::Unauthorized, 'Malicious activity detected'
end
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)
render json: { csrf: tokens[:csrf] }
end
end
Specファイルは、
# spec/controllers/refresh_controller_spec.rb
RSpec.describe RefreshController, type: :controller do
let(:access_cookie) { @tokens[:access] }
let(:csrf_token) { @tokens[:csrf] }
describe "POST #create" do
let(:user) { create(:user) }
context 'success' do
before do
# set expiration time to 0 to create an already expired access token
JWTSessions.access_exp_time = 0
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
@tokens = session.login
JWTSessions.access_exp_time = 3600
end
it do
request.cookies[JWTSessions.access_cookie] = access_cookie
request.headers[JWTSessions.csrf_header] = csrf_token
post :create
expect(response).to be_successful
expect(response_json.keys.sort).to eq ['csrf']
expect(response.cookies[JWTSessions.access_cookie]).to be_present
end
end
context 'failure' do
before do
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
@tokens = session.login
end
it do
request.cookies[JWTSessions.access_cookie] = access_cookie
request.headers[JWTSessions.csrf_header] = csrf_token
post :create
expect(response).to have_http_status(401)
end
end
end
end
■
もう少しです。これが、todosコントローラを構築する時です。
# app/controllers/todos_controller.rb
class TodosController < ApplicationController
before_action :authorize_access_request!
before_action :set_todo, only: [:show, :update, :destroy]
# GET /todos
def index
@todos = current_user.todos
render json: @todos
end
# GET /todos/1
def show
render json: @todo
end
# POST /todos
def create
@todo = current_user.todos.build(todo_params)
if @todo.save
render json: @todo, status: :created, location: @todo
else
render json: @todo.errors, status: :unprocessable_entity
end
end
# PATCH/PUT /todos/1
def update
if @todo.update(todo_params)
render json: @todo
else
render json: @todo.errors, status: :unprocessable_entity
end
end
# DELETE /todos/1
def destroy
@todo.destroy
end
private
def set_todo
@todo = current_user.todos.find(params[:id])
end
def todo_params
params.require(:todo).permit(:title)
end
end
これを機能させるにはcurrent_userも必要なので、追加しましょう。 トークンが承認されたら、ペイロードを調べて、格納することに決めたものを何でもフェッチできます。今回の場合はuser_idです。
# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include JWTSessions::RailsAuthorization
rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized
private
def current_user
@current_user ||= User.find(payload['user_id'])
end
def not_authorized
render json: { error: 'Not authorized' }, status: :unauthorized
end
end
todoのSpecです
# specs/controller/todos_controller_spec.rb
RSpec.describe TodosController, type: :controller do
let(:user) { create(:user) }
let(:valid_attributes) {
{ title: 'new title' }
}
let(:invalid_attributes) {
{ title: nil }
}
before do
payload = { user_id: user.id }
session = JWTSessions::Session.new(payload: payload)
@tokens = session.login
end
describe 'GET #index' do
let!(:todo) { create(:todo, user: user) }
it 'returns a success response' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
get :index
expect(response).to be_successful
expect(response_json.size).to eq 1
expect(response_json.first['id']).to eq todo.id
end
# usually there's no need to test this kind of stuff
# within the resources endpoints
# the quick spec is here only for the presentation purposes
it 'unauth without cookie' do
get :index
expect(response).to have_http_status(401)
end
end
describe 'GET #show' do
let!(:todo) { create(:todo, user: user) }
it 'returns a success response' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
get :show, params: { id: todo.id }
expect(response).to be_successful
end
end
describe 'POST #create' do
context 'with valid params' do
it 'creates a new Todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
expect {
post :create, params: { todo: valid_attributes }
}.to change(Todo, :count).by(1)
end
it 'renders a JSON response with the new todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
post :create, params: { todo: valid_attributes }
expect(response).to have_http_status(:created)
expect(response.content_type).to eq('application/json')
expect(response.location).to eq(todo_url(Todo.last))
end
it 'unauth without CSRF' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
post :create, params: { todo: valid_attributes }
expect(response).to have_http_status(401)
end
end
context 'with invalid params' do
it 'renders a JSON response with errors for the new todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
post :create, params: { todo: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to eq('application/json')
end
end
end
describe 'PUT #update' do
let!(:todo) { create(:todo, user: user) }
context 'with valid params' do
let(:new_attributes) {
{ title: 'Super secret title' }
}
it 'updates the requested todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
put :update, params: { id: todo.id, todo: new_attributes }
todo.reload
expect(todo.title).to eq new_attributes[:title]
end
it 'renders a JSON response with the todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
put :update, params: { id: todo.to_param, todo: valid_attributes }
expect(response).to have_http_status(:ok)
expect(response.content_type).to eq('application/json')
end
end
context 'with invalid params' do
it 'renders a JSON response with errors for the todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
put :update, params: { id: todo.to_param, todo: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
expect(response.content_type).to eq('application/json')
end
end
end
describe 'DELETE #destroy' do
let!(:todo) { create(:todo, user: user) }
it 'destroys the requested todo' do
request.cookies[JWTSessions.access_cookie] = @tokens[:access]
request.headers[JWTSessions.csrf_header] = @tokens[:csrf]
expect {
delete :destroy, params: { id: todo.id }
}.to change(Todo, :count).by(-1)
end
end
end
■