APIの環境に出会い、railsの勉強がてら個人で同じ構成で何か作ってみようとした時に、APIでの認証へ具体的な理解が浅いなと思ったので、調べてみることにしました。この記事では、APIでの認証の基本的な仕組みを理解した上で簡単な実装ができるまでを目指します。認証とリソースアクセスのフローさっそくですが、APIサービスでの認証と認証後のリソースへのアクセスフロー(一例)を図にしてみました。メールアドレスとパスワードを送信するサーバ側でユーザーの存在を検証する検証に問題なければ、クライアントにトークンを返すクライアントはサーバから送られてきたトークンをリクエストヘッダーに含めてリクエストを送るサーバはトークンをデコード・検証し、ユーザーの確認が取れた場合、リソースへのアクセスを行うリクエストに対するレスポンスを返却する(RESTfulを意識した)APIはステートレスであることが望ましく、各通信が独立している必要があります。サーバ側はそのリクエストが誰のものか特定する情報(ここではトークン)を保持しません。比較としてセッション方式のフローを図にしてみました。この方式だとサーバはセッションという形でユーザーの情報を保持していますが、上のトークン方式の場合、サーバーはトークンを払い出すだけでトークン自体の管理はしていません。サーバ側で状態を持たずに認証を可能にする仕組みがトークンベースの認証になります。認証処理の鍵は「トークン」ここまでで、トークンを使ったAPIの認証フローはイメージできたと思います。ここからは、トークン自体に焦点を当てていきます。JWTAPIの認証で一般的に使われるトークンには規格が存在します。それがRFC7519で定義されている「JWT」です。Token」の略で文字通りJSON形式で表現されたトークンです。JWTは大きく3つの構造から成り立ちます。ヘッダーペイロード署名1つずつ見ていきましょう。ヘッダートークンの処理方法に関するメタデータを含みます。具体的にはトークンタイプや署名生成に使われるアルゴリズムを指定します。様々なクレームを含みます。ここでのクレームとは1つの「キー:値」1セットのイメージで大丈夫です。JSONのキーが「クレーム名」、値が「クレーム値」という表現がされます。ペイロード内のクレームには3種類あります。NameRFC7519で予約されているクレーム名です。Claims”で定義されているクレーム名の内、特に限定して指定されています。具体的には以下になります。Claims”で定義されているクレーム名。JWT使用者間で共有される情報(=外部サービスでも使われ得る情報)が該当します。Name特定のアプリケーション間でのみ意味を持ち、一般に公開しないものが該当し、一般的にはこれを利用します。ペイロードの具体例は以下になります。署名は誰がそのJWTを発行したかヘッダーとペイロードは改ざんされていないかを保証するために付け加えられます。署名生成の流れは以下のイメージです。URLでエンコードJWTの生成とセキュリティ面の考慮ここまで見てきたJWTの3つの構造はエンコードされると「.」で繋いだ文字列になります。1eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDAwNTQzOTB9.uIALzjUXmOdsFy0vMetZivYoWq5haiufCKeeNQYzFWw.区切りで「ヘッダー」「ペイロード」「署名」の構造となっており、この文字列をデコードするとJSON形式になります。URLというフォーマットで行われますが、これは暗号化しているわけでなく、単に形式を変えているだけであり、セキュリティ面を考慮するならHTTPSで通信する必要があります。署名を利用すると改ざんの検知が可能にはなりますが、JWT自体は丸見えのデータです。トークン失効戦略ここまで、トークン認証のフロートJWTの中身について見てきました。冒頭で、サーバはトークンを生成するが、それ自体を管理しないと述べました。いうことは、有効期限切れ以外でトークンを無効化させる方法は自前で用意する必要があります。そのための方法は複数あり、今回は3つ取り上げます。ブラックリスト方式トークンに付与される一意のID「jti」を保存するテーブルを用意し、無効化したいトークンのjtiをテーブルに格納していきます。このテーブルに保存されているjtiに紐づくトークンは利用できないものとして扱います。ホワイトリスト方式ブラックリスト方式とは逆に、許可したいトークンのjtiを保存し、このテーブルに紐づくトークンのみ利用可能とします。JTI一致方式ユーザーテーブルのカラムにjti列を生成し、トークン生成時に現在設定されているjtiの値をJWTに含めます。ログアウト時に、ユーザーテーブルのjtiカラムの値を更新することで、既存トークンが送られてきた際に、一致しないため無効と判断できます。次回トークンを生成するときは、更新後のjtiの値をトークンに含め、以降ログアウト→ログインで繰り返します。※この記事ではJTI一致方式で実装していきます。railsでAPIの認証を実装していきます。※前提としてrailsのAPIモードでプロジェクトを作成しているものとします。installを実行します。db:migrateを実行します。Userパスワード暗号化のために、モデルにhas_secure_passwordを書きます。パスワードカラムはpassword_digestという名前にします。db:seedを実行してダミーデータを作成します。1ログイン処理ルーティングとコントローラでログイン処理(トークン生成)を実装します。end19endこの部分で行っていることは以下です。URLエンコードトークンをレスポンスに含めて返すここまでできたら、http://localhost:3000/loginへリクエストを飛ばしてみます。"password"}'以下のようなレスポンスが返ってきていたら成功です。"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI3MmEwMjgzNi01Mjk1LTQ4ZWUtYjBYTQ2NDUiLCJleHAiOjE3MDA1MDU0MzcsInVzZXJfaWQiOjF9.bmLssfADwPtbWgzCqTi2p4QaXY"3}トークン検証とリソースアクセス生成したトークンを使って今度はリソースへのアクセスを行います。修正し、各コントローラ処理の前にトークン検証処理をはさみます。XXXXXXXXの形式になっていることです。"Bearer"が邪魔なので、半角スペースを区切りに分離し、トークン文字列のみ抽出しています。(しばらくこれに気づかずにエラーと格闘していました。)次に、posts_controller.rbを作成します。endこのコントローラーはApplicationControllerを継承しているので、上のトークン検証処理通過して実行されます。このままだとログイン処理時にトークン検証が強制されてしまうのでスキップする処理を追加します。[:create]:createアクションのみ、トークン検証処理をスキップする記述です。BearerTokenに指定して/postsにリクエストをしてみます。トークン文字列"このようなレスポンスが戻ってきたら成功です。最後にログアウト処理を実装します。app/controllers/sessions_controller.rbを開き、以下を追加します。endこれは、ログアウト処理時に現在ログインしているユーザーのjtiカラムの値を更新しています。することで、前回のjtiをペイロードに持つトークンが送られてきた場合、verify_tokenメソッドで弾くことができます。/logoutへリクエストを送り、その後前回のトークンで/postsにリクエストを送ってみるとエラーが返ってくると思います。おわりにjwt使ったトークン認証について実際に実装してみることで流れのイメージが鮮明になりました。ここからリフレッシュトークンの実装やサードパーティの認証機能の組み込みなど展開していくのも面白そうだなと思ったので、引き続き調べてみようと思います。技術力で世の中をもっと面白くエンジニア積極採用中!ライトコードでは、エンジニアを積極採用中!ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。採用情報へたけちゃん(エンジニア)2022年7月に入社しました。Studioモリ(エンジニア)2024.10.31immichを知ってほしいIT技術immichキムくん(エンジニア)2024.10.31ブログ一覧エンジニア大募集中!ライトコードでは、エンジニアを積極採用中です。WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!また、フリーランスエンジニア様も大募集中です。