Contents
API における認証と認可の基本概念と Flask での位置付け
Web API が外部アプリやフロントエンドとデータをやり取りする際、「誰が」 リクエストを送っているか(認証)と 「何ができる」 のか(認可)を明確にしなければなりません。
Flask はシンプルなリクエストハンドラを提供しますが、認証・認可のロジックはミドルウェアやデコレータで組み合わせて実装する必要があります。この章では代表的な 3 種類の方式と選択基準を示し、Flask での実装イメージを掴んでもらいます。
Basic 認証の概要
Basic 認証は HTTP ヘッダー Authorization: Basic <base64> にユーザー名とパスワードをそのまま送る方式です。実装は数行で済むため テスト用や内部ツール では便利ですが、平文に近い情報がネットワーク上に流れる点で 本番環境のインターネット公開 API には不向き です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from flask import Flask, request, jsonify import base64 app = Flask(__name__) USERS = {"alice": "s3cr3t"} def check_basic_auth(auth_header: str) -> bool: if not auth_header or not auth_header.startswith("Basic "): return False b64_credentials = auth_header.split()[1] credentials = base64.b64decode(b64_credentials).decode() username, password = credentials.split(":") return USERS.get(username) == password @app.route("/basic-protected") def basic_protected(): if not check_basic_auth(request.headers.get("Authorization")): return jsonify({"msg": "Unauthorized"}), 401 return {"msg": "Hello, Basic Auth user!"} |
結論:導入コストは低いが、HTTPS が必須でありスケーラビリティや細かい権限管理が求められる場面では代替手段を検討すべきです。
Token 認証(JWT)とは
JSON Web Token (JWT) は 自己完結型 の文字列で、ペイロードにユーザー ID・ロール・有効期限などを署名付きで格納します。サーバーはトークンの検証だけで認可判断ができるため、ステートレスな API に最適です。
- ステートレス:セッションストレージ不要
- スケールしやすい:ロードバランサ配下でも同一ロジックで処理可能
- 柔軟なクレーム:
role,scopeなどを自由に追加できる
結論:モダンな SPA やマイクロサービス間の認証に広く採用され、Flask‑JWT‑Extended が公式にサポートしています。
OAuth2 の概要と選択基準
OAuth2 は「リソースオーナーが第三者アプリにアクセス権を委譲」するフレームワークです。認可コードフロー、クライアントクレデンシャルフロー、パスワードフローなど複数のグラントタイプがありますが、現在は Implicit フローは非推奨 であり、実装から除外すべきです。
| 用途 | 推奨グラント | 主なシナリオ |
|---|---|---|
| ユーザーが自分の Google アカウントでログイン | Authorization Code Flow + PKCE | SNS 連携、外部 IdP 利用 |
| サーバ間 API 呼び出し(クライアント認証のみ) | Client Credentials | バックエンドサービス間 |
| モバイル/SPA のトークン取得 | Authorization Code Flow + PKCE | 従来の SPA でも安全に利用可能 |
注記:Implicit フローはブラウザベースの攻撃サーフェスが大きく、RFC 6749 でも「非推奨」と明示されています。実装例から除外し、PKCE を必ず組み合わせた Authorization Code Flow のみを推奨します。
結論:外部 IdP(Google, Azure AD 等)と連携したい場合や 委譲認可 が必要なシステムでは OAuth2 が必須です。内部サービスだけで完結するなら JWT 単体でも十分です。
Flask‑JWT‑Extended のインストールと基本設定
この章では Flask アプリに安全な JWT 機能を組み込むまでの手順を、実務ですぐに使える形で紹介します。まずは依存パッケージの導入から、鍵管理・アルゴリズム選択まで網羅的に解説します。
ライブラリの導入手順
|
1 2 3 4 5 6 7 |
# 仮想環境を作成し、アクティベートする python -m venv .venv source .venv/bin/activate # Flask と JWT 関連パッケージをインストール pip install Flask Flask-JWT-Extended[crypto] SQLAlchemy bcrypt |
[crypto] オプションにより PyJWT の暗号化拡張が有効になり、HS256 以外のアルゴリズム(例: RS256)も利用可能です。
シークレットキー・アルゴリズムの設定方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import os from flask import Flask from flask_jwt_extended import JWTManager app = Flask(__name__) # ==== 環境変数から安全に取得 ==== app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY", "dev-secret-key") # RS256 を利用する場合は下記を有効化し、鍵ファイルまたは文字列を設定 # app.config["JWT_PRIVATE_KEY"] = os.getenv("JWT_PRIVATE_KEY") # app.config["JWT_PUBLIC_KEY"] = os.getenv("JWT_PUBLIC_KEY") app.config["JWT_ALGORITHM"] = "HS256" # RS256 に変更可能 app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 3600 # 1 時間 (秒) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = 30 * 86400 # 30 日 jwt = JWTManager(app) |
- シークレットキーは必ず外部管理(環境変数、Vault、KMS 等)。コードベースにハードコーディングしないこと。
- アルゴリズムはデフォルトの HS256 でも十分ですが、公開鍵方式 (RS256) にするとトークン検証を別サービスへ委譲でき、キー漏洩時の被害範囲が限定されます。
ユーザーモデル例(SQLAlchemy)とパスワードハッシュ化
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy(app) class User(db.Model): __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) role = db.Column(db.String(20), default="user") def set_password(self, raw_pw: str) -> None: self.password_hash = generate_password_hash(raw_pw) # bcrypt が内部で使用 def check_password(self, raw_pw: str) -> bool: return check_password_hash(self.password_hash, raw_pw) |
werkzeug.securityは bcrypt を内部的に利用し、ソルト管理を自動化します。- パスワードは決して平文で保存せず、必ずハッシュ化した値だけを DB に格納してください。
RS256 鍵管理のベストプラクティス
RS256(RSA SHA‑256)を採用する場合、公開鍵 / 秘密鍵の安全な保管とロード方法 が重要です。以下は実務で推奨される手順です。
- 鍵ペアの生成
bash
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private_key.pem -out public_key.pem - ファイルパーミッション
private_key.pemは600(所有者のみ読み取り)にし、コンテナやサーバ上でマウントする際はシークレットボリュームを使用。public_key.pemは644でも問題ありませんが、外部に漏れても安全です。- 環境変数またはファイル経由で Flask に設定
python
# 環境変数に PEM 全体を格納する例(Base64 エンコード推奨)
app.config["JWT_PRIVATE_KEY"] = os.getenv("JWT_PRIVATE_KEY")
app.config["JWT_PUBLIC_KEY"] = os.getenv("JWT_PUBLIC_KEY")
app.config["JWT_ALGORITHM"] = "RS256"
または、ファイルから直接読み込む方法:
python
with open("/run/secrets/jwt_private_key.pem") as f:
app.config["JWT_PRIVATE_KEY"] = f.read()
with open("/run/secrets/jwt_public_key.pem") as f:
app.config["JWT_PUBLIC_KEY"] = f.read()
- ローテーション戦略
- 定期的に新しい鍵ペアを生成し、
kid(key ID)クレームでバージョン管理。 - 旧鍵は一定期間保持し、既存トークンの検証ができるように「鍵リスト」方式で運用。
セキュリティ上のポイント:RSA 鍵はサイズが大きいため、環境変数やシークレット管理ツールで扱う際は Base64 エンコードし、ロード時にデコードして使用してください。
ログインエンドポイントでのトークン発行と保護ルートの実装
ここでは POST /login でアクセストークン・リフレッシュトークンを同時に発行し、以降は @jwt_required() デコレータやカスタムロールチェックでエンドポイントを保護する方法を示します。
アクセストークン・リフレッシュトークンの生成
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from flask import request, jsonify from flask_jwt_extended import create_access_token, create_refresh_token @app.route("/login", methods=["POST"]) def login(): data = request.get_json() username = data.get("username") password = data.get("password") user = User.query.filter_by(username=username).first() if not user or not user.check_password(password): return jsonify({"msg": "Bad credentials"}), 401 additional_claims = {"role": user.role} access_token = create_access_token( identity=user.id, additional_claims=additional_claims ) refresh_token = create_refresh_token(identity=user.id) return jsonify(access_token=access_token, refresh_token=refresh_token) |
identityには 最小限の情報(ユーザー ID)だけを入れ、権限はクレーム (role) として付与します。- 有効期限は設定ファイルで一元管理し、短めに保つことでリスクを低減できます。
@jwt_required デコレータによる認証必須エンドポイント
|
1 2 3 4 5 6 7 8 9 |
from flask_jwt_extended import jwt_required, get_jwt_identity @app.route("/profile") @jwt_required() def profile(): user_id = get_jwt_identity() user = User.query.get(user_id) return {"username": user.username, "role": user.role} |
- デコレータはリクエストごとにトークンの署名・有効期限を検証し、失効チェック(後述)も自動的に行います。
ロールベースアクセス制御(RBAC)の実装例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
from functools import wraps from flask_jwt_extended import get_jwt def role_required(required_role: str): """指定ロールがトークンに含まれているかをチェックするデコレータ""" def decorator(fn): @wraps(fn) @jwt_required() def wrapper(*args, **kwargs): claims = get_jwt() if claims.get("role") != required_role: return {"msg": "Forbidden: insufficient role"}, 403 return fn(*args, **kwargs) return wrapper return decorator @app.route("/admin/dashboard") @role_required("admin") def admin_dashboard(): return {"msg": "Welcome, admin!"} |
get_jwt()が現在のトークンから全クレームを取得し、ロール比較でアクセス制御します。- 必要に応じて
scopeクレームや複数ロール(配列)への対応も容易です。
まとめ:アクセストークンとリフレッシュトークンを分離し、@jwt_required とカスタムデコレータで RBAC を実装すれば、Flask API はシンプルかつ安全な認可層を持ちます。
トークン失効(ブラックリスト)と期限切れハンドリング
JWT は自己完結型なので「サーバ側で無効化できない」という課題があります。実務では ブラックリスト(例: Redis)を併用し、ログアウトやパスワード変更時にトークンを失効させます。
ブラックリスト方式の概要
- トークン生成時に
jti(JWT ID)クレームが自動付与される。 - ログアウト・パスワード変更時に
jtiを Redis のセットへ保存し、TTL はトークン残存時間と同等に設定する。 - 各リクエストで
@jwt_required()が呼び出された際にjtiがブラックリストに存在すれば 無効 と判断する。
安全な Redis 接続例とエラーハンドリング
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import redis, logging logger = logging.getLogger(__name__) def get_redis_client(): try: client = redis.StrictRedis( host=os.getenv("REDIS_HOST", "localhost"), port=int(os.getenv("REDIS_PORT", 6379)), db=0, socket_timeout=2, # 接続タイムアウトを短く設定 decode_responses=True, ) # ping で接続確認(例外が出なければ OK) client.ping() return client except redis.RedisError as exc: logger.error(f"Redis connection failed: {exc}") # 本番環境ではアプリ起動を止めるか、フェイルオーバー戦略へ移行する raise SystemExit("Unable to connect to Redis – aborting.") from exc r = get_redis_client() |
- タイムアウト と 例外捕捉 を必ず入れ、接続失敗時はログに残すと同時にアプリの起動を中止させることで、ブラックリスト機能が無効になることによるセキュリティギャップを防げます。
ブラックリストチェックローダー
|
1 2 3 4 5 6 7 8 9 |
from flask_jwt_extended import JWTManager jwt = JWTManager(app) @jwt.token_in_blocklist_loader def check_if_token_revoked(jwt_header, jwt_payload): jti = jwt_payload["jti"] return r.get(jti) is not None # True → ブロックリストにある=失効 |
ログアウトエンドポイント例
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import time from flask_jwt_extended import get_jwt, jwt_required @app.route("/logout", methods=["POST"]) @jwt_required() def logout(): jti = get_jwt()["jti"] exp_timestamp = get_jwt()["exp"] ttl = max(exp_timestamp - int(time.time()), 0) r.setex(jti, ttl, "revoked") return {"msg": "Successfully logged out"} |
トークンリフレッシュ時のエラーハンドリング(最新API)
|
1 2 3 4 5 6 7 8 9 10 11 12 |
from flask_jwt_extended import jwt_required, get_jwt_identity, create_access_token @app.route("/refresh", methods=["POST"]) @jwt_required(refresh=True) # ← 旧 API は廃止済み def refresh(): try: identity = get_jwt_identity() new_access = create_access_token(identity=identity) return {"access_token": new_access} except Exception: # 例外は InvalidTokenError 系列が投げられる return {"msg": "Refresh token invalid or expired"}, 401 |
@jwt_required(refresh=True)がリフレッシュトークンの有無とブラックリストチェックを自動で行います。- カスタムエラーハンドラを別途登録すれば、全体のレスポンスフォーマットを統一できます。
まとめ:Redis を用いたブラックリストは「即時失効」を実現する有力手段です。接続エラーへの対策と @jwt_required(refresh=True) の正しい利用で、トークンライフサイクル全体の安全性を高められます。
OAuth2 認可コードフローの簡易実装と本番環境向けセキュリティベストプラクティス
外部 IdP と連携しつつ自前 JWT を発行したいケースでは、Authlib + Flask‑JWT‑Extended の組み合わせが最もシンプルです。以下に PKCE 付き Authorization Code Flow の実装例と、運用時の重要チェックポイントをまとめます。
Authlib を使った認可コードフロー例(PKCE 必須)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
import os, logging from authlib.integrations.flask_client import OAuth from flask import url_for, redirect, session, jsonify, request oauth = OAuth(app) google = oauth.register( name='google', client_id=os.getenv("GOOGLE_CLIENT_ID"), client_secret=os.getenv("GOOGLE_CLIENT_SECRET"), access_token_url='https://oauth2.googleapis.com/token', authorize_url='https://accounts.google.com/o/oauth2/v2/auth', client_kwargs={ 'scope': 'openid email profile', 'code_challenge_method': 'S256', # PKCE を有効化 }, ) @app.route("/login/google") def login_google(): redirect_uri = url_for('auth_callback', _external=True) return google.authorize_redirect(redirect_uri) @app.route("/auth/callback") def auth_callback(): try: token = google.authorize_access_token() except Exception as exc: logging.error(f"OAuth callback error: {exc}") return jsonify({"msg": "Authorization failed"}), 400 user_info = google.parse_id_token(token) # DB にユーザーがいなければ作成 user = User.query.filter_by(email=user_info["email"]).first() if not user: user = User(username=user_info["email"], role="user") db.session.add(user) db.session.commit() # 自前 JWT(アクセストークン)を発行 access_token = create_access_token(identity=user.id, additional_claims={"role": user.role}) refresh_token = create_refresh_token(identity=user.id) resp = jsonify(access_token=access_token, refresh_token=refresh_token) # HttpOnly Cookie に保存(XSS 対策) resp.set_cookie( "access_token", access_token, httponly=True, secure=True, samesite="Lax" ) return resp |
- PKCE を必ず有効化し、
code_challenge_method: 'S256'を設定します。 - エラーハンドリングを入れることで、IdP 側の認可失敗やネットワーク障害時に安全に落ちます。
JWT と OAuth2 の併用シナリオ
- ユーザーは外部 IdP(Google 等)で認証し Authorization Code を取得。
- アプリはコードをアクセストークンへ交換し、ユーザープロファイル情報 (
sub,emailなど) を取得。 - 取得した情報から自前のユーザーエンティティを作成/検索し、独自 JWT(access/refresh)を発行。
- API 呼び出しは自前 JWT(または HttpOnly Cookie)で認証 → RBAC が適用可能。
この流れにより シングルサインオン と API の統一トークン形式 を同時に実現できます。
本番環境向けセキュリティベストプラクティス
| 項目 | 推奨設定・実装ポイント | 理由 |
|---|---|---|
| HTTPS | TLS 1.2+ を必須、全てのエンドポイントで有効化 | 通信の盗聴・改ざん防止 |
| CORS | Access-Control-Allow-Origin に信頼できるドメインのみを列挙 |
不正オリジンからの呼び出し遮断 |
| トークン保存場所 | HttpOnly, Secure Cookie を推奨(localStorage は XSS リスク) | クライアント側でスクリプトが取得できない |
| シークレット管理 | 環境変数、HashiCorp Vault、AWS KMS などで暗号化保存 | ソースコードに埋め込まない |
| 署名アルゴリズム | RS256(公開鍵)か HS256+長くランダムなキー | 予測不可能な署名で改ざん防止 |
| RS256 鍵配置 | 秘密鍵は 600 権限のファイル、またはシークレットマウントで管理公開鍵は配布用に安全に保存 |
秘密鍵漏洩リスク低減 |
| トークン失効 | Redis 等でブラックリストか、短期有効期限+リフレッシュトークン利用 | 盗難時の被害最小化 |
| ログ監視 | jti と IP アドレスを組み合わせた異常検知ロジックを実装 |
不審な認証パターンを早期発見 |
| PKCE | Authorization Code フローでは必ず使用 | 認可コードの盗聴防止 |
| キー・シークレットのローテーション | 定期的に鍵ペア/シークレットを更新し、kid でバージョン管理 |
長期間同一鍵が使われ続けるリスク回避 |
重要:Implicit フローは非推奨です。全ての新規実装では PKCE 付き Authorization Code Flow のみを採用してください。
まとめ:Authlib と Flask‑JWT‑Extended を組み合わせ、上記ベストプラクティスに従うことで、外部 IdP とのシングルサインオンと自前 API の安全な認可基盤が構築できます。
本稿は「Basic 認証」「JWT」「OAuth2」の比較から実装例、そして運用上のセキュリティ対策まで網羅的に解説しました。読者が自プロジェクトで適切な認証方式を選択し、安全に実装できることを願っています。