今回達成すること
JWT(以下、トークン)を検証し、正しければ発行主のユーザーを返すAuthenticator
モジュールを作成します。
モジュールで作成したメソッドは、Railsコントローラーのbefore_action
で読み込みます。
これにより、ログインユーザーしかアクセスできないコントローラーアクションを作成することができます。
モジュールファイルを作成する
モジュールファイルは、前回作成した「services/user_auth」ディレクトリ内に作成します。
root $ touch api/app/services/user_auth/authenticator.rb
UserAuthを継承したモジュールを作成する
Authenticator
モジュールを宣言します。
api/app/services/user_auth/authenticator.rb
module UserAuth
module Authenticator
end
end
これで、UserAuth::Authenticator
で呼び出すことができます。
トークンを取得するメソッドの作成
トークンを取得するメソッドを作成します。
トークンの取得は2パターンあって
- リクエストのヘッダーから取得するパターンと
- クッキーに保存したトークンを取得するパターンがあります。
リクエストヘッダーから取得するメソッド
リクエストのヘッダーから取得するメソッドは、主に一時的に発行したトークンを取得する際に使用します。
一時的に発行するトークンを使用する場面は、
- 会員登録時のメールアドレス認証時
- パスワードリセット時
- メールアドレス変更時
などです。
Railsのrequest
メソッドを使って、token_from_request_headers
をprivate
内に作成します。
api/app/services/user_auth/authenticator.rb
module UserAuth
module Authenticator
# 以下、追加
private
# リクエストヘッダーからトークンを取得する
def token_from_request_headers
request.headers["Authorization"]&.split&.last
end
end
end
-
&(ぼっち演算子)
... レシーバがnil
の場合に、エラーを発生させずnil
を返す。レシーバとは、メソッドの働きかけるオブジェクトのことで、ここでは
request.headers["Authorization"]
を指します。
Nuxt.jsからのリクエストヘッダーにトークンを埋め込む場合は、axios(アクシオス)をこのように記述します。
(参考コード)plugins/axios.js
export default ({ $axios }) => {
$axios.onRequest((config) => {
config.headers.common.Authorization = `Bearer ${<accessToken>}`
})
}
クッキーに保存したトークンを取得するメソッド
クッキーに保存したトークンはログイン判定に使用します。
Railsのcookies
メソッドを使ってtoken
を作成します。
api/app/services/user_auth/authenticator.rb
...
# リクエストヘッダーからトークンを取得する
def token_from_request_headers
request.headers["Authorization"]&.split&.last
end
# 以下、追加
# クッキーのオブジェクトキー(config/initializers/user_auth.rb)
def token_access_key
UserAuth.token_access_key
end
# トークンの取得(リクエストヘッダー優先)
def token
token_from_request_headers || cookies[token_access_key]
end
end
end
-
token
... メールアドレスの変更時は、-
リクエストヘッダーのトークンと
-
クッキートークンの
双方が存在することになるため、リクエストヘッダーを優先して取得するよう実装しています。
-
Rails APIモードでクッキーを扱う
RailsのAPIモードのデフォルトでは、クッキーを扱うことができません。
そこでクッキーを扱うモジュールを、
api/app/controllers/application_controller.rb
class ApplicationController < ActionController::API
# 追加
include ActionController::Cookies
end
これでcookies
メソッドが使用できるようになりました。
トークンからユーザーを取得するメソッド
トークンからユーザーを取得するfetch_entity_from_token
メソッドを追加します。
ユーザーが存在しない場合や、無効なトークンのデコードエラーは全てnil
を返すようにしています。
api/app/services/user_auth/authenticator.rb
...
# トークンの取得(リクエストヘッダー優先)
def token
token_from_request_headers || cookies[token_access_key]
end
# 以下、追加
# トークンからユーザーを取得する
def fetch_entity_from_token
AuthToken.new(token: token).entity_for_user
rescue ActiveRecord::RecordNotFound, JWT::DecodeError, JWT::EncodeError
nil
end
end
end
AuthTokenクラスにメソッドを追加する
AuthToken
クラスにentity_for_user
メソッドを追加します。
entity_for_user
は、payloadのsub
クレームからユーザーを検索します。
api/app/services/user_auth/auth_token.rb
...
@token = JWT.encode(@payload, secret_key, algorithm, header_fields)
end
end
# 追加
# subjectからユーザーを検索する
def entity_for_user
User.find @payload["sub"]
end
private
...
現在のユーザーを返すメソッド
current_user
メソッドを追加します。
api/app/services/user_auth/authenticator.rb
...
# トークンからユーザーを取得する
def fetch_entity_from_token
AuthToken.new(token: token).entity_for_user
rescue ActiveRecord::RecordNotFound, JWT::DecodeError, JWT::EncodeError
nil
end
# 以下、追加
# トークンのユーザーを返す
def current_user
return if token.blank?
@_current_user ||= fetch_entity_from_token
end
end
end
-
@_
... インスタンス変数の書き方。メモとして記憶するためだけに使用するインスタンス変数の書き方です。
必須のルールではありませんが、メモ用のインスタンス変数と言う意味を明示的にしています。
-
||=
... 左の値が存在しない場合、右を代入する。インスタンス変数の値がある場合はその値を返し、無ければメソッドを実行します。
この書き方はローカルキャッシュと言い、無駄なメソッドの実行を防ぎます。
認証失敗時のメソッド
認証が失敗した時は、401エラーを返し、同時にクッキーを削除します。
クッキーを削除するメソッドは、後に作成するログインコントローラー内で使用するので、アクティブメソッドとします。
api/app/services/user_auth/authenticator.rb
module UserAuth
module Authenticator
# 追加
# クッキーを削除する
def delete_cookie
return if cookies[token_access_key].blank?
cookies.delete(token_access_key)
end
private
...
include
するとプライベートメソッドでもコントローラーから呼び出すことができますが、コントローラー内で使用するメソッドを明確にするために、アクティブメソッドとしています。
認証エラーを返すメソッドは、このモジュール内でしか使用しないのでプライベートメソッドで追加します。
api/app/services/user_auth/authenticator.rb
...
# トークンのユーザーを返す
def current_user
return if token.blank?
@_current_user ||= fetch_entity_from_token
end
# 以下、追加
# 401エラーかつ、クッキーを削除する
def unauthorized_user
head(:unauthorized) && delete_cookie
end
end
end
current_userを返すメソッド
current_user
が存在する場合はオブジェクトを返し、そうでない場合はエラーを返すauthenticate_user
メソッドを作成します。
このメソッドは、各コントローラーのbfore_action
で使用します。
api/app/services/user_auth/authenticator.rb
module UserAuth
module Authenticator
# 先頭に追加
# トークンからcurrent_userを検索し、存在しない場合は401を返す
def authenticate_user
current_user.presence || unauthorized_user
end
...
presence(プレゼンス)
...precent?
がtrue
の場合に対象のオブジェクトを返す。
includeする
Authenticator
モジュールはこれで完成です。
このモジュールをコントローラーで使うためにinclude
します。
api/app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include ActionController::Cookies
# 追加
include UserAuth::Authenticator
end
コミットする
これで準備が整いました。
ここまでの変更をコミットしておきましょう。
root $ cd api
api $ git add -A && git commit -m "add_authenticator.rb"
api $ cd ..
root $
まとめ
今回はAuthenticator
モジュールを作成しました。
authenticate_user
メソッドを各コントローラーのbefore_action
で読み込むことで、アクション直前にトークンを検証することができます。
さらに、トークンのユーザーを返すため、アクション内で現在のユーザー(current_user
)を認識することがきます。
不正なトークンの場合は、401を返すため、非ログインユーザーの不正アクセスから守ることができるのです。
次回は?
さて、次回はユーザークラスでトークンの発行とデコードを簡単に行うために、新たなモジュールファイルを追加します。
どうぞお楽しみに!