JSON Web Token(JWT)認証で Ruby アプリを安全に強化する方法
ユーザーが関わるWebアプリケーションでは、当然ながらユーザーのデータを保護し、安全に管理する必要があります。
Webアプリケーションのセキュリティ確保にはさまざまな側面がありますが、本記事では、その中でも「JSON Web Token(JWT)を使った認証」と、Ruby on Railsフレームワークとの組み合わせに焦点を当てて解説します。
それでは始めましょう!
JSON Web Token(JWT)とは?
JSON Web Tokenは、Internet Engineering Task Force(IETF)が定義したインターネット標準であり、「2者間で受け渡されるクレーム(主張)を表現する、コンパクトでURLセーフな手段」とされています。
ここでいう「クレーム」とは、対象に関するさまざまな情報の断片を指します。クレームは名前と値のペアとして表現され、名前は常に文字列、値には任意のJSON値を使用できます。
JWTの基本構造
JWTの細部まで踏み込むと記事の範囲を超えてしまうため、ここでは構造の概要だけ押さえておきましょう。
JWTは3つのパートで構成され、それぞれピリオド(.)で区切られています。すなわち「ヘッダー」「ペイロード」「署名」です。
読みやすさのために各パートを改行して示していますが、実際にはすべて連結された1つの文字列として送受信されます。このトークンはサーバーからクライアントへ送られ、クライアントは以降のリクエストでこのトークンをサーバーに送り返すことで自身を識別します。
1. ヘッダー
最初のパートであるヘッダーには、トークンの生成に使用したアルゴリズムやトークンの種類といった情報が含まれます。デコードすると次のような内容になります。
{
"alg": "HS256",
"typ": "JWT"
}
2. ペイロード
2番目のパートであるペイロードには、ユーザーに関する一連のクレームが格納されます。多くの場合、これはクライアント・サーバー構成におけるクライアント側の情報です。
3. 署名
最後のパートである署名は、トークンの正当性を検証するために使われます。ヘッダーとペイロードをBase64urlエンコード(RFC 4648)し、ピリオドで連結したものに対して生成されます。
具体的には、HMAC_SHA256のような鍵付きハッシュアルゴリズムが署名の計算に使われます。どの暗号アルゴリズムを選ぶかは、ヘッダー内の"alg": "HS256"の指定によって決まります。なお、未署名のトークンの場合は、ヘッダーとペイロードのみで署名は含まれません。
JWTとその他の認証方式の比較
名前の通り、JWTはトークンベースの認証です。一方、より伝統的な認証方式として「セッションベース認証」があります。両者の流れは大きく異なります。
セッションベース認証の典型的な流れは以下の通りです。
- ユーザー(クライアント)が認証情報を含むリクエストを送信する。
- サーバーがユーザーを認証し、セッションを保存して、セッションIDをCookieとしてブラウザに返す。
- クライアントは以降のリクエストにCookieを添えて送信する。
- サーバーは提示されたセッション情報を検証し、有効であればユーザーを認証して要求された情報を返す。
セッションベース認証は主にクライアント・サーバー間の接続で使われるのに対し、トークンベース認証はAPI同士などサーバー間の接続でよく利用されます。
重要な違いとして、セッションベース認証では認証状態をサーバー側で管理するのに対し、トークンベース認証ではトークンをクライアント側で管理するという点が挙げられます。
なぜJWTを認証に使うのか?
実装が比較的シンプルであることに加え、JWTには次のようなメリットがあります。
- ステートレスである: セッションストアが不要です。トークン自体にユーザー情報がすべて含まれているため、リクエストごとにデータベースや認証サーバーへ問い合わせる必要がありません。
- 高いパフォーマンス: ユーザー認証のためにデータベース参照などを行わない限り、従来の認証方式よりも高速に動作し、効率的です。
- 堅牢なセキュリティ保証: 署名済みのJWTは改ざん防止機構を備えており、攻撃者やクライアントがトークンを書き換えて保護されたデータへアクセスすることを防げます。
RubyアプリにおけるJWTのベストプラクティス
言うまでもなく、JWTの署名に使う秘密鍵は長く、ランダムで、複雑な文字の組み合わせを持つべきです。これにより鍵の安全性が高まり、攻撃者によるブルートフォース攻撃を困難にできます。
Railsが自動生成する秘密鍵は概ね安全ですが、うっかりコミットして公開してしまえば安全性の保証は無意味になります。鍵の取り扱いには十分注意しましょう。
また、ネットワーク上でトークンを受け渡す際は、Transport Layer Security(TLS)の使用が重要です。TLSは中間者攻撃(MITM)を緩和できます(トークンベース・セッションベースのいずれの認証方式も、この種の攻撃に対して脆弱になり得ます)。
RailsアプリへのJWT認証の実装
それでは、トークンベース認証の流れを見ていきましょう。

セッションで認証済みユーザーを追跡するステートフルな認証とは異なり、JWTを使ったトークンベース認証はステートレスです。サーバー側にユーザーの認証状態を保存する必要がないため、アプリケーション設計がシンプルになります。
本記事では、アプリケーションがフロントエンドとバックエンドに分かれていることを想定します。認証はバックエンドで行うため、Rails APIバックエンドに認証機能を実装していきます。
サンプルコードはRails 7.0.5およびRuby 3.2.2を基準としています。
jwt gemとbcrypt gemの活用
必要なgemは2つです。jwtとbcryptです。
jwtはRFC 7519(OAuth JSON Web Token標準)のRuby実装で、bcryptはOpenBSDのbcrypt()パスワードハッシュアルゴリズムのRubyバインディングです。
補足: JWTを扱えるのはjwtだけではありません。devise-jwtのように、DeviseとRails向けにJWT認証を提供する有名なgemもあります。ただし本記事ではjwtに焦点を絞ります。
Rails APIの構築
まずはAPIアプリケーションを作成します。次のコマンドを実行しましょう。
rails new my_app --api
--apiオプションを付けると、API専用に最小化されたRailsスタックが事前設定されます。
Gemfileに最初の依存関係であるjwtを追加します。2つ目のbcryptは新規生成されたRailsアプリのGemfileにすでに含まれているため、コメントアウトを外すだけでOKです。
bcryptはユーザーのパスワードをデータベース内で安全にハッシュ化するために必要です。直接使うのではなく、Active Modelのhas_secure_passwordクラスメソッド(内部でbcryptに依存)を活用します。
Gemfileの準備ができたら、bundle installでgemをインストールしておきましょう。
UserモデルとProductモデルの生成
次に、UserとProductの2つのモデルを生成します。Userはユーザーを表すモデルで、これを認証してProductへのアクセスを許可します。
rails g model User username:string password_digest:string
rails g model Product name:string
rails db:migrate
マイグレーション実行後、db/schema.rbにスキーマが反映され、モデルのセットアップは完了です。
なお、この段階ではデータベース制約、バリデーション、一意性の担保などの潜在的な問題は意図的に省略しています。エラーハンドリングも割愛します。本記事の目的は、ステートレスな認証手段としてのJWTの動作を示すことです。本番アプリケーションでは、これらすべてを必ずカバーしてください。
jwt gemのラッパークラスを作成する
次に、インストールしたjwt gemのラッパーを作成します。このラッパーを使って、サーバーからクライアントへのクレームのエンコード・デコードを行います。そのため、app/libフォルダを作成しましょう。
Rails標準のlibフォルダを使わない理由は、そこがオートロード対象ではないからです。app配下のファイルはデフォルトでオートロード・イーガーロードされるため、今回のケースでは設定がシンプルになります。
ラッパークラスはapp/lib/json_web_token.rbに配置します。主要なメソッドは、ユーザー情報をエンコードするencodeと、後ほどサーバー側でユーザー情報をデコードするdecodeです。実際の処理はJWT.encodeとJWT.decodeを通じてjwt gemに委譲している点に注目してください。
この時点で、Railsコンソールでクラスの動作を確認できます。
JsonWebToken.encode(data)
JsonWebToken.encode(data)の結果がピリオドで3つの部分(ヘッダー、ペイロード、署名)に分割されていることが分かります。署名が存在するのは、Rails.application.secrets.secret_key_base経由でRailsが提供する秘密鍵でペイロードを署名しているからです。
Userモデルに戻る
ここでapp/models/user.rbのUserモデルに手を加えます。必要なのはhas_secure_passwordクラスメソッドの追加だけです。
class User < ApplicationRecord
has_secure_password
end
has_secure_passwordにより、ユーザーのパスワードがデータベース内で安全にハッシュ化されます。
サンプルのユーザーと商品を作成する
Railsコンソールに入り、サンプルのユーザーと商品をデータベースに作成して、アプリのセキュリティをテストできる状態にしましょう。同じコードをseeds.rbに置いておけば、データベースをリセットした際の入力の手間も省けます。
RailsコントローラーでのJWT活用
続いて、コントローラー内でJWTを使ったセキュリティを実装します。まずはapp/controllers/application_controller.rbから始めましょう。
ここでは、ユーザーから送られてきたJSON Web Tokenをデコードするauthenticateメソッドを作成します。トークンの検証に成功すれば、リクエスト元のユーザーを表すUserオブジェクトを返します。今回は正常系に焦点を絞り、多くのチェックを省略しています。
ApplicationControllerにauthenticateメソッドを定義し、before_actionとして設定すれば、それを継承するすべてのコントローラーが保護されます。他のコントローラーへのリクエストは、有効なJWTなしではアクセスできません(すべてのコントローラーがこの基底コントローラーを継承するためです)。
次に、ユーザーがリクエストを送り、署名済みのJSON Web Tokenを受け取れるAuthenticationControllerが必要です。app/controllers/authentication_controller.rbに配置します。
このコントローラーへのリクエストは「トークンを求めるユーザー」からのものなので、最初の段階では認証を行いません。このコントローラーの役割は、ユーザーがサーバー上の他のリソースへアクセスするために使えるトークンを返すことです。そのため、2行目でskip_before_action :authenticateを指定しています。
loginアクション(ユーザーがトークン取得のために叩くアクション)では、リクエストのパラメータからusernameとpasswordを取り出します。ユーザーの認証——つまりユーザー名とパスワードがデータベースに保存されているものと一致するかの検証——に成功すれば、署名済みトークンとその有効期限情報を返します。
本記事ではトークンの有効期限は使いませんが、本番アプリケーションではリソースへのアクセス権を取り消す用途などに活用できます。
これらの手順は、後ほどcurlを使って順番に確認していきます。
保護されたリソースでRubyアプリをテストする
認証フローについては前のセクションの図で一部説明しました。全体を網羅してテストするには、保護対象となるリソースが必要です。
Productモデルはすでにあるので、次は商品用のコントローラーと、商品およびトークンへアクセスするためのルーティングが必要です。
rails g controller Product indexでコントローラーを作成し、config/routes.rbにルートを定義しましょう。
準備が整ったら、curlでテストします。認証用のユーザーとアクセス先の商品リソースはすでに用意済みです。
まず、存在しないユーザーでJWTの取得を試みます。すると、認証失敗を示すレスポンスが返ってきます。
次に、先ほど作成した正しいユーザーで同じ操作を行うと、署名済みのJSON Web Tokenが発行されます。
このトークンを保持したまま、不正なトークン(トークン内のランダムな1文字を変更したもの)で商品リソースへのアクセスを試みると、アクセスは拒否されます。
しかし、先ほどサーバーから受け取った有効なトークンで同じリクエストを送れば、商品リソースへのアクセスが許可されます。
以上で完了です!JSON Web Tokenを使ってRubyアプリケーションの保護に成功しました!
まとめ
本記事では、JSON Web Tokenの仕組みについて解説しました。まずJWTの基本——構造やベストプラクティス——を取り上げ、その後jwt gemを使ったシンプルなJWT認証を実装しました。
本記事が皆さんのお役に立てば幸いです。Happy coding!
-
Railsでクラスター間のアソシエーションをマスターする:disable_joinsによるマルチデータベース対応の完全ガイド
Railsフレームワークの魅力のひとつは、モデル間のアソシエーション(関連付け)を活用できる点です。Active Recordのアソシエーションを使えば、複雑なSQLクエリを意識することなく、美しいRubyの構文でレコードのコレクションにアクセスできます。しかし、この抽象化が機能するのは、すべてのデータが1つのデータベースに存在している場合だけです。テーブルが複数のデータベースクラスタに分散した瞬間、特定の種類のアソシエーションは動作しなくなります。 本記事では、その境界線がどこにあるのか、そしてRailsがその制約の中で何を提供してくれるのかを詳しく解説します。まず問題が発生する理由と影響を
-
Rubyのメモリリークを見つけて修正する方法:memory_profilerとderailed_benchmarks徹底活用ガイド
本記事は、メモリリークに関する2部構成シリーズの後編です。前編では、Rubyがどのようにメモリを管理し、ガベージコレクション(GC)がどのように動作するのかを解説しました。 大容量メモリを搭載した高性能なマシンを用意することはできるかもしれませんし、アプリを頻繁に再起動すればユーザーが気づかないかもしれません。それでも、メモリ使用量は重要です。 メモリの確保(アロケーション)とガベージコレクションは無料ではありません。メモリリークがあると、本来アプリにさせたい処理ではなく、ガベージコレクションに費やす時間がどんどん増えていきます。 この記事では、メモリリークの発見と診断に役立つツールについて、