第28章|今さら学ぶ「API認証とレスポンス設計」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- jbuilder — JSONの見た目を整えるテンプレート職人
- トークン認証の基本 — 「入場チケット」で本人確認する
- JWT認証 — 「使い捨てパスポート」で身分を証明する
- OAuth — 「Googleに本人確認を代行してもらう」仕組み
- エラーレスポンスの統一設計
🏠 たとえ話で掴む「API認証」
Webページのログインは「ホテルのチェックイン」でした(→ 第21章)。API認証は少し違います。
APIはブラウザのCookieが使えない場面が多いため、 入場チケット(トークン) 方式になります。
| 認証方式 | たとえ | 特徴 |
|---|---|---|
| セッション(Cookie) | ホテルのルームキー | ブラウザ向き。サーバがセッションを管理 |
| トークン認証 | 入場チケット | API向き。リクエストごとにチケットを提示 |
| JWT | 使い捨てパスポート | 自己完結型。サーバに問い合わせ不要 |
| OAuth | 「Googleに聞いてくれ」 | 他サービスに認証を委託 |
API認証とは何か — 技術的な定義
第27章で学んだAPIは、URLとHTTPメソッドでデータを操作する仕組みでした。しかし「誰でも DELETE /api/v1/articles/1 できてしまう」状態では困ります。 API認証 は「このリクエストを送っているのは誰か」を確認する仕組みです。
通常のWebアプリでは Cookie + セッション で認証します(→ 第6章、第21章)。ブラウザが自動的にCookieを付けてくれるので、ユーザーは意識せずに認証済みの状態を維持できます。
しかしAPIのクライアントはブラウザとは限りません。スマホアプリ、他社のサーバ、CLIツールなど、Cookieの仕組みが使えない環境も多いです。そこで トークン認証 が使われます。クライアントは毎回のリクエストに トークン(認証情報を表す文字列) を付けて、自分が誰かを証明します。
【セッション認証(ブラウザ向き)】
ブラウザ → Cookie: session_id=abc123 → サーバがセッションを検索 → ユーザー特定
【トークン認証(API向き)】
クライアント → Authorization: Bearer xyz789 → サーバがトークンを検証 → ユーザー特定
トークン認証にもいくつかの方式がありますが、この章では シンプルなAPIトークン、 JWT、 OAuth の3つを扱います。
もう1つ重要なのが レスポンス設計 です。HTMLを返す通常のアプリと違い、APIはJSONで情報を返します。どのフィールドを含めるか、エラー時はどんな形式で返すかといった「JSONの設計」も、APIの使いやすさを左右します。
🎨 jbuilder — JSONの見た目を整える
第27章では serialize_article メソッドでJSONを手動で組み立てました。これでも動きますが、レスポンスが複雑になると管理しづらくなります。
jbuilder は、APIのJSONレスポンスをERBのように テンプレート で組み立てるGemです。Railsチームが管理しているgemで、Gemfileに追加して使います。
# Gemfile
gem "jbuilder"
# app/views/api/v1/articles/show.json.jbuilder
json.id @article.id
json.title @article.title
json.body @article.body
json.status @article.status
json.created_at @article.created_at.iso8601
json.author do
json.id @article.user.id
json.name @article.user.name
end
json.tags @article.tags do |tag|
json.id tag.id
json.name tag.name
end
json.likes_count @article.likes.count
json.comments_count @article.comments.count
// 出力されるJSON
{
"id": 1,
"title": "Railsのルーティングを完全理解する",
"body": "ルーティングとは...",
"status": "published",
"created_at": "2025-02-15T10:30:00Z",
"author": { "id": 3, "name": "田中太郎" },
"tags": [
{ "id": 1, "name": "Ruby" },
{ "id": 3, "name": "Rails" }
],
"likes_count": 5,
"comments_count": 2
}
コントローラ側は render json: ではなく、通常の render でjbuilderテンプレートを呼び出します。
# app/controllers/api/v1/articles_controller.rb
def show
@article = Article.find(params[:id])
# → 自動的に app/views/api/v1/articles/show.json.jbuilder が使われる
end
jbuilder の Partial
ビューのPartial(→ 第13章)と同じように、jbuilderでも共通のJSONテンプレートを切り出せます。
# app/views/api/v1/articles/_article.json.jbuilder
json.extract! article, :id, :title, :status, :created_at
json.author do
json.extract! article.user, :id, :name
end
# app/views/api/v1/articles/index.json.jbuilder
json.articles @articles, partial: "api/v1/articles/article", as: :article
json.total_count @articles.total_count
json.current_page @articles.current_page
json.extract! は指定したカラムだけを抜き出す便利メソッドです。password_digest などの機密情報を含めてしまうリスクを減らせます。
🎫 トークン認証 — 入場チケット方式
最もシンプルなAPI認証は、 APIトークン をHTTPヘッダーに付けてリクエストする方式です。
# リクエスト(クライアント側)
GET /api/v1/articles HTTP/1.1
Authorization: Bearer abc123xyz789
# ↑ トークン(入場チケット)
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
before_action :authenticate_api_user!
private
def authenticate_api_user!
token = request.headers["Authorization"]&.split(" ")&.last
@current_api_user = User.find_by(api_token: token)
unless @current_api_user
render json: { error: "認証に失敗しました" }, status: :unauthorized
end
end
def current_api_user
@current_api_user
end
end
end
end
この方式はシンプルですが、トークンが漏洩したら悪用されるリスクがあります。トークンの 有効期限がない のも弱点です。これを改善したのがJWTです。
💡
ActionController::APIはApplicationController(ActionController::Base)の軽量版です。APIに不要なCSRF対策やCookie処理、ビューのレンダリング機能が省かれているため、APIコントローラの基底クラスに適しています。
🛂 JWT(JSON Web Token)— 使い捨てパスポート
JWT(ジョット) は、ユーザー情報を暗号化して トークン自体に埋め込む 方式です。サーバ側でセッションを保持しなくて済むため、 ステートレス な認証が実現できます。
JWTの構造(3つの部分をドットで繋いだ文字列)
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDgwODAwMDB9.xxx
├── Header ────────┤├── Payload ──────────────────────────┤├── Signature ┤
(暗号方式) (ユーザーID, 有効期限 等) (署名)
Header — 暗号化アルゴリズム(HS256等)を指定します。
Payload — ユーザーIDや有効期限(exp)などの情報を含みます。Base64でエンコードされているだけなので、 暗号化はされていません。誰でも中身を読めます。パスワードのような機密情報は絶対に入れてはいけません。
Signature — HeaderとPayloadを秘密鍵で署名したもの。この署名のおかげで「トークンが改ざんされていないか」を検証できます。
# Gemfile
gem "jwt"
# app/services/jwt_service.rb
class JwtService
# JWT署名用の専用秘密鍵(credentials に jwt_secret_key を追加して管理する)
# 例: EDITOR=vi rails credentials:edit で以下を追記
# jwt_secret_key: "ランダムな長い文字列(bin/rails secret で生成)"
#
# ⚠️ secret_key_base(Railsセッション署名用の鍵)を流用しない。
# JWT専用の鍵を別途用意するのがベストプラクティス。
SECRET_KEY = Rails.application.credentials.jwt_secret_key
# トークンを発行する
def self.encode(user_id, exp: 24.hours.from_now)
payload = { user_id: user_id, exp: exp.to_i }
JWT.encode(payload, SECRET_KEY)
end
# トークンを検証してユーザーIDを取り出す
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY).first
decoded["user_id"]
rescue JWT::DecodeError, JWT::ExpiredSignature
nil
end
end
ログインAPI(トークン発行)
# app/controllers/api/v1/sessions_controller.rb
module Api
module V1
class SessionsController < BaseController
skip_before_action :authenticate_api_user!, only: [:create]
# POST /api/v1/login
def create
user = User.authenticate_by(
email_address: params[:email_address],
password: params[:password]
)
if user
token = JwtService.encode(user.id)
render json: { token: token, user: { id: user.id, name: user.name } }
else
render json: { error: "メールアドレスまたはパスワードが正しくありません" },
status: :unauthorized
end
end
end
end
end
BaseControllerでJWT認証に切り替える
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
before_action :authenticate_api_user!
private
def authenticate_api_user!
token = request.headers["Authorization"]&.split(" ")&.last
user_id = JwtService.decode(token)
@current_api_user = User.find_by(id: user_id)
unless @current_api_user
render json: { error: "認証に失敗しました" }, status: :unauthorized
end
end
def current_api_user
@current_api_user
end
end
end
end
クライアント側の使い方は以下の流れです。
① POST /api/v1/login (メールアドレス+パスワードを送信)
→ レスポンス: { "token": "eyJhbG..." }
② GET /api/v1/articles (Authorization ヘッダーにトークンを付ける)
→ Authorization: Bearer eyJhbG...
→ レスポンス: 記事一覧のJSON
🤝 OAuth — 他サービスに認証を委託する
OAuth は「Googleに本人確認を代行してもらう」仕組みです。「Googleでログイン」「GitHubでログイン」はこの仕組みです。
① ユーザーが「Googleでログイン」ボタンを押す
↓
② KnowledgeNote が Google に「この人を認証してください」とリダイレクト
↓
③ ユーザーが Google のログイン画面でログインする
↓
④ Google が KnowledgeNote に「認証OK、この人のメールはxxx@gmail.comです」と返す
(コールバックURLにリダイレクト + 認可コードを付与)
↓
⑤ KnowledgeNote が認可コードをアクセストークンに交換し、
メールアドレスでユーザーを検索 or 作成してログイン
ユーザーのパスワードを KnowledgeNote が受け取る必要がないため、セキュリティ面で優れています。
RailsでのOAuth実装(OmniAuth)
# Gemfile
gem "omniauth"
gem "omniauth-google-oauth2"
gem "omniauth-rails_csrf_protection" # CSRF対策
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2,
Rails.application.credentials.dig(:google, :client_id),
Rails.application.credentials.dig(:google, :client_secret)
end
OAuthの実装は設定項目が多いため、この章では流れだけを押さえる。実装の詳細は公式ドキュメントが詳しい。
⚠️ エラーレスポンスの統一設計
APIのエラーは 統一したフォーマット で返すのがベストプラクティスです。クライアント側が「どの形式でエラーが返ってくるか」を予測できないと、エラーハンドリングが困難になります。
// 成功時
{
"id": 1,
"title": "Ruby入門"
}
// エラー時(バリデーション)
{
"errors": [
"タイトルを入力してください",
"本文を入力してください"
]
}
// エラー時(認証)
{
"error": "認証に失敗しました"
}
// エラー時(権限)
{
"error": "この操作は許可されていません"
}
# app/controllers/api/v1/base_controller.rb(エラーハンドリング部分)
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "リソースが見つかりません" }, status: :not_found
end
rescue_from Pundit::NotAuthorizedError do |e|
render json: { error: "この操作は許可されていません" }, status: :forbidden
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: "パラメータが不足しています: #{e.param}" }, status: :bad_request
end
ここで重要なのは、バリデーションエラーは 配列 (errors)、それ以外は 単一メッセージ (error)で返すパターンです。クライアント側は errors キーがあれば複数のエラーをまとめて表示し、error キーなら1つのメッセージを表示する、という実装ができます。
🛠️ KnowledgeNoteでの具体例
KnowledgeNoteでは、HTMLビュー用の通常コントローラとAPI用コントローラの両方を持ちます。認証方式が異なるだけで、ビジネスロジック(Service Object等)は共通で使えます。
# 通常のコントローラ(セッション認証 + HTML)
class ArticlesController < ApplicationController
def create
@article = current_user.articles.build(article_params)
result = ArticlePublishService.new(
article: @article,
tag_names: params[:tag_names],
user: current_user
).call
if result
redirect_to @article, notice: "記事を投稿しました"
else
render :new, status: :unprocessable_entity
end
end
end
# APIコントローラ(JWT認証 + JSON)
module Api
module V1
class ArticlesController < BaseController
def create
@article = current_api_user.articles.build(article_params)
result = ArticlePublishService.new(
article: @article,
tag_names: params[:tag_names],
user: current_api_user
).call
if result
render json: serialize_article(@article), status: :created
else
render json: { errors: @article.errors.full_messages },
status: :unprocessable_entity
end
end
end
end
end
Service Object(→ 第25章)を使ってビジネスロジックを切り出しておけば、HTMLコントローラとAPIコントローラで 同じロジックを共有 できます。違うのは認証方式とレスポンス形式だけです。
💼 面接で聞かれたら?
Q:JWTとは何ですか?
「JWTはJSON Web Tokenの略で、ユーザー情報と有効期限を暗号化してトークンに埋め込む認証方式です。サーバ側でセッションを保持しないステートレスな仕組みなので、API認証に適しています。トークンはHeader・Payload・Signatureの3部構成で、秘密鍵で署名されているため改ざんを検知できます。有効期限を短く設定し、期限切れトークンは拒否する仕組みにします。」
深掘りされたら:
- 「OAuthとは?」→ GoogleやGitHubなどの外部サービスに認証を委託する仕組み。アプリがユーザーのパスワードを直接受け取らないためセキュリティが高い。「Googleでログイン」はこの仕組み。
- 「JWTのPayloadは暗号化されている?」→ Base64エンコードされているだけで、暗号化はされていない。誰でもデコードして中身を読める。パスワードなどの機密情報は絶対に入れてはいけない。署名(Signature)は改ざん検知のためのもので、中身を隠すためのものではない。
- 「セッション認証とトークン認証の使い分けは?」→ ブラウザ(HTMLアプリ)はCookieベースのセッション認証が自然。スマホアプリやSPA、外部連携などブラウザ以外のクライアントにはトークン認証が適している。
🔗 もっと深く知りたい人へ(1次情報リンク)
- jbuilder(GitHub) — Rails標準のJSONテンプレートエンジン
- JWT.io — JWTの仕組みを視覚的に解説。デバッグツールもある
- OmniAuth(GitHub) — RailsでのOAuth実装の定番gem
-
Rails ガイド:Rails で API 専用アプリケーションを作る —
ActionController::APIの使い方
まとめ
- ✅ API認証は「入場チケット」方式。CookieではなくAuthorizationヘッダーにトークンを付ける
- ✅ jbuilderでJSONレスポンスをテンプレートとして整形。
extract!やPartialも使える - ✅ JWTはトークン自体にユーザー情報を埋め込む。ステートレスでAPI認証に最適
- ✅ JWTのPayloadは暗号化されていない。機密情報を入れてはいけない
- ✅ OAuthは外部サービスに認証を委託。パスワードを預からないので安全
- ✅ エラーレスポンスは統一フォーマットにする。バリデーションは配列、それ以外は単一メッセージ
- ✅ Service Objectを使えば、HTMLコントローラとAPIコントローラでロジックを共有できる
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに