今回達成すること
今回は、ログイン認証の流れを理解し、JWTの実装に入ります。
JWTの初期設定ファイルを作成するところまでをゴールとしましょう。
ログイン認証機能の参考
ここで紹介するログイン機能は、RailsAPIでJWT認証が実装できるGem knockを参考にしています。
Knockは2016年から更新が止まっており、Rails6には対応しなくなりました。(非公式版は2020年も更新あり)
そこで今回は、Knockのコードを参考に1からログイン認証を手作りします。
JWTを使ったログイン認証の流れ
今回実装するログイン認証では、以下の手順で認証が行われます。
1. クライアントからログイン情報を投げる
まず初めにNuxt.jsからログインに必要な
- メールアドレス
- パスワード
をRailsに投げます。
2. サーバーサイドでユーザーを検索する
Railsでは、メールアドレスとパスワードが一致するユーザーを検索します。
-
存在する場合、
JWTを発行しCookieに保存します。
-
存在しない場合
401エラーを返します。
3. クライアントにJSONを返す
RailsからNuxt.jsに返すJSONは、
- 有効期限と
- ユーザーオブジェクトです。
JWTそのものはNuxt.jsに渡さず、Cookieに保存します。
JWTの保存先
JWTの保存先には、ローカルストレージがよく挙げられますが、JavaScriptで簡単にアクセスできてしまいます。
認証に使用するJWTは、ログインメールアドレスとパスワードと同じ意味を持ちますので、外部のJavaScriptからアクセスできない場所に保管することが望ましいです。
そこで、今回の実装では、JWTをCookie(クッキー)に保存します。
Cookieとは
Cookieとは、クライアント(ブラウザ)に保存される簡易なテキストファイルのことを言います。
サーバー(Rails)がクライアントに、一時的に情報を記録させるときに使用します。
httponlyオプション
httponly
は、HTTP通信でのみアクセスできるCookieを生成するためのオプションです。
このオプションを付けると、JavaScriptからのアクセスを完全に遮断することができ、サーバーサイドのRailsでしかアクセスできなくなります。
これにより、外部JavaScriptからのCookie盗聴を防ぎます。
Cookieでも万全ではないことを理解しておく
Cookieへの保存は、外部からの攻撃を完全に遮断できるわけではありません。
JavaScriptで参照できるローカルストレージよりはマシなだけであって、Cookieのhttponly
オプションは、あくまで最低限のセキュリティだとお考えください。
クロスサイト・スクリプティングなどの攻撃でCookieを絶対に取られなくなると誤解されてる方もいらっしゃいますが、javascriptなどから直接参照・操作はできませんが、Content-Length: 0やCRLFの挿入によりレスポンス分割攻撃(HTTP Response Splitting)が成立してしまうとやはりHttpOnly属性の設定ではCookieの送信は回避できません。”bypass httponly cookie”と調べると方法は出てきます。
4. クライアントでJSONを保存する
返されたユーザーは、Nuxt.jsのVuexに保存します。
保存したVuexのユーザーオブジェクトを、現在ログイン中のユーザーとして扱います。
有効期限はローカルストレージに保存します。
これは、ユーザーがサイトに訪問した際に、ログイン状態を維持するかの判定に使われます。
5. ログイン状態の有効期限の判定
Vuexに保存したユーザーオブジェクトは、ブラウザをリロードするたびに初期化されます。
つまり、ユーザーは存在しなくなります。
そこで、
- Nuxt.jsが初期化された直後に、
- ローカルストレージに保存した有効期限内であれば、
- Railsにユーザーを取得するようリクエストします。
Railsは、
- Cookieに保存したJWTからユーザーを検索し、
- Nuxt.jsに返します。
これにより、有効期限内であればログイン状態を維持することができます。
JWTの初期設定ファイルを作成する
2021年06月09日 修正
token_audienceの値に間違いがありましたので、下記のように修正しました。
ENV["API_DOMAIN"]
=>ENV["APP_URL"]
audienceの値は、受信者(トークンが送られる保護対象リソースのURL)となります。
今回のjwtの受信者は、Railsとなるので、RailsのURLを指定するよう変更しました。
間違った情報があったことを、お詫び申し上げます。
それでは実装に入りましょう。
「config/initializers(イニシャライザーズ)」ディレクトリ内に
このファイルには、JWTを発行する初期値を設定する役目を持ちます。
root $ touch api/config/initializers/user_auth.rb
UserAuth
モジュールを宣言します。
api/config/initializers/user_auth.rb
module UserAuth
# 必須
mattr_accessor :token_lifetime
self.token_lifetime = 2.week
mattr_accessor :token_audience
self.token_audience = -> {
# あとで作成する
ENV["APP_URL"]
}
mattr_accessor :token_signature_algorithm
self.token_signature_algorithm = "HS256"
mattr_accessor :token_secret_signature_key
self.token_secret_signature_key = -> {
Rails.application.credentials.secret_key_base
}
mattr_accessor :token_public_key
self.token_public_key = nil
mattr_accessor :token_access_key
self.token_access_key = :access_token
mattr_accessor :not_found_exception_class
self.not_found_exception_class = ActiveRecord::RecordNotFound
end
-
mattr_accessor(モジュールアクセサ)
... Rubyのattr_accessor(アトリビューアクセサ)
を拡張した、Railsのモジュール用アクセサメソッド。ここで宣言したアクセサは、
<モジュール名>.<アクセサ名>
でアクセスすることができます。 -
token_lifetime(トークン ライフタイム)
... JWT有効期限のデフォルト値。2週間としています。トークンを盗まれても2週間後には無効になります。
セキュリティを高めたい場合は、短い有効期限の方が良いでしょう。
トークンは無期限とすることもできますが、今回の実装では有効期限を必須項目としています。
-
token_audience(トークン オーディエンス)
... 受信者を識別する文字列を指定する。ここには、トークンを受け取る者の情報を指定します。
一般的には、トークンが送られる保護対象リソースのURLを指定します。
今回は、発行者も受信者もRailsとなるので、RailsのURLを指定しています。
ここで指定した値は誰でも閲覧できます。機密情報は指定しないでください。
JWT 検証のトラブルシューティング - GoogleCloud
2021年06月09日 修正
当初の説明は間違った情報です。申し訳ありません。
誰のために発行したか明確にするために、クライアントを識別する文字列、もしくはその文字列の入った配列を指定します。Auth0のような認証サービスではアプリケーションIDを指定しますが、一般的にはクライアントのドメインやURLを指定します。 -
token_signature_algorithm(トークン シグネチャー アルゴリズム)
... 署名アルゴリズムを指定する。アルゴリズムとは計算方法のことで、署名の検証方法を指定します。
HS256とは、1つの鍵で署名と検証を行うアルゴリズムです。
他には、秘密鍵と公開鍵のペアで検証するRS256などが指定できます。
使用できるアルゴリズム一覧 JSON Web Algorithms
-
token_secret_signature_key(トークン シークレット シグネチャー キー)
... 署名に使用する鍵を指定。Railsのシークレットキーを使用します。
この秘密鍵で署名と検証を行います。
-
token_public_key(トークン パブリック キー)
... 公開鍵を使用する場合はここに指定。署名アルゴリズムがHS256の場合は使用しません。
-
token_access_key(トークン アクセス キー)
... Cookieに保存する際のオブジェクトキーを指定。CookieからJWTを取得する場合は、
cookies[:access_token]
となります。 -
:not_found_exception_class(ノット ファウンド エクセプション クラス)
... ログインユーザーが見つからない場合のRailsの例外を指定する。ここの値はログインコントローラーの実装時に使用します。
audienceに指定した環境変数を追加する
audienceに指定した、APP_URL
という環境変数を追加します。
api
サービスに追記します。
docker-compose.yml
api:
...
environment:
...
API_DOMAIN: ...
APP_URL: "http://localhost:$API_PORT" # 追加
忘れないうちにHerokuの環境変数にも登録しておきましょう。
値は、Railsの本番環境のURLを指定します。
root $ cd api
api $ heroku config:set APP_URL=https://<Railsの本番環境アプリ名>.herokuapp.com
確認して、値が出力されたら登録された証拠です。
api $ heroku config:get APP_URL
https://<Railsの本番環境アプリ名>.herokuapp.com
api $ cd ..
root $
コミットする
今回の実装は以上です。コミットしておきましょう。
root $ cd api
api $ git add -A && git commit -m "add_initializers_user_auth.rb"
api $ cd ..
root $
まとめ
今回は、ログイン認証の流れを把握し、JWTの初期設定ファイルを作成しました。
このファイルには、JWT認証全体で使用する値を指定します。
初期値を確認したり、新たに指定する場合は「initializers」ディレクトリの
さて、次回は?
次回は、JWTの発行と検証を行うクラスファイルを作成します。
どうぞお楽しみに。(↓用意できています)
修正情報
-
2021年06月09日
目次「JWTの初期設定ファイルを作成する」のコードを修正しました。