1. HOME
  2. ブログ
  3. IT技術
  4. APIの認証について学んで実装してみた(by rails API)

APIの認証について学んで実装してみた(by rails API)

きっかけ

業務でrails APIの環境に出会い、railsの勉強がてら個人で同じ構成で何か作ってみようとした時に、APIでの認証へ具体的な理解が浅いなと思ったので、調べてみることにしました。
この記事では、APIでの認証の基本的な仕組みを理解した上で簡単な実装ができるまでを目指します。

認証とリソースアクセスのフロー

さっそくですが、APIサービスでの認証と認証後のリソースへのアクセスフロー(一例)を図にしてみました。



  1. メールアドレスとパスワードを送信する
  2. サーバ側でユーザーの存在を検証する
  3. 検証に問題なければ、クライアントにトークンを返す
  4. クライアントはサーバから送られてきたトークンをリクエストヘッダーに含めてリクエストを送る
  5. サーバはトークンをデコード・検証し、ユーザーの確認が取れた場合、リソースへのアクセスを行う
  6. リクエストに対するレスポンスを返却する

(RESTfulを意識した)APIはステートレスであることが望ましく、各通信が独立している必要があります。つまり、サーバ側はそのリクエストが誰のものか特定する情報(ここではトークン)を保持しません。

比較としてセッション方式のフローを図にしてみました。



この方式だとサーバはセッションという形でユーザーの情報を保持していますが、上のトークン方式の場合、サーバーはトークンを払い出すだけでトークン自体の管理はしていません。

サーバ側で状態を持たずに認証を可能にする仕組みがトークンベースの認証になります。

認証処理の鍵は「トークン」

ここまでで、トークンを使ったAPIの認証フローはイメージできたと思います。

ここからは、トークン自体に焦点を当てていきます。

JWT

APIの認証で一般的に使われるトークンには規格が存在します。それがRFC7519で定義されている「JWT」です。

JWTは「JSON Web Token」の略で文字通りJSON形式で表現されたトークンです。JWTは大きく3つの構造から成り立ちます。

  • ヘッダー
  • ペイロード
  • 署名

1つずつ見ていきましょう。

ヘッダー

トークンの処理方法に関するメタデータを含みます。具体的にはトークンタイプや署名生成に使われるアルゴリズムを指定します。

ペイロード

トークンの本体で、ユーザー識別情報、有効期限など、様々なクレームを含みます。
ここでのクレームとは1つの「キー:値」1セットのイメージで大丈夫です。
JSONのキーが「クレーム名」、値が「クレーム値」という表現がされます。

ペイロード内のクレームには3種類あります。

Registered Claim Name

RFC7519で予約されているクレーム名です。
IANAの”JSON Web Token Claims”で定義されているクレーム名の内、特に限定して指定されています。

具体的には以下になります。

  • iss(発行者)
  • sub(件名)
  • aud(オーディエンス)
  • exp(有効期限)
  • nbf(有効になる日時)
  • iat(発行日時)
  • jti(JWTを一意に特定する値)

Public Claim Name

IANAの”JSON Web Token Claims”で定義されているクレーム名。
JWT使用者間で共有される情報(=外部サービスでも使われ得る情報)が該当します。

Private Claim Name

特定のアプリケーション間でのみ意味を持ち、一般に公開しないものが該当し、一般的にはこれを利用します。

ペイロードの具体例は以下になります。

署名

署名は

  • 誰がそのJWTを発行したか
  • ヘッダーとペイロードは改ざんされていないか

を保証するために付け加えられます。

署名生成の流れは以下のイメージです。

  1. ヘッダー・ペイロードをBase64 URLエンコード
  2. それぞれを「.」で連結
  3. 連結文字列をヘッダー「alg」の方式でハッシュ化
  4. ハッシュ化した文字列をサーバの秘密鍵で署名
  5. 署名した値をBase64 URLでエンコード

JWTの生成とセキュリティ面の考慮

ここまで見てきたJWTの3つの構造はエンコードされると「.」で繋いだ文字列になります。

.区切りで「ヘッダー」「ペイロード」「署名」の構造となっており、この文字列をデコードするとJSON形式になります。

ここでのエンコードとは、URLにくっつけても大丈夫な形式Base64 URLというフォーマットで行われますが、これは暗号化しているわけでなく、単に形式を変えているだけであり、セキュリティ面を考慮するならHTTPSで通信する必要があります。
つまり、署名を利用すると改ざんの検知が可能にはなりますが、JWT自体は丸見えのデータです。

トークン失効戦略

ここまで、トークン認証のフロートJWTの中身について見てきました。
冒頭で、サーバはトークンを生成するが、それ自体を管理しないと述べました。ということは、有効期限切れ以外でトークンを無効化させる方法は自前で用意する必要があります。

そのための方法は複数あり、今回は3つ取り上げます。

ブラックリスト方式

トークンに付与される一意のID「jti」を保存するテーブルを用意し、無効化したいトークンのjtiをテーブルに格納していきます。

このテーブルに保存されているjtiに紐づくトークンは利用できないものとして扱います。

ホワイトリスト方式

ブラックリスト方式とは逆に、許可したいトークンのjtiを保存し、このテーブルに紐づくトークンのみ利用可能とします。

JTI一致方式

ユーザーテーブルのカラムにjti列を生成し、トークン生成時に現在設定されているjtiの値をJWTに含めます。
ログアウト時に、ユーザーテーブルのjtiカラムの値を更新することで、既存トークンが送られてきた際に、一致しないため無効と判断できます。
次回トークンを生成するときは、更新後のjtiの値をトークンに含め、以降ログアウト→ログインで繰り返します。

※この記事ではJTI一致方式で実装していきます。

実装

ここからは実際にruby on railsでAPIの認証を実装していきます。
※前提としてrailsのAPIモードでプロジェクトを作成しているものとします。

gemのインストール

Gemfileに以下を記入し、bundle installを実行します。

UserモデルとPostモデルの作成

各マイグレーションファイルとモデルを作成し、rails db:migrateを実行します。

  • User
    • パスワード暗号化のために、モデルにhas_secure_passwordを書きます。
    • パスワードカラムはpassword_digestという名前にします。
  • Post

ついでにseederも作成し、rails db:seedを実行してダミーデータを作成します。

ログイン処理

ルーティングとコントローラでログイン処理(トークン生成)を実装します。

この部分で行っていることは以下です。

  1. メールアドレス、パスワードの検証(ユーザー情報取得)
  2. 1が成功の場合ペイロード生成
    1. jti
    2. exp
    3. user_id
  3. gem 'jwt'を使ってJWTトークンを生成&Base64 URLエンコード
  4. トークンをレスポンスに含めて返す

    ここまでできたら、http://localhost:3000/loginへリクエストを飛ばしてみます。

    以下のようなレスポンスが返ってきていたら成功です。

    トークン検証とリソースアクセス

    生成したトークンを使って今度はリソースへのアクセスを行います。

    app/controllers/application_controller.rb を修正し、各コントローラ処理の前にトークン検証処理をはさみます。(veryfiy_tokenの処理)

    ここで地味に罠なのが、request.headers['Authorization']の値はBearer XXXXXXXXの形式になっていることです。"Bearer"が邪魔なので、半角スペースを区切りに分離し、トークン文字列のみ抽出しています。(しばらくこれに気づかずにエラーと格闘していました。)

    次に、posts_controller.rbを作成します。

    このコントローラーはApplicationControllerを継承しているので、上のトークン検証処理通過して実行されます。

    また、このままだとログイン処理時にトークン検証が強制されてしまうのでスキップする処理を追加します。

    :createアクションのみ、トークン検証処理をスキップする記述です。

    ここで、上で生成されたトークンをAuthorization BearerTokenに指定して/postsにリクエストをしてみます。

    このようなレスポンスが戻ってきたら成功です。

    ログアウト処理

    最後にログアウト処理を実装します。
    app/controllers/sessions_controller.rbを開き、以下を追加します。

    これは、ログアウト処理時に現在ログインしているユーザーのjtiカラムの値を更新しています。
    そうすることで、前回のjtiをペイロードに持つトークンが送られてきた場合、verify_tokenメソッドで弾くことができます。

    /logoutへリクエストを送り、その後前回のトークンで/postsにリクエストを送ってみるとエラーが返ってくると思います。

    おわりに

    jwt使ったトークン認証について実際に実装してみることで流れのイメージが鮮明になりました。
    ここからリフレッシュトークンの実装やサードパーティの認証機能の組み込みなど展開していくのも面白そうだなと思ったので、引き続き調べてみようと思います。

    書いた人はこんな人

    たけちゃん(エンジニア)
    たけちゃん(エンジニア)
    2022年7月に入社しました。開発未経験で未知の領域だらけですが、楽しく学びつつ、早く戦力になれるようにがんばります!

    関連記事

    採用情報

    \ あの有名サービスに参画!? /

    バックエンドエンジニア

    \ クリエイティブの最前線 /

    フロントエンドエンジニア

    \ 世界を変える…! /

    Androidエンジニア

    \ みんなが使うアプリを創る /

    iOSエンジニア